diff --git a/src/config/sessions/session-entry-lifecycle.test.ts b/src/config/sessions/session-entry-lifecycle.test.ts index c482bb13788e..b1122d27c57e 100644 --- a/src/config/sessions/session-entry-lifecycle.test.ts +++ b/src/config/sessions/session-entry-lifecycle.test.ts @@ -52,6 +52,88 @@ describe("session entry lifecycle seam", () => { ).toBeUndefined(); }); + it("preserves an existing raw row key while exposing normalized context", async () => { + fs.writeFileSync( + storePath, + JSON.stringify({ + GLOBAL: { + sessionId: "session-raw", + updatedAt: 10, + }, + }), + ); + let contextKey: string | undefined; + + await patchSessionLifecycleEntry( + { sessionKey: "GLOBAL", storePath }, + (entry, context) => { + contextKey = context.sessionKey; + entry.fastMode = true; + return entry; + }, + { replaceEntry: true, skipMaintenance: true }, + ); + + const store = loadSessionStore(storePath, { skipCache: true }); + expect(contextKey).toBe("global"); + expect(store.GLOBAL?.fastMode).toBe(true); + expect(store.global).toBeUndefined(); + }); + + it("collapses legacy aliases when a usable canonical row exists", async () => { + fs.writeFileSync( + storePath, + JSON.stringify({ + "agent:main:main": { + sessionId: "session-canonical", + updatedAt: 10, + fastMode: false, + }, + "Agent:Main:Main": { + sessionId: "session-legacy", + updatedAt: 20, + fastMode: false, + }, + }), + ); + + await patchSessionLifecycleEntry( + { sessionKey: "Agent:Main:Main", storePath }, + (entry) => { + entry.fastMode = true; + return entry; + }, + { replaceEntry: true, skipMaintenance: true }, + ); + + const store = loadSessionStore(storePath, { skipCache: true }); + expect(store["agent:main:main"]?.sessionId).toBe("session-legacy"); + expect(store["agent:main:main"]?.fastMode).toBe(true); + expect(store["Agent:Main:Main"]).toBeUndefined(); + }); + + it("preserves a fallback raw row key", async () => { + await patchSessionLifecycleEntry( + { sessionKey: "UNKNOWN", storePath }, + (entry) => { + entry.fastMode = true; + return entry; + }, + { + fallbackEntry: { + sessionId: "session-fallback", + updatedAt: 10, + }, + replaceEntry: true, + skipMaintenance: true, + }, + ); + + const store = loadSessionStore(storePath, { skipCache: true }); + expect(store.UNKNOWN?.fastMode).toBe(true); + expect(store.unknown).toBeUndefined(); + }); + it("patches multiple entries without exposing a mutable store", async () => { await upsertSessionEntry( { sessionKey: "agent:main:one", storePath }, diff --git a/src/config/sessions/session-entry-lifecycle.ts b/src/config/sessions/session-entry-lifecycle.ts index 625e311ee5e3..4d6a258a0f2a 100644 --- a/src/config/sessions/session-entry-lifecycle.ts +++ b/src/config/sessions/session-entry-lifecycle.ts @@ -1,5 +1,6 @@ import { getRuntimeConfig } from "../io.js"; import { resolveStorePath } from "./paths.js"; +import { hasMismatchedCaseSensitiveDeliveryProof } from "./store-entry.js"; import { archiveRemovedSessionTranscripts, loadSessionStore, @@ -63,6 +64,11 @@ export async function patchSessionLifecycleEntry( if (!existing) { return { changed: false, entry: null }; } + const storageKey = resolveLifecyclePatchStorageKey({ + store, + resolved, + requestedKey: scope.sessionKey, + }); const patch = await update(structuredClone(existing), { existingEntry: resolved.existing ? structuredClone(resolved.existing) : undefined, sessionKey: resolved.normalizedKey, @@ -78,8 +84,11 @@ export async function patchSessionLifecycleEntry( : options.preserveActivity ? mergeSessionEntryPreserveActivity(existing, patch) : mergeSessionEntry(existing, patch); - store[resolved.normalizedKey] = next; + store[storageKey] = next; for (const legacyKey of resolved.legacyKeys) { + if (legacyKey === storageKey) { + continue; + } delete store[legacyKey]; } return { changed: true, entry: next }; @@ -219,6 +228,35 @@ function collectReferencedSessionIds(store: Record): Set; + requestedKey: string; + resolved: ReturnType; +}): string { + if ( + params.resolved.existing && + params.store[params.resolved.normalizedKey] === params.resolved.existing + ) { + return params.resolved.normalizedKey; + } + const canonicalEntry = params.store[params.resolved.normalizedKey]; + if ( + canonicalEntry && + !hasMismatchedCaseSensitiveDeliveryProof(canonicalEntry, params.resolved.normalizedKey) + ) { + return params.resolved.normalizedKey; + } + const existingLegacyKey = params.resolved.legacyKeys.find( + (legacyKey) => params.store[legacyKey] === params.resolved.existing, + ); + if (existingLegacyKey) { + return existingLegacyKey; + } + return params.requestedKey.trim() || params.resolved.normalizedKey; +} + function rememberRemovedSessionFile( removedSessionFiles: Map, entry: SessionEntry,