{"record":{"id":"498bc8791cdd33d4","repo":"paperclipai/paperclip","slug":"invalid","errorCode":null,"errorMessage":"invalid","messagePattern":"invalid","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-reuse.ts","lineNumber":257,"sourceCode":"  if (typeof value !== \"string\" || value.length === 0 || value.length > 1024) {\n    throw new Error(\"paperclip_runner_chat_attachment_cursor_invalid\");\n  }\n  try {\n    const parsed = record(\n      JSON.parse(Buffer.from(value, \"base64url\").toString(\"utf8\")),\n    );\n    const createdAt =\n      typeof parsed.createdAt === \"string\" ? parsed.createdAt : \"\";\n    const parsedDate = new Date(createdAt);\n    if (\n      parsed.schema !== \"paperclip.chat-attachment-list-cursor.v1\" ||\n      parsed.conversationId !== conversationId ||\n      (parsed.sourceCommentId ?? null) !== sourceCommentId ||\n      Number.isNaN(parsedDate.getTime()) ||\n      !isUuid(parsed.attachmentId) ||\n      !isUuid(parsed.sourceCommentIdTieBreak)\n    ) {\n      throw new Error(\"invalid\");\n    }\n    return parsed as ListCursor;\n  } catch {\n    throw new Error(\"paperclip_runner_chat_attachment_cursor_invalid\");\n  }\n}\n\nfunction destinationAllowed(\n  endpoint: typeof chatEndpoints.$inferSelect,\n  conversation: typeof chatConversations.$inferSelect,\n  resource: typeof chatEndpointResources.$inferSelect | null,\n): boolean {\n  if (conversation.isDirectMessage) return endpoint.allowDirectMessages;\n  if (!resource || resource.availability !== \"available\") return false;\n  if (\n    endpoint.provider === \"microsoft-teams\" &&\n    resource.type === \"group_chat\"\n  ) {","sourceCodeStart":239,"sourceCodeEnd":275,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-reuse.ts#L239-L275","documentation":"An internal sentinel thrown inside decodeListCursor's try block when the decoded cursor payload fails semantic validation: wrong schema version, mismatched conversationId, mismatched sourceCommentId, unparseable createdAt date, or non-UUID attachmentId / sourceCommentIdTieBreak. It is immediately caught by the enclosing catch and rethrown as the public paperclip_runner_chat_attachment_cursor_invalid error, so developers never see this raw 'invalid' message — only via the rethrown code.","triggerScenarios":"A base64url cursor that decodes to valid JSON but whose fields don't match: schema !== 'paperclip.chat-attachment-list-cursor.v1', conversationId differs from the requested conversation, sourceCommentId differs, createdAt is missing/not an ISO date, or attachmentId/sourceCommentIdTieBreak are not UUID v1-5 strings.","commonSituations":"Using a cursor from one conversation to list another conversation's attachments; cursors generated by a different/older schema version after an upgrade; hand-forged or mutated cursor payloads; clock/date corruption producing an invalid createdAt.","solutions":["Restart pagination from page 1 (omit the cursor) — a cursor is only valid for its exact conversation and sourceCommentId.","Verify the cursor matches the same conversationId and sourceCommentId used in the original listing request.","Ensure client and server versions agree on the cursor schema (paperclip.chat-attachment-list-cursor.v1); upgrade stale clients.","Send cursors back verbatim — never decode, edit, or re-encode them client-side."],"exampleFix":"// before\nconst cursor = savedCursors[newConversationId]; // reused across conversations\nconst page = await listAttachments({ conversationId, cursor });\n\n// after\nconst cursor = conversationId === savedCursorConversation ? savedCursor : undefined; // cursor is scoped to its conversation\nconst page = await listAttachments({ conversationId, cursor });","handlingStrategy":"validation","validationCode":"function cursorMatchesContext(cursor: string, conversationId: string, sourceCommentId: string | null): boolean {\n  try {\n    const parsed = JSON.parse(Buffer.from(cursor, \"base64url\").toString(\"utf8\"));\n    return parsed.schema === \"paperclip.chat-attachment-list-cursor.v1\" && parsed.conversationId === conversationId;\n  } catch { return false; }\n}","typeGuard":"function isCursorFor(value: unknown, conversationId: string): value is string {\n  try {\n    const parsed: unknown = JSON.parse(Buffer.from(String(value), \"base64url\").toString(\"utf8\"));\n    return typeof parsed === \"object\" && parsed !== null && (parsed as Record<string, unknown>).schema === \"paperclip.chat-attachment-list-cursor.v1\" && (parsed as Record<string, unknown>).conversationId === conversationId;\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  page = await listAttachments({ conversationId, sourceCommentId, cursor });\n} catch (err) {\n  if (err instanceof Error && err.message === \"paperclip_runner_chat_attachment_cursor_invalid\") {\n    page = await listAttachments({ conversationId, sourceCommentId }); // cursor/context mismatch — restart\n  } else {\n    throw err;\n  }\n}","preventionTips":["Scope stored cursors by (conversationId, sourceCommentId) and discard them when either changes.","Never reuse a cursor across conversations or after the source comment changes.","Re-paginate from page 1 after client/server upgrades that may change cursor schema.","Treat cursors as opaque; inspecting or editing fields guarantees this error eventually."],"tags":["pagination","cursor","validation","schema-version"],"backgroundTag":"invalid-argument-value","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}