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
- Omit the cursor parameter entirely (or send null) to request the first page — only send back the exact cursor string the API returned.
- Validate the cursor is a non-empty string of at most 1024 characters before sending it.
- Regenerate the cursor by re-fetching page 1 and paginating forward with returned cursors.
- 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
- Always echo cursors back verbatim from API responses; never build them by hand.
- Treat a missing/empty cursor as 'first page' — omit the parameter rather than sending "".
- Bound persisted cursor storage to the 1024-char limit and validate before use.
- Don't round-trip cursors through transformations (URL decode, base64 normalize) that alter bytes.
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
- codex_history_invalid_cursor
- invalid
- paperclip_current_wake_comments_cursor_invalid
- Attachment exceeds the configured size limit
- Completion cites no registered attachment on this task. Use…
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)