From 9632541ceee583eed58ccceb8366e179f36dc6f0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 16 May 2026 07:38:05 +0100 Subject: [PATCH] fix: patch legacy sqlite session keys --- ...-entries.session-key-normalization.test.ts | 22 ++++++++++++ src/config/sessions/session-entries.sqlite.ts | 18 ++++++++++ src/config/sessions/store.ts | 36 +++++++++++++++---- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/config/sessions/session-entries.session-key-normalization.test.ts b/src/config/sessions/session-entries.session-key-normalization.test.ts index a1996aa1171..fe736343e02 100644 --- a/src/config/sessions/session-entries.session-key-normalization.test.ts +++ b/src/config/sessions/session-entries.session-key-normalization.test.ts @@ -171,6 +171,28 @@ describe("SQLite session row key normalization", () => { expect(store[CANONICAL_KEY]?.updatedAt).toBeGreaterThan(100); }); + it("patches and migrates legacy direct mixed-case rows", async () => { + seedRawSessionEntry(MIXED_CASE_KEY, { + sessionId: "legacy-session", + updatedAt: 1, + chatType: "direct", + channel: "webchat", + }); + + await patchSessionEntry({ + agentId: "main", + sessionKey: MIXED_CASE_KEY, + update: () => ({ updatedAt: 200, modelOverride: "gpt-5.5" }), + }); + + const store = readMainSessionRows(); + expect(Object.keys(store)).toEqual([CANONICAL_KEY]); + expect(store[CANONICAL_KEY]).toMatchObject({ + sessionId: "legacy-session", + modelOverride: "gpt-5.5", + }); + }); + it("does not migrate legacy mixed-case entries during runtime updates", async () => { seedRawSessionEntry(MIXED_CASE_KEY, { sessionId: "legacy-session", diff --git a/src/config/sessions/session-entries.sqlite.ts b/src/config/sessions/session-entries.sqlite.ts index ce3db768795..b415ddf088e 100644 --- a/src/config/sessions/session-entries.sqlite.ts +++ b/src/config/sessions/session-entries.sqlite.ts @@ -41,6 +41,7 @@ export type MoveSqliteSessionEntryKeyOptions = SqliteSessionEntriesOptions & { export type ApplySqliteSessionEntriesPatchOptions = SqliteSessionEntriesOptions & { upsertEntries?: Readonly>; expectedEntries?: ReadonlyMap; + deleteEntries?: readonly string[]; }; export type SqliteSessionDeliveryContext = { @@ -764,6 +765,23 @@ export function applySqliteSessionEntriesPatch( }), ), ); + for (const sessionKey of options.deleteEntries ?? []) { + if (Object.prototype.hasOwnProperty.call(upsertEntries, sessionKey)) { + continue; + } + executeSqliteQuerySync( + database.db, + getNodeSqliteKysely(database.db) + .deleteFrom("session_entries") + .where("session_key", "=", sessionKey), + ); + executeSqliteQuerySync( + database.db, + getNodeSqliteKysely(database.db) + .deleteFrom("session_routes") + .where("session_key", "=", sessionKey), + ); + } return true; }, options); } diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index c443b18e472..4be79be77f8 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -100,6 +100,24 @@ export function moveSessionEntryKey( return moveSqliteSessionEntryKey(options); } +function resolvePatchSessionEntry(options: SessionEntryRowOptions & { sessionKey: string }): { + entry?: SessionEntry; + entryKey: string; + normalizedKey: string; +} { + const trimmedKey = options.sessionKey.trim(); + const normalizedKey = normalizeSessionRowKey(trimmedKey); + const canonical = readSqliteSessionEntry({ ...options, sessionKey: normalizedKey }); + if (canonical) { + return { entry: canonical, entryKey: normalizedKey, normalizedKey }; + } + const direct = + trimmedKey === normalizedKey + ? undefined + : readSqliteSessionEntry({ ...options, sessionKey: trimmedKey }); + return { entry: direct, entryKey: direct ? trimmedKey : normalizedKey, normalizedKey }; +} + export async function patchSessionEntry( options: SessionEntryRowOptions & { sessionKey: string; @@ -110,10 +128,10 @@ export async function patchSessionEntry( }, ): Promise { for (let attempt = 0; attempt < SESSION_ROW_PATCH_RETRY_LIMIT; attempt += 1) { - const stored = getSessionEntry(options); - const expected = stored ? structuredClone(stored) : null; - const existing = stored - ? structuredClone(stored) + const resolved = resolvePatchSessionEntry(options); + const expected = resolved.entry ? structuredClone(resolved.entry) : null; + const existing = resolved.entry + ? structuredClone(resolved.entry) : options.fallbackEntry ? structuredClone(options.fallbackEntry) : undefined; @@ -125,13 +143,17 @@ export async function patchSessionEntry( return existing; } const next = mergeSessionEntry(existing, patch); - const normalizedKey = normalizeSessionRowKey(options.sessionKey); + const expectedEntries = new Map([[resolved.entryKey, expected]]); + if (resolved.entryKey !== resolved.normalizedKey) { + expectedEntries.set(resolved.normalizedKey, null); + } const applied = applySqliteSessionEntriesPatch({ agentId: options.agentId, env: options.env, path: options.path, - upsertEntries: { [normalizedKey]: next }, - expectedEntries: new Map([[normalizedKey, expected]]), + upsertEntries: { [resolved.normalizedKey]: next }, + expectedEntries, + deleteEntries: resolved.entryKey === resolved.normalizedKey ? undefined : [resolved.entryKey], }); if (applied) { return next;