diff --git a/scripts/e2e/lib/upgrade-survivor/assertions.mjs b/scripts/e2e/lib/upgrade-survivor/assertions.mjs index b07a36d77afa..23fa623c5902 100644 --- a/scripts/e2e/lib/upgrade-survivor/assertions.mjs +++ b/scripts/e2e/lib/upgrade-survivor/assertions.mjs @@ -479,15 +479,9 @@ function assertSessionMetadataMigrated(stateDir) { const main = store["agent:main:main"]; const direct = store["agent:main:+15551234567"]; const group = store["agent:main:slack:channel:cupgrade"]; - assert(main?.sessionId === LEGACY_SESSION_MAIN_ID, "main legacy session row missing from SQLite"); - assert( - direct?.sessionId === LEGACY_SESSION_DIRECT_ID, - "direct legacy session row missing from SQLite", - ); - assert( - group?.sessionId === LEGACY_SESSION_GROUP_ID, - "channel legacy session row missing from SQLite", - ); + assert(main?.sessionId === LEGACY_SESSION_MAIN_ID, "main legacy session row missing"); + assert(direct?.sessionId === LEGACY_SESSION_DIRECT_ID, "direct legacy session row missing"); + assert(group?.sessionId === LEGACY_SESSION_GROUP_ID, "channel legacy session row missing"); assert( main?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_MAIN_ID}.jsonl`), "main legacy session row still points at the old sessions directory", @@ -506,7 +500,7 @@ function assertSessionMetadataMigrated(stateDir) { ); assert( main.skillsSnapshot?.resolvedSkills === undefined, - "heavy resolvedSkills cache was persisted into SQLite session metadata", + "heavy resolvedSkills cache was persisted into migrated session metadata", ); } diff --git a/src/commands/doctor-state-migrations.test.ts b/src/commands/doctor-state-migrations.test.ts index e91c56462323..2f8f80bdf7de 100644 --- a/src/commands/doctor-state-migrations.test.ts +++ b/src/commands/doctor-state-migrations.test.ts @@ -629,7 +629,7 @@ describe("doctor legacy state migrations", () => { result: Awaited>; targetDir: string; legacySessionsDir: string; - store: Record; + store: Record; }; beforeAll(async () => { @@ -638,8 +638,12 @@ describe("doctor legacy state migrations", () => { const legacySessionsDir = writeLegacySessionsFixture({ root, sessions: { - "+1555": { sessionId: "a", updatedAt: 10 }, - "+1666": { sessionId: "b", updatedAt: 20 }, + "+1555": { + sessionId: "a", + sessionFile: path.join(root, "sessions", "a.jsonl"), + updatedAt: 10, + }, + "+1666": { sessionId: "b", sessionFile: "b.jsonl", updatedAt: 20 }, "slack:channel:C123": { sessionId: "c", updatedAt: 30 }, "group:abc": { sessionId: "d", updatedAt: 40 }, "subagent:xyz": { sessionId: "e", updatedAt: 50 }, @@ -661,7 +665,7 @@ describe("doctor legacy state migrations", () => { const targetDir = path.join(root, "agents", "main", "sessions"); const store = JSON.parse( fs.readFileSync(path.join(targetDir, "sessions.json"), "utf-8"), - ) as Record; + ) as Record; migratedLegacySessionsCase = { result, targetDir, legacySessionsDir, store }; }); @@ -676,6 +680,8 @@ describe("doctor legacy state migrations", () => { expect(store["agent:main:main"]?.sessionId).toBe("b"); expect(store["agent:main:+1555"]?.sessionId).toBe("a"); expect(store["agent:main:+1666"]?.sessionId).toBe("b"); + expect(store["agent:main:+1555"]?.sessionFile).toBe(path.join(targetDir, "a.jsonl")); + expect(store["agent:main:+1666"]?.sessionFile).toBe(path.join(targetDir, "b.jsonl")); expect(store["+1555"]).toBeUndefined(); expect(store["+1666"]).toBeUndefined(); expect(store["agent:main:slack:channel:c123"]?.sessionId).toBe("c"); diff --git a/src/infra/state-migrations.ts b/src/infra/state-migrations.ts index 16f20b0cf7ce..34888e69225f 100644 --- a/src/infra/state-migrations.ts +++ b/src/infra/state-migrations.ts @@ -2878,6 +2878,7 @@ async function migrateLegacySessions( return { changes, warnings }; } + const movedSessionFiles = new Map(); const entries = safeReadDir(detected.sessions.legacyDir); for (const entry of entries) { if (!entry.isFile()) { @@ -2887,18 +2888,54 @@ async function migrateLegacySessions( continue; } const from = path.join(detected.sessions.legacyDir, entry.name); - const to = path.join(detected.sessions.targetDir, entry.name); + let to = path.join(detected.sessions.targetDir, entry.name); if (fileExists(to)) { - continue; + const parsed = path.parse(entry.name); + to = path.join(detected.sessions.targetDir, `${parsed.name}.legacy-${now()}${parsed.ext}`); } try { fs.renameSync(from, to); + movedSessionFiles.set(path.resolve(from), to); changes.push(`Moved ${entry.name} → agents/${detected.targetAgentId}/sessions`); } catch (err) { warnings.push(`Failed moving ${from}: ${String(err)}`); } } + if (movedSessionFiles.size > 0) { + let rewroteSessionFiles = false; + for (const entry of Object.values(merged)) { + const rawSessionFile = entry.sessionFile; + const legacySessionFile = + typeof rawSessionFile === "string" + ? path.resolve(detected.sessions.legacyDir, rawSessionFile) + : typeof entry.sessionId === "string" + ? path.join(detected.sessions.legacyDir, `${entry.sessionId}.jsonl`) + : undefined; + const movedSessionFile = legacySessionFile + ? movedSessionFiles.get(path.resolve(legacySessionFile)) + : undefined; + if (!movedSessionFile) { + continue; + } + entry.sessionFile = movedSessionFile; + rewroteSessionFiles = true; + } + if (rewroteSessionFiles) { + const normalized: Record = {}; + for (const [key, entry] of Object.entries(merged)) { + const normalizedEntry = normalizeSessionEntry(entry); + if (normalizedEntry) { + normalized[key] = normalizedEntry; + } + } + await saveSessionStore(detected.sessions.targetStorePath, normalized, { + skipMaintenance: true, + }); + changes.push("Rewrote migrated session transcript paths"); + } + } + if (legacyParsed.ok && targetReadable) { try { if (fileExists(detected.sessions.legacyStorePath)) {