paperclipai/paperclip · error

invalid

Error message

invalid

What it means

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.

Solutions

  1. Restart pagination from page 1 (omit the cursor) — a cursor is only valid for its exact conversation and sourceCommentId.
  2. Verify the cursor matches the same conversationId and sourceCommentId used in the original listing request.
  3. Ensure client and server versions agree on the cursor schema (paperclip.chat-attachment-list-cursor.v1); upgrade stale clients.
  4. Send cursors back verbatim — never decode, edit, or re-encode them client-side.

Example fix

// before
const cursor = savedCursors[newConversationId]; // reused across conversations
const page = await listAttachments({ conversationId, cursor });

// after
const cursor = conversationId === savedCursorConversation ? savedCursor : undefined; // cursor is scoped to its conversation
const page = await listAttachments({ conversationId, cursor });
Defensive patterns

Strategy: validation

Validate before calling

function cursorMatchesContext(cursor: string, conversationId: string, sourceCommentId: string | null): boolean {
  try {
    const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
    return parsed.schema === "paperclip.chat-attachment-list-cursor.v1" && parsed.conversationId === conversationId;
  } catch { return false; }
}

Type guard

function isCursorFor(value: unknown, conversationId: string): value is string {
  try {
    const parsed: unknown = JSON.parse(Buffer.from(String(value), "base64url").toString("utf8"));
    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;
  } catch { return false; }
}

Try / catch

try {
  page = await listAttachments({ conversationId, sourceCommentId, cursor });
} catch (err) {
  if (err instanceof Error && err.message === "paperclip_runner_chat_attachment_cursor_invalid") {
    page = await listAttachments({ conversationId, sourceCommentId }); // cursor/context mismatch — restart
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at server/src/services/native-runtime/chat-attachment-reuse.ts:257

  if (typeof value !== "string" || value.length === 0 || value.length > 1024) {
    throw new Error("paperclip_runner_chat_attachment_cursor_invalid");
  }
  try {
    const parsed = record(
      JSON.parse(Buffer.from(value, "base64url").toString("utf8")),
    );
    const createdAt =
      typeof parsed.createdAt === "string" ? parsed.createdAt : "";
    const parsedDate = new Date(createdAt);
    if (
      parsed.schema !== "paperclip.chat-attachment-list-cursor.v1" ||
      parsed.conversationId !== conversationId ||
      (parsed.sourceCommentId ?? null) !== sourceCommentId ||
      Number.isNaN(parsedDate.getTime()) ||
      !isUuid(parsed.attachmentId) ||
      !isUuid(parsed.sourceCommentIdTieBreak)
    ) {
      throw new Error("invalid");
    }
    return parsed as ListCursor;
  } catch {
    throw new Error("paperclip_runner_chat_attachment_cursor_invalid");
  }
}

function destinationAllowed(
  endpoint: typeof chatEndpoints.$inferSelect,
  conversation: typeof chatConversations.$inferSelect,
  resource: typeof chatEndpointResources.$inferSelect | null,
): boolean {
  if (conversation.isDirectMessage) return endpoint.allowDirectMessages;
  if (!resource || resource.availability !== "available") return false;
  if (
    endpoint.provider === "microsoft-teams" &&
    resource.type === "group_chat"
  ) {

View on GitHub (pinned to 3f1d897a7c)