{"record":{"id":"bb771698d2231c25","repo":"paperclipai/paperclip","slug":"external-chat-publications-support-at-most-max-attachments","errorCode":null,"errorMessage":"External chat publications support at most ${MAX_ATTACHMENTS} attachments","messagePattern":"External chat publications support at most (.+?) attachments","errorType":"validation","errorClass":"UnsafeChatPublicationError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-publication-projection.ts","lineNumber":278,"sourceCode":"    .replace(/[ \\t]+\\n/g, \"\\n\")\n    .replace(/\\n{3,}/g, \"\\n\\n\")\n    .trim();\n\n  if (!output) return \"Update available in Paperclip.\";\n  if (output.length > MAX_TEXT_OUTPUT_LENGTH) {\n    throw new UnsafeChatPublicationError(\n      \"External chat text exceeds its projected processing limit\",\n    );\n  }\n  return output;\n}\n\nfunction projectAttachmentIds(\n  input: readonly string[] | null | undefined,\n): string[] | undefined {\n  if (!input?.length) return undefined;\n  if (input.length > MAX_ATTACHMENTS) {\n    throw new UnsafeChatPublicationError(\n      `External chat publications support at most ${MAX_ATTACHMENTS} attachments`,\n    );\n  }\n\n  const output: string[] = [];\n  const seen = new Set<string>();\n  for (const id of input) {\n    const normalized = id.trim().toLowerCase();\n    if (!UUID_RE.test(normalized)) {\n      throw new UnsafeChatPublicationError(\n        \"External chat attachment ids must be UUIDs\",\n      );\n    }\n    if (!seen.has(normalized)) {\n      seen.add(normalized);\n      output.push(normalized);\n    }\n  }","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/chat-publication-projection.ts#L260-L296","documentation":"projectAttachmentIds validates the attachmentIds array of a chat publication before it is sent to an external provider. It enforces MAX_ATTACHMENTS (20, chat-publication-projection.ts:13). If more than 20 attachment ids are supplied it throws UnsafeChatPublicationError, because external chat publications are capped at 20 attachments. The projection is the single boundary that accepts attachments, so this check guards the provider-bound payload.","triggerScenarios":"Calling projectSafeChatPublication with attachmentIds containing 21 or more string ids. Examples: an agent comment attaching every file touched in a large change set; a loop that accumulates attachment ids without a cap; an API client passing an unfiltered list of uploaded asset ids.","commonSituations":"An automation attaches all artifacts from a long-running task; a migration script forwards a legacy issue's full attachment history; a UI bulk-select that has no client-side limit sends 30 screenshots with one publication.","solutions":["Trim attachmentIds to the 20 most relevant before calling projectSafeChatPublication","Batch the publication: send attachments in chunks of at most 20 across multiple publications","Attach a single bundle (zip) instead of many individual files","Fix upstream accumulation code that appends ids without a limit"],"exampleFix":"// before\nawait publish({ classification: \"external\", source: \"explicit_board_send\", text, attachmentIds: allIds });\n// after\nconst MAX_ATTACHMENTS = 20;\nawait publish({\n  classification: \"external\",\n  source: \"explicit_board_send\",\n  text,\n  attachmentIds: allIds.slice(0, MAX_ATTACHMENTS),\n});","handlingStrategy":"validation","validationCode":"const MAX_ATTACHMENTS = 20;\nif (attachmentIds && attachmentIds.length > MAX_ATTACHMENTS) {\n  attachmentIds = attachmentIds.slice(0, MAX_ATTACHMENTS);\n}","typeGuard":"function isWithinAttachmentLimit(ids: readonly string[] | null | undefined): ids is readonly string[] {\n  return !!ids && ids.length <= 20;\n}","tryCatchPattern":"try {\n  const payload = projectSafeChatPublication({ classification: \"external\", source, text, attachmentIds });\n} catch (err) {\n  if (err instanceof UnsafeChatPublicationError && /at most 20 attachments/.test(err.message)) {\n    const payload = projectSafeChatPublication({ classification: \"external\", source, text, attachmentIds: attachmentIds!.slice(0, 20) });\n  } else throw err;\n}","preventionTips":["Enforce the 20-attachment cap in UI bulk-select and upload flows","Chunk publications when an operation legitimately produces many attachments","Bundle many files into a single zip attachment","Log when attachments are dropped so users know content was truncated"],"tags":["validation","limit-exceeded","attachments","chat"],"backgroundTag":"value-out-of-range","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}