paperclipai/paperclip · error

paperclip_runner_chat_attachment_cursor_invalid

paperclip_runner_chat_attachment_cursor_invalid

Error message

paperclip_runner_chat_attachment_cursor_invalid

What it means

Thrown by decodeListCursor when the supplied pagination cursor is not a usable value at all: not a string, an empty string, or longer than 1024 characters. List cursors for chat-attachment reuse listings are base64url-encoded JSON blobs; anything failing this basic shape check is rejected before parsing. A null/undefined cursor is valid and means 'first page', only malformed values throw.

Solutions

  1. Omit the cursor parameter entirely (or send null) to request the first page — only send back the exact cursor string the API returned.
  2. Validate the cursor is a non-empty string of at most 1024 characters before sending it.
  3. Regenerate the cursor by re-fetching page 1 and paginating forward with returned cursors.
  4. If cursors were persisted, check that the stored value was not truncated or re-encoded (base64url is case-sensitive; URL-decoding/encoding damage is common).

Example fix

// before
const items = await listAttachmentReuse({ cursor: searchParams.cursor ?? "" });

// after
const cursor = typeof searchParams.cursor === "string" && searchParams.cursor.length > 0 && searchParams.cursor.length <= 1024
  ? searchParams.cursor
  : undefined;
const items = await listAttachmentReuse({ cursor });
Defensive patterns

Strategy: validation

Validate before calling

function cursorIsSendable(cursor: unknown): boolean {
  return typeof cursor === "string" && cursor.length > 0 && cursor.length <= 1024;
}

Type guard

function isValidCursorString(value: unknown): value is string {
  return typeof value === "string" && value.length > 0 && value.length <= 1024;
}

Try / catch

try {
  page = await listAttachmentReuse({ cursor });
} catch (err) {
  if (err instanceof Error && err.message === "paperclip_runner_chat_attachment_cursor_invalid") {
    page = await listAttachmentReuse({}); // restart from first page
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Passing a cursor param that is a number, object, or array instead of a string; passing an empty string ""; passing a cursor string exceeding 1024 chars (e.g. a client echoing back a bloated or corrupted cursor).

Common situations: Client sends cursor as a query param and it arrives as something other than a string after parsing; truncated cursor values from URL length limits; a client constructing cursors by hand instead of using the previously returned next-cursor; a client sending "" meaning 'first page' instead of omitting the parameter.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

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

    typeof value === "string" &&
    /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
      value,
    )
  );
}

function encodeListCursor(cursor: ListCursor): string {
  return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
}

function decodeListCursor(
  value: unknown,
  conversationId: string,
  sourceCommentId: string | null,
): ListCursor | null {
  if (value === null || value === undefined) return null;
  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");
    }

View on GitHub (pinned to 3f1d897a7c)