diff --git a/src/config/sessions/session-accessor.conformance.test.ts b/src/config/sessions/session-accessor.conformance.test.ts index 466895d438b1..dd4e424d4716 100644 --- a/src/config/sessions/session-accessor.conformance.test.ts +++ b/src/config/sessions/session-accessor.conformance.test.ts @@ -472,9 +472,9 @@ describe.each([fileBackedAdapter, sqliteAdapter])( const readScope = adapter.transcriptReadScope(paths); const event = { id: "event-1", - message: { role: "user", content: "hello" }, parentId: null, - type: "message", + payload: { content: "hello" }, + type: "metadata", }; await adapter.appendTranscriptEvent(scope, { type: "session", sessionId: "session-1" }); @@ -491,9 +491,9 @@ describe.each([fileBackedAdapter, sqliteAdapter])( const readScope = sqliteAdapter.transcriptReadScope(paths); const event = { id: "event-1", - message: { role: "user", content: "hello" }, parentId: null, - type: "message", + payload: { content: "hello" }, + type: "metadata", }; await sqliteAdapter.appendTranscriptEvent(scope, event); @@ -869,4 +869,90 @@ describe("sqlite session normalization", () => { updated_at: expect.any(Number), }); }); + + it("normalizes missing entry updatedAt before writing root and entry rows", async () => { + const env = { ...process.env, OPENCLAW_STATE_DIR: paths.stateDir }; + await replaceSqliteSessionEntry( + { + agentId: "main", + env, + sessionKey: "agent:main:minimal", + storePath: paths.sqlitePath, + }, + { + sessionId: "minimal-session", + sessionStartedAt: 123, + } as SessionEntry, + ); + + const loaded = loadSqliteSessionEntry({ + agentId: "main", + env, + sessionKey: "agent:main:minimal", + storePath: paths.sqlitePath, + }); + expect(loaded).toMatchObject({ + sessionId: "minimal-session", + sessionStartedAt: 123, + updatedAt: 123, + }); + + const database = openOpenClawAgentDatabase({ + agentId: "main", + env, + path: paths.sqlitePath, + }); + const db = getNodeSqliteKysely(database.db); + const row = executeSqliteQueryTakeFirstSync( + database.db, + db + .selectFrom("sessions as s") + .innerJoin("session_entries as se", "se.session_id", "s.session_id") + .innerJoin("session_routes as sr", "sr.session_key", "se.session_key") + .select([ + "s.created_at as root_created_at", + "s.updated_at as root_updated_at", + "se.entry_json", + "se.updated_at as entry_updated_at", + "sr.updated_at as route_updated_at", + ]) + .where("s.session_id", "=", "minimal-session"), + ); + expect(row).toEqual({ + entry_json: JSON.stringify({ + sessionId: "minimal-session", + sessionStartedAt: 123, + updatedAt: 123, + }), + entry_updated_at: 123, + root_created_at: 123, + root_updated_at: 123, + route_updated_at: 123, + }); + + await upsertSqliteSessionEntry( + { + agentId: "main", + env, + sessionKey: "agent:main:minimal-upsert", + storePath: paths.sqlitePath, + }, + { + sessionId: "minimal-upsert-session", + }, + ); + const upsertRow = executeSqliteQueryTakeFirstSync( + database.db, + db + .selectFrom("session_entries") + .select(["entry_json", "updated_at"]) + .where("session_key", "=", "agent:main:minimal-upsert"), + ); + const upsertEntry = JSON.parse(upsertRow?.entry_json ?? "{}") as Partial; + expect(upsertEntry).toMatchObject({ + sessionId: "minimal-upsert-session", + updatedAt: expect.any(Number), + }); + expect(upsertRow?.updated_at).toBe(upsertEntry.updatedAt); + }); }); diff --git a/src/config/sessions/session-accessor.sqlite.ts b/src/config/sessions/session-accessor.sqlite.ts index cf088fa4f1f5..fa556e6f23f2 100644 --- a/src/config/sessions/session-accessor.sqlite.ts +++ b/src/config/sessions/session-accessor.sqlite.ts @@ -779,8 +779,9 @@ function writeSessionEntry( entry: SessionEntry, ): void { const db = getSessionKysely(database.db); - const updatedAt = entry.updatedAt; - const sessionRow = bindSqliteSessionRoot({ entry, sessionKey, updatedAt }); + const normalizedEntry = normalizeSqliteSessionEntryTimestamp(entry); + const updatedAt = normalizedEntry.updatedAt; + const sessionRow = bindSqliteSessionRoot({ entry: normalizedEntry, sessionKey, updatedAt }); executeSqliteQuerySync( database.db, db @@ -817,20 +818,34 @@ function writeSessionEntry( .insertInto("session_entries") .values({ session_key: sessionKey, - session_id: entry.sessionId, - entry_json: JSON.stringify(entry), + session_id: normalizedEntry.sessionId, + entry_json: JSON.stringify(normalizedEntry), updated_at: updatedAt, }) .onConflict((conflict) => conflict.column("session_key").doUpdateSet({ - session_id: entry.sessionId, - entry_json: JSON.stringify(entry), + session_id: normalizedEntry.sessionId, + entry_json: JSON.stringify(normalizedEntry), updated_at: updatedAt, }), ), ); } +function normalizeSqliteSessionEntryTimestamp(entry: SessionEntry): SessionEntry { + if (typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt)) { + return entry; + } + const updatedAt = + typeof entry.sessionStartedAt === "number" && Number.isFinite(entry.sessionStartedAt) + ? entry.sessionStartedAt + : Date.now(); + return { + ...entry, + updatedAt, + }; +} + function ensureTranscriptSessionRoot( database: OpenClawAgentDatabase, scope: ResolvedTranscriptScope,