Skip to content

Commit d7cebdc

Browse files
fix(gateway): rotate already-stale generated transcript filename on /reset (#93496)
Merged via squash. Prepared head SHA: 6ae356c Co-authored-by: harjothkhara <[email protected]> Co-authored-by: vincentkoc <[email protected]> Reviewed-by: @vincentkoc
1 parent 53da30d commit d7cebdc

4 files changed

Lines changed: 62 additions & 6 deletions

File tree

src/gateway/drain-active-sessions-for-shutdown.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ vi.mock("../plugins/hook-runner-global.js", () => ({
2929
}));
3030

3131
vi.mock("./session-transcript-files.fs.js", () => ({
32+
extractGeneratedTranscriptSessionId: vi.fn(() => undefined),
3233
resolveStableSessionEndTranscript: vi.fn(() => ({
3334
sessionFile: undefined,
3435
transcriptArchived: false,

src/gateway/server.sessions.reset-models.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,56 @@ test("sessions.reset rotates generated topic transcript files with the new sessi
372372
expect(path.basename(persistedEntry?.sessionFile ?? "")).toBe(`${nextSessionId}-topic-456.jsonl`);
373373
});
374374

375+
test("sessions.reset rotates an already-stale generated transcript file to the new session id", async () => {
376+
const { dir, storePath } = await createSessionStoreDir();
377+
// Post-upgrade state: the stored sessionFile still embeds an OLDER generated id
378+
// that no longer matches the entry's logical sessionId, so rotation must key off
379+
// the file's embedded id rather than the current sessionId (issue #77770).
380+
const staleFileSessionId = "11111111-1111-4111-8111-111111111111";
381+
const currentSessionId = "22222222-2222-4222-8222-222222222222";
382+
const staleSessionFile = path.join(dir, `${staleFileSessionId}.jsonl`);
383+
await fs.writeFile(staleSessionFile, `${JSON.stringify({ role: "user", content: "old" })}\n`);
384+
385+
await writeSessionStore({
386+
entries: {
387+
main: sessionStoreEntry(currentSessionId, {
388+
sessionFile: staleSessionFile,
389+
}),
390+
},
391+
});
392+
393+
const reset = await directSessionReq<{
394+
ok: true;
395+
key: string;
396+
entry: {
397+
sessionId: string;
398+
sessionFile?: string;
399+
};
400+
}>("sessions.reset", { key: "main" });
401+
402+
expect(reset.ok).toBe(true);
403+
const nextSessionId = reset.payload?.entry.sessionId;
404+
const nextSessionFile = reset.payload?.entry.sessionFile;
405+
if (!nextSessionId || !nextSessionFile) {
406+
throw new Error("expected reset session id and file");
407+
}
408+
expect(nextSessionId).not.toBe(currentSessionId);
409+
// The new session must adopt the new session id, not keep the stale generated name.
410+
expect(path.basename(nextSessionFile)).toBe(`${nextSessionId}.jsonl`);
411+
expect(path.basename(nextSessionFile)).not.toBe(`${staleFileSessionId}.jsonl`);
412+
413+
const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
414+
string,
415+
{
416+
sessionId?: string;
417+
sessionFile?: string;
418+
}
419+
>;
420+
const persistedEntry = store["agent:main:main"];
421+
expect(persistedEntry?.sessionId).toBe(nextSessionId);
422+
expect(path.basename(persistedEntry?.sessionFile ?? "")).toBe(`${nextSessionId}.jsonl`);
423+
});
424+
375425
test("sessions.reset preserves legacy explicit model overrides without modelOverrideSource", async () => {
376426
await expectMainResetModelFields({
377427
defaultPrimary: "openai/gpt-test-a",

src/gateway/session-reset-service.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
import { findDirectChildSessionsForParent } from "./session-child-sessions.js";
6262
import {
6363
archiveSessionTranscriptsDetailed,
64+
extractGeneratedTranscriptSessionId,
6465
resolveStableSessionEndTranscript,
6566
type ArchivedSessionTranscript,
6667
} from "./session-transcript-files.fs.js";
@@ -82,12 +83,16 @@ function resolveResetSessionFile(params: {
8283
agentId: string;
8384
}): string {
8485
const currentEntry = params.currentEntry;
85-
// Preserve explicit session-file placement across reset while swapping the
86-
// embedded session id, so linked runtimes keep writing beside old transcripts.
87-
const rewrittenSessionFile = currentEntry?.sessionId
86+
// Rotate generated transcript names by the file's *embedded* id, not the logical
87+
// session id: a post-upgrade sessionFile can embed a stale id, so keying off
88+
// currentEntry.sessionId would orphan the reset session on the old file. Explicit
89+
// custom placements have no embedded id and stay preserved.
90+
const rotationPreviousSessionId =
91+
extractGeneratedTranscriptSessionId(currentEntry?.sessionFile) ?? currentEntry?.sessionId;
92+
const rewrittenSessionFile = rotationPreviousSessionId
8893
? rewriteSessionFileForNewSessionId({
89-
sessionFile: currentEntry.sessionFile,
90-
previousSessionId: currentEntry.sessionId,
94+
sessionFile: currentEntry?.sessionFile,
95+
previousSessionId: rotationPreviousSessionId,
9196
nextSessionId: params.nextSessionId,
9297
})
9398
: undefined;

src/gateway/session-transcript-files.fs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ function classifySessionTranscriptCandidate(
106106
return transcriptSessionId === sessionId ? "current" : "stale";
107107
}
108108

109-
function extractGeneratedTranscriptSessionId(sessionFile?: string): string | undefined {
109+
export function extractGeneratedTranscriptSessionId(sessionFile?: string): string | undefined {
110110
const trimmed = sessionFile?.trim();
111111
if (!trimmed) {
112112
return undefined;

0 commit comments

Comments
 (0)