Skip to content

Commit 2f30f8c

Browse files
committed
fix(session): only divert foreign stores, not same-session resumes (CL-10372)
Review found the writePrompt divert over-fired: on a normal same-session resume the reactor writes the materialized prompt in pre-inference before any writeTurns, and the prompt legitimately differs from on-disk turns (context transforms, ephemeral turns, summaries). The prompt-only signal marked the owner store as foreign, froze turns.jsonl, and hid new turns from load(). Rework: ownership is decided solely by writeTurns. Only a fresh store whose first writeTurns diverges from the on-disk history (a genuinely foreign concurrent session) diverts to versioned bases; writePromptSegmented is rebound with it. A differing prompt alone never diverts, so resumes keep writing turns.jsonl/prompt.jsonl as before. Adds a regression test: same-session resume with a differing first prompt must keep updating turns.jsonl and be visible to load(), with no versioned file. The concurrent-clobber repro still passes.
1 parent 380101d commit 2f30f8c

2 files changed

Lines changed: 40 additions & 18 deletions

File tree

‎src/session/optimized-context-store.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,39 @@ describe("concurrent sessions write disjoint transcripts (CL-10372)", () => {
637637
];
638638
expect(new Set(all).size).toBe(turnsA.length + turnsB.length);
639639
});
640+
641+
test("same-session resume with a differing first prompt keeps writing turns.jsonl (CL-10372 regression)", async () => {
642+
const dir = tempDir();
643+
644+
// Owner session lands a committed transcript, then is resumed by a fresh
645+
// store in a new process (the reactor writes the materialized prompt in
646+
// pre-inference BEFORE any writeTurns; the prompt legitimately differs
647+
// from the on-disk turns when context transforms / ephemeral turns apply).
648+
const owner = await createOptimizedContextStore(dir);
649+
const live = [turn("a"), turn("b")];
650+
await owner.writeTurns([...live]);
651+
await owner.writeMetadata(EMPTY_CHECKPOINT_METADATA);
652+
await owner.commit({ message: "owner" });
653+
654+
const resumed = await createOptimizedContextStore(dir);
655+
await resumed.writePrompt([turn("a"), turn("b"), turn("ephemeral")]);
656+
const next = [...live, turn("c")];
657+
await resumed.writeTurns([...next]);
658+
await resumed.writeMetadata(EMPTY_CHECKPOINT_METADATA);
659+
await resumed.commit({ message: "resume append" });
660+
661+
// No versioned base: this store is the session owner resuming, so it
662+
// keeps owning turns.jsonl and the new turn is visible to load().
663+
expect(
664+
fs
665+
.readdirSync(dir)
666+
.filter((name) => /^turns-\d+-[0-9a-f]+\.jsonl$/.test(name)),
667+
).toEqual([]);
668+
expect(turnTexts(readTurnsFromFile(path.join(dir, TURNS_FILE)))).toEqual(
669+
turnTexts(next),
670+
);
671+
expect(turnTexts((await resumed.load()).turns)).toEqual(turnTexts(next));
672+
});
640673
});
641674

642675
function readTurnsFromFile(fullPath: string): ConversationTurn[] {

‎src/session/optimized-context-store.ts‎

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -606,13 +606,6 @@ export async function createSessionStores(
606606
liveTurnRefs = [...turns];
607607
return;
608608
}
609-
if (versionedBase !== null) {
610-
// Already diverted (e.g. a colliding prompt wrote first): this store's
611-
// transcript owns a disjoint versioned base; write turns there too.
612-
await writeSegmented(writeTurnsSegmented, turns);
613-
liveTurnRefs = [...turns];
614-
return;
615-
}
616609
const extraTexts = await readExtraSegmentTexts(dir, TURNS_FILE);
617610
let baseTurns: ConversationTurn[];
618611
try {
@@ -692,17 +685,13 @@ export async function createSessionStores(
692685
}
693686
}
694687
if (live === null || !promptEqualsLiveTurns(live, turns)) {
695-
// A fresh instance whose on-disk live turns came from another session
696-
// (CL-10372) must not clobber that session's prompt snapshot either;
697-
// divert this store's prompt to its own disjoint versioned base.
698-
if (
699-
versionedBase === null &&
700-
liveTurnRefs === null &&
701-
live !== null &&
702-
live.length > 0
703-
) {
704-
divertToVersionedBases();
705-
}
688+
// The prompt is a materialized snapshot and legitimately differs from
689+
// the live turns on a normal resume (context transforms, ephemeral
690+
// turns, summaries), so a differing prompt alone never proves a foreign
691+
// store. Ownership is decided by writeTurns: only its fresh-store
692+
// divergence diverts to versioned bases, and writePromptSegmented is
693+
// rebound with it. Prompt writes therefore stay on the shared base
694+
// unless that divert already happened.
706695
await writeSegmented(writePromptSegmented, turns);
707696
return;
708697
}

0 commit comments

Comments
 (0)