diff --git a/src/config/sessions/session-accessor.conformance.test.ts b/src/config/sessions/session-accessor.conformance.test.ts index f11320a8faef..f46f58b938bb 100644 --- a/src/config/sessions/session-accessor.conformance.test.ts +++ b/src/config/sessions/session-accessor.conformance.test.ts @@ -9,6 +9,7 @@ import { appendTranscriptEvent, appendTranscriptMessage, listSessionEntries, + loadExactSessionEntry, loadSessionEntry, loadTranscriptEvents, patchSessionEntry, @@ -17,6 +18,7 @@ import { replaceSessionEntry, updateSessionEntry, upsertSessionEntry, + type ExactSessionEntry, type SessionAccessScope, type SessionEntrySummary, type SessionTranscriptAccessScope, @@ -31,6 +33,7 @@ import { appendSqliteTranscriptEvent, appendSqliteTranscriptMessage, listSqliteSessionEntries, + loadExactSqliteSessionEntry, loadSqliteSessionEntry, loadSqliteTranscriptEvents, loadSqliteTranscriptEventsSync, @@ -48,6 +51,7 @@ type AccessorAdapter = { entryScope(paths: TestPaths): SessionAccessScope; transcriptReadScope(paths: TestPaths, id?: string): SessionTranscriptReadScope; transcriptScope(paths: TestPaths, id?: string): SessionTranscriptAccessScope; + loadExactSessionEntry(scope: SessionAccessScope): ExactSessionEntry | undefined; loadSessionEntry(scope: SessionAccessScope): SessionEntry | undefined; listSessionEntries(scope: Partial>): SessionEntrySummary[]; readSessionUpdatedAt(scope: SessionAccessScope): number | undefined; @@ -106,6 +110,7 @@ const fileBackedAdapter: AccessorAdapter = { storePath: paths.storePath, }), loadSessionEntry, + loadExactSessionEntry, listSessionEntries, readSessionUpdatedAt, upsertSessionEntry, @@ -140,6 +145,7 @@ const sqliteAdapter: AccessorAdapter = { storePath: paths.sqlitePath, }), loadSessionEntry: loadSqliteSessionEntry, + loadExactSessionEntry: loadExactSqliteSessionEntry, listSessionEntries: listSqliteSessionEntries, readSessionUpdatedAt: readSqliteSessionUpdatedAt, upsertSessionEntry: upsertSqliteSessionEntry, @@ -255,6 +261,30 @@ describe.each([fileBackedAdapter, sqliteAdapter])( }); }); + it("conforms for exact persisted-key lookup without canonical alias fallback", async () => { + const scope = adapter.entryScope(paths); + const mixedCaseScope = { ...scope, sessionKey: "AGENT:MAIN:MAIN" }; + + await adapter.upsertSessionEntry(scope, { + model: "gpt-5.5", + sessionId: "exact-session", + updatedAt: 10, + }); + + expect(adapter.loadSessionEntry(mixedCaseScope)).toMatchObject({ + model: "gpt-5.5", + sessionId: "exact-session", + }); + expect(adapter.loadExactSessionEntry(mixedCaseScope)).toBeUndefined(); + expect(adapter.loadExactSessionEntry(scope)).toEqual({ + sessionKey: "agent:main:main", + entry: expect.objectContaining({ + model: "gpt-5.5", + sessionId: "exact-session", + }), + }); + }); + it("conforms for raw transcript event load and append", async () => { const scope = adapter.transcriptScope(paths); const readScope = adapter.transcriptReadScope(paths); diff --git a/src/config/sessions/session-accessor.sqlite.ts b/src/config/sessions/session-accessor.sqlite.ts index 1b6f87f7f534..b21844cdb485 100644 --- a/src/config/sessions/session-accessor.sqlite.ts +++ b/src/config/sessions/session-accessor.sqlite.ts @@ -22,6 +22,7 @@ import { type OpenClawAgentDatabaseOptions, } from "../../state/openclaw-agent-db.js"; import type { + ExactSessionEntry, SessionAccessScope, SessionEntryPatchContext, SessionEntryPatchOptions, @@ -82,6 +83,20 @@ export function loadSqliteSessionEntry(scope: SessionAccessScope): SessionEntry return readSessionEntryRow(database, resolved.sessionKey)?.entry; } +/** Loads one exact persisted-key entry from the additive SQLite session store. */ +export function loadExactSqliteSessionEntry( + scope: SessionAccessScope, +): ExactSessionEntry | undefined { + const sessionKey = scope.sessionKey.trim(); + if (!sessionKey) { + return undefined; + } + const resolved = resolveSqliteScope(scope); + const database = openOpenClawAgentDatabase(toDatabaseOptions(resolved)); + const row = readExactSessionEntryRow(database, sessionKey); + return row ? { sessionKey, entry: row.entry } : undefined; +} + /** Lists session entries from the additive SQLite session store. */ export function listSqliteSessionEntries( scope: Partial> = {}, @@ -498,14 +513,18 @@ function parseSessionEntryRow(row: Pick): Session function readSessionEntryRow( database: OpenClawAgentDatabase, sessionKey: string, +): { entry: SessionEntry; row: SessionEntryRow } | undefined { + return readExactSessionEntryRow(database, normalizeSqliteSessionKey(sessionKey)); +} + +function readExactSessionEntryRow( + database: OpenClawAgentDatabase, + sessionKey: string, ): { entry: SessionEntry; row: SessionEntryRow } | undefined { const db = getSessionKysely(database.db); const row = executeSqliteQueryTakeFirstSync( database.db, - db - .selectFrom("session_entries") - .selectAll() - .where("session_key", "=", normalizeSqliteSessionKey(sessionKey)), + db.selectFrom("session_entries").selectAll().where("session_key", "=", sessionKey), ); if (!row) { return undefined;