Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/adapters/cursor/call-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* literal newline ("call-<uuid>-<n>\nfc_<uuid>_<n>"). OpenCodex forwards ids
* verbatim, so that newline leaked into Responses-visible `call_id` values,
* where line-oriented clients (logging, splitting, validation) break. The codec
* encodes only ids containing CR/LF into a versioned single-line form and
* decodes both that form and legacy raw multi-line ids back to the exact
* upstream bytes before anything is serialized toward Cursor.
* encodes ids containing CR/LF into a versioned single-line form. It also
* escapes ids already in that form's reserved namespace so encoding remains
* injective. Both forms decode back to the exact upstream bytes before
* anything is serialized toward Cursor.
*/

const CALL_ID_PREFIX = "ocxc1_";
Expand All @@ -19,7 +20,7 @@ function needsEncoding(id: string): boolean {

/** Encode a Cursor wire call id into a single-line Responses-safe id. */
export function encodeCursorCallId(id: string): string {
if (!needsEncoding(id)) return id;
if (!needsEncoding(id) && !id.startsWith(CALL_ID_PREFIX)) return id;
return CALL_ID_PREFIX + Buffer.from(id, "utf8").toString("base64url");
}

Expand All @@ -37,6 +38,7 @@ export function decodeCursorCallId(id: string): string {
const decoded = Buffer.from(payload, "base64url").toString("utf8");
// Round-trip guard: only trust payloads our encoder could have produced.
if (Buffer.from(decoded, "utf8").toString("base64url") !== payload) return id;
if (!needsEncoding(decoded) && !decoded.startsWith(CALL_ID_PREFIX)) return id;
return decoded;
} catch {
return id;
Expand Down
47 changes: 37 additions & 10 deletions tests/cursor-call-id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@ describe("cursor call-id codec", () => {
expect(decodeCursorCallId("call_abc123")).toBe("call_abc123");
});

test("reserved-prefix ids are escaped and round-trip", () => {
for (const id of ["ocxc1_", "ocxc1_Y2FsbF8x", "ocxc1_!!not-base64url!!", "ocxc1_raw\nwire"]) {
const encoded = encodeCursorCallId(id);
expect(encoded).not.toBe(id);
expect(encoded.startsWith("ocxc1_")).toBe(true);
expect(encoded).not.toContain("\n");
expect(encoded).not.toContain("\r");
expect(decodeCursorCallId(encoded)).toBe(id);
}
});

test("reserved-prefix ids resembling legacy newline encodings stay opaque", () => {
const id = "ocxc1_YQpi";
const encoded = encodeCursorCallId(id);
expect(encoded).not.toBe(id);
expect(decodeCursorCallId(encoded)).toBe(id);
});

test("legacy encoded line breaks remain decodable", () => {
expect(decodeCursorCallId("ocxc1_YQpi")).toBe("a\nb");
expect(decodeCursorCallId("ocxc1_DQ")).toBe("\r");
expect(decodeCursorCallId("ocxc1_DQo")).toBe("\r\n");
});

test("newline composite id round-trips through a single-line form", () => {
const encoded = encodeCursorCallId(COMPOSITE);
expect(encoded).not.toContain("\n");
Expand All @@ -34,15 +58,18 @@ describe("cursor call-id codec", () => {
expect(decodeCursorCallId("ocxc1_!!not-base64url!!")).toBe("ocxc1_!!not-base64url!!");
});

test("tool_call_start ids are single-line at the adapter boundary", () => {
const events = mapCursorServerMessage(
{ type: "tool_call_start", id: COMPOSITE, name: "get_weather" },
mapperState(),
);
expect(events).toHaveLength(1);
const event = events[0]!;
if (event.type !== "tool_call_start") throw new Error("expected tool_call_start");
expect(event.id).not.toContain("\n");
expect(decodeCursorCallId(event.id)).toBe(COMPOSITE);
test("tool_call_start ids are reversible and single-line at the adapter boundary", () => {
for (const id of [COMPOSITE, "ocxc1_YQpi"]) {
const events = mapCursorServerMessage(
{ type: "tool_call_start", id, name: "get_weather" },
mapperState(),
);
expect(events).toHaveLength(1);
const event = events[0]!;
if (event.type !== "tool_call_start") throw new Error("expected tool_call_start");
expect(event.id).not.toContain("\n");
expect(event.id).not.toContain("\r");
expect(decodeCursorCallId(event.id)).toBe(id);
}
});
});
Loading