Skip to content
Open
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
8 changes: 8 additions & 0 deletions src/codex/lineage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ export function codexConversationKeyFor(familyId: string, threadId: string): str
.digest("base64url")}`;
}

/** A per-thread identity for consumers whose state must not be shared by a cache cohort. */
export function codexThreadConversationKey(headers: Headers): string | undefined {
const sessionId = boundedLineageComponent(headers.get("session-id"));
const threadId = boundedLineageComponent(headers.get("thread-id"));
if (sessionId === undefined || threadId === undefined) return undefined;
return codexConversationKeyFor(sessionId, threadId);
}

/**
* The cohort anchor's key: the same string on both sides of the derivation.
*
Expand Down
3 changes: 2 additions & 1 deletion src/server/responses/request-prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
hasCallerCodexBearer,
requestOwnedMainPinState,
} from "../../codex/auth-context";
import { codexThreadConversationKey } from "../../codex/lineage";
import {
copyPreviousResponseReplayProvenance,
expandPreviousResponseInput,
Expand Down Expand Up @@ -245,7 +246,7 @@ export async function prepareResponsesRequest(
// parent all present the same parent id. `codexConversationIdentity` already reads this header
// for the same reason, and a surface that must tell siblings apart needs it too (#5033).
const inboundOwnThreadId = req.headers.get("thread-id")?.trim() || undefined;
const cursorClientThreadId = codexPoolAffinityKey(req.headers);
const cursorClientThreadId = codexThreadConversationKey(req.headers);
const originalBody = body;
if (options.comboReplaySnapshot) {
copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
Expand Down
5 changes: 3 additions & 2 deletions structure/providers/cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ a process-local store and reuses that snapshot on the next validated linear cont
rebuilding rootPromptMessagesJson and conversationTurns. Tool-result turns reuse the last completed
checkpoint plus only the uncovered suffix. A request without checkpointRef may use the prefix index
only when a remembered Cursor conversation or stable client thread owns the resolved conversation id.
The stable owner may be the Codex parent-thread header or the existing bounded process-local HMAC of
the complete Desktop session-id/thread-id pair. The request must also have a covered message prefix
The stable owner may be the Codex parent-thread header or the bounded process-local HMAC of the
complete Desktop session-id/thread-id pair, never its session-wide pool-affinity cohort. The request
must also have a covered message prefix
and system/developer digest that match exactly one snapshot for that same
conversation. Headerless requests without a stable owner full-replay. Isolated helper/shadow turns
never join the parent or sibling conversation. An explicit missing checkpointRef full-replays. Compaction, account or model mismatch, missing refs, decode failures, and
Expand Down
9 changes: 9 additions & 0 deletions tests/codex-integration/codex-lineage-placement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
codexLineageScopeKey,
codexLineageWorkflowLane,
codexThreadLineageLookup,
codexThreadConversationKey,
recordCodexThreadLineage,
} from "../../src/codex/lineage";
import { clearPoolRotationState } from "../../src/codex/pool-rotation";
Expand Down Expand Up @@ -570,6 +571,14 @@ describe("cohort pool affinity (#4780)", () => {
expect(bareChild.conversationKey).not.toBe(root.conversationKey);
});

test("thread conversation identity stays distinct inside a shared cache cohort", () => {
const first = rootHeaders();
const second = new Headers({ "session-id": "sess", "thread-id": "sibling" });

expect(codexPoolAffinityKey(first)).toBe(codexPoolAffinityKey(second));
expect(codexThreadConversationKey(first)).not.toBe(codexThreadConversationKey(second));
});

test("which requests bind at all is unchanged", () => {
// Deliberately untouched by #4780: only the VALUE of the key moves, never the set of
// requests that produce one. A bare thread-id still has no family anchor.
Expand Down
Loading