paperclipai/paperclip · error · UnsafeChatPublicationError

External chat text exceeds its projected processing limit

Error message

External chat text exceeds its projected processing limit

What it means

projectSafeChatPublicationText sanitizes text before it leaves Paperclip for an external chat provider (Slack, Discord, etc.). After stripping hidden reasoning/tool sections, redacting credentials, and neutralizing unsafe links and broadcast mentions, it enforces a projected output cap of MAX_TEXT_OUTPUT_LENGTH (4,000,000 UTF-16 units, chat-publication-projection.ts:12). If the sanitized output still exceeds that cap, it throws UnsafeChatPublicationError so oversized text is never published. The input cap is 1,000,000 units; expansion comes from redaction markers and mention neutralization, so this error fires when the input is large or grows substantially during sanitization.

Solutions

  1. Truncate the text before calling the projection function so post-sanitization output stays under 4,000,000 units
  2. Move the bulk content (logs, transcripts) into attachments and publish a short summary text instead
  3. Pre-strip the hidden/log sections yourself (the same patterns the projector strips) so redaction expansion does not push output over the cap
  4. Check upstream callers for accidental concatenation of large payloads into a single publication text

Example fix

// before
await publish({ classification: "external", source: "agent_comment", text: hugeLog });
// after
const MAX_SAFE_TEXT = 900_000;
const text = hugeLog.length > MAX_SAFE_TEXT
  ? hugeLog.slice(0, MAX_SAFE_TEXT) + "\n... (truncated; see attachments)"
  : hugeLog;
await publish({ classification: "external", source: "agent_comment", text, attachmentIds: [logAttachmentId] });
Defensive patterns

Strategy: validation

Validate before calling

const MAX_PROJECTED_OUTPUT = 4_000_000;
const MAX_SAFE_INPUT = 900_000; // headroom for redaction expansion
if (typeof text !== "string") throw new TypeError("text must be a string");
if (text.length > MAX_SAFE_INPUT) {
  text = text.slice(0, MAX_SAFE_INPUT) + "\n... (truncated)";
}

Type guard

function isWithinPublicationLimits(text: unknown): text is string {
  return typeof text === "string" && text.length <= 900_000;
}

Try / catch

try {
  const payload = projectSafeChatPublication({ classification: "external", source, text });
} catch (err) {
  if (err instanceof UnsafeChatPublicationError && /projected processing limit/.test(err.message)) {
    payload = projectSafeChatPublication({ classification: "external", source, text: truncateForPublication(text) });
  } else throw err;
}

Prevention

When it happens

Trigger: Calling projectSafeChatPublicationText (directly or via projectSafeChatPublication) with a text string whose sanitized output exceeds 4,000,000 UTF-16 units. Realistic cases: input near the 1,000,000-unit input cap where credential redaction ('[REDACTED]' replacements), broadcast neutralization ('@' plus zero-width space), and hidden-section removal boundaries expand the text ~4x; or an input already above 1,000,000 units would hit the earlier input-limit error instead, so this specific error means the text passed the input check but grew past the output cap during projection.

Common situations: An agent comment dumps a huge log file or build output as its message; a milestone title/body concatenation balloons after redaction markers are inserted; a backfill script pushing large document text through explicit_board_send; a provider adapter passing an entire conversation transcript as publication text.

Understand the failure class

Background: payload too large / request exceeds maximum size: why libraries cap bytes and how to fix oversize payloads — this error's family across 50 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/c74e583773b5a3e3. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/chat-publication-projection.ts:266

  }
  let output = input.replace(/<\|[^|\r\n]{1,80}\|>/g, "");
  for (const pattern of HIDDEN_BLOCKS) output = output.replace(pattern, "");
  output = stripHiddenSections(output);
  // Strip token-bearing query strings before the general credential scanner.
  // That scanner deliberately consumes uncertain unquoted values aggressively;
  // running it first could eat the visible prose following a Markdown URL.
  output = sanitizeUrls(output);
  output = sanitizeCredentialText(output);
  output = output
    .replace(SLACK_BROADCAST_RE, (_match, name: string) => `@\u200b${name}`)
    .replace(PROVIDER_BROADCAST_RE, (_match, name: string) => `@\u200b${name}`)
    .replace(/[ \t]+\n/g, "\n")
    .replace(/\n{3,}/g, "\n\n")
    .trim();

  if (!output) return "Update available in Paperclip.";
  if (output.length > MAX_TEXT_OUTPUT_LENGTH) {
    throw new UnsafeChatPublicationError(
      "External chat text exceeds its projected processing limit",
    );
  }
  return output;
}

function projectAttachmentIds(
  input: readonly string[] | null | undefined,
): string[] | undefined {
  if (!input?.length) return undefined;
  if (input.length > MAX_ATTACHMENTS) {
    throw new UnsafeChatPublicationError(
      `External chat publications support at most ${MAX_ATTACHMENTS} attachments`,
    );
  }

  const output: string[] = [];
  const seen = new Set<string>();

View on GitHub (pinned to 3f1d897a7c)