paperclipai/paperclip · error · UnsafeChatPublicationError
External chat progress state is invalid
Error message
External chat progress state is invalid
What it means
projectSafeChatPublication builds the provider-bound payload for external chat delivery (Slack etc.). Before projecting, it validates every field against allowlists; this throw fires when the optional progressState field is present but is not one of the allowed states (queued, working, waiting_for_input, approval_needed, completed). It is a fail-closed guard: an UnsafeChatPublicationError is raised rather than letting a malformed progress value reach an external provider.
Solutions
- Check the value of input.progressState at the call site and compare it against the allowed set (queued, working, waiting_for_input, approval_needed, completed) in chat-publication-projection.ts:77
- Fix the internal-status-to-progress-state mapping so only whitelisted strings are forwarded, or omit progressState entirely when there is no valid mapping
- If a new state is legitimately needed, add it to the PROGRESS_STATES set and to the SafeChatPublicationPayload type, keeping db/shared/server/ui contracts in sync
- Add a unit test covering every progressState value the caller can produce
Example fix
// before
await publish({ source: "agent_message", classification: "external", progressState: task.status });
// after
const PROGRESS_MAP = { pending: "queued", running: "working", blocked: "waiting_for_input", done: "completed" } as const;
const progressState = PROGRESS_MAP[task.status as keyof typeof PROGRESS_MAP];
await publish({ source: "agent_message", classification: "external", ...(progressState ? { progressState } : {}) }); Defensive patterns
Strategy: validation
Validate before calling
const ALLOWED = new Set(["queued","working","waiting_for_input","approval_needed","completed"]);
function isValidProgressState(v) { return v === undefined || (typeof v === "string" && ALLOWED.has(v)); }
if (!isValidProgressState(input.progressState)) throw new Error(`invalid progressState: ${input.progressState}`); Type guard
const isProgressState = (v: unknown): v is "queued"|"working"|"waiting_for_input"|"approval_needed"|"completed" => typeof v === "string" && ["queued","working","waiting_for_input","approval_needed","completed"].includes(v);
Prevention
- Derive progressState from a typed union via an exhaustive status->state mapping, never forward raw strings
- Keep the PROGRESS_STATES allowlist and the shared type in lockstep; add a test per state value
- Omit progressState (undefined) when no mapping exists instead of guessing a value
When it happens
Trigger: Calling projectSafeChatPublication with input.progressState set to a string outside PROGRESS_STATES — e.g. a caller passes "in_progress", "done", "", a lowercase/uppercase variant like "Working", or a stale enum value after the PROGRESS_STATES set changed.
Common situations: A service layer maps an internal task status to a progress state with an incomplete switch/table that leaks raw internal status strings; a new progress state was added to a type but the PROGRESS_STATES allowlist was not updated; deserialized data from an older DB row or API payload carries a retired state value.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- CHAT_PROVIDER_PRETRANSPORT_REJECTED
- External chat publications support at most
- External chat text exceeds its projected processing limit
- Seed implementation returned without required validation…
- A full lowercase source SHA is required.
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/d5e2dcdae34d7328.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/chat-publication-projection.ts:391
}
/**
* Builds the complete provider-bound payload. This is intentionally the only
* API that accepts attachments or rich interaction metadata.
*/
export function projectSafeChatPublication(
input: ChatPublicationProjectionInput,
): ProjectedSafeChatPublicationPayload {
if (
input.classification !== "external" ||
!PUBLICATION_SOURCES.has(input.source)
) {
throw new UnsafeChatPublicationError(
"Chat publication source must be explicitly classified for external delivery",
);
}
if (input.progressState && !PROGRESS_STATES.has(input.progressState)) {
throw new UnsafeChatPublicationError(
"External chat progress state is invalid",
);
}
const attachmentIds = projectAttachmentIds(input.attachmentIds);
const interaction = input.interaction ? projectCard(input.interaction) : null;
return {
text: projectSafeChatPublicationText(input.text),
...(attachmentIds ? { attachmentIds } : {}),
...(input.progressState ? { progressState: input.progressState } : {}),
...(interaction ?? {}),
};
}
View on GitHub (pinned to 3f1d897a7c)