fix: patch legacy sqlite session keys

This commit is contained in:
Peter Steinberger
2026-05-16 07:38:05 +01:00
parent cbef25bae3
commit 9632541cee
3 changed files with 69 additions and 7 deletions

View File

@@ -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",

View File

@@ -41,6 +41,7 @@ export type MoveSqliteSessionEntryKeyOptions = SqliteSessionEntriesOptions & {
export type ApplySqliteSessionEntriesPatchOptions = SqliteSessionEntriesOptions & {
upsertEntries?: Readonly<Record<string, SessionEntry>>;
expectedEntries?: ReadonlyMap<string, SessionEntry | null>;
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<SessionEntriesDatabase>(database.db)
.deleteFrom("session_entries")
.where("session_key", "=", sessionKey),
);
executeSqliteQuerySync(
database.db,
getNodeSqliteKysely<SessionEntriesDatabase>(database.db)
.deleteFrom("session_routes")
.where("session_key", "=", sessionKey),
);
}
return true;
}, options);
}

View File

@@ -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<SessionEntry | null> {
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;