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

  1. 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
  2. Fix the internal-status-to-progress-state mapping so only whitelisted strings are forwarded, or omit progressState entirely when there is no valid mapping
  3. 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
  4. 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

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


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)