From 63bfc49f1f74e662765ed179fe7d09552af11b2f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 8 May 2026 13:35:10 +0100 Subject: [PATCH] refactor: resolve session transcripts through sqlite locators --- .../reply/session-updates.lifecycle.test.ts | 6 +- src/gateway/session-transcript-paths.test.ts | 46 ++++++++++++ src/gateway/session-transcript-paths.ts | 75 +++---------------- 3 files changed, 59 insertions(+), 68 deletions(-) create mode 100644 src/gateway/session-transcript-paths.test.ts diff --git a/src/auto-reply/reply/session-updates.lifecycle.test.ts b/src/auto-reply/reply/session-updates.lifecycle.test.ts index 4261cb4cb84..a6a5abf67f9 100644 --- a/src/auto-reply/reply/session-updates.lifecycle.test.ts +++ b/src/auto-reply/reply/session-updates.lifecycle.test.ts @@ -89,7 +89,7 @@ describe("session-updates lifecycle hooks", () => { }); it("emits compaction lifecycle hooks when newSessionId replaces the session", async () => { - const { sessionKey, sessionStore, entry, transcriptPath } = await createFixture(); + const { sessionKey, sessionStore, entry } = await createFixture(); const cfg = { session: {} } as OpenClawConfig; await incrementCompactionCount({ @@ -111,7 +111,9 @@ describe("session-updates lifecycle hooks", () => { sessionKey, reason: "compaction", }); - expect(endEvent?.sessionFile).toBe(path.resolve(transcriptPath)); + expect(endEvent?.sessionFile).toBe( + createSqliteSessionTranscriptLocator({ agentId: "main", sessionId: "s1" }), + ); expect(endContext).toMatchObject({ sessionId: "s1", sessionKey, diff --git a/src/gateway/session-transcript-paths.test.ts b/src/gateway/session-transcript-paths.test.ts new file mode 100644 index 00000000000..0a86139d54d --- /dev/null +++ b/src/gateway/session-transcript-paths.test.ts @@ -0,0 +1,46 @@ +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js"; +import { + resolveSessionTranscriptCandidates, + resolveStableSessionEndTranscript, +} from "./session-transcript-paths.js"; + +describe("resolveSessionTranscriptCandidates", () => { + it("returns sqlite locators and does not synthesize legacy jsonl paths", () => { + expect(resolveSessionTranscriptCandidates("s2", path.join("/tmp", "s1.jsonl"), "main")).toEqual( + [createSqliteSessionTranscriptLocator({ agentId: "main", sessionId: "s2" })], + ); + }); + + it("preserves explicit sqlite locators before generated agent locator candidates", () => { + const topicLocator = createSqliteSessionTranscriptLocator({ + agentId: "main", + sessionId: "s1", + topicId: "alerts", + }); + + expect(resolveSessionTranscriptCandidates("s2", topicLocator, "main")).toEqual([ + topicLocator, + createSqliteSessionTranscriptLocator({ agentId: "main", sessionId: "s2" }), + ]); + }); + + it("does not return legacy paths when no agent can resolve a database locator", () => { + expect(resolveSessionTranscriptCandidates("s1", path.join("/tmp", "s1.jsonl"))).toEqual([]); + }); +}); + +describe("resolveStableSessionEndTranscript", () => { + it("uses a generated sqlite locator instead of a legacy sessionFile path", () => { + expect( + resolveStableSessionEndTranscript({ + sessionId: "s1", + sessionFile: path.join("/tmp", "s1.jsonl"), + agentId: "main", + }), + ).toEqual({ + sessionFile: createSqliteSessionTranscriptLocator({ agentId: "main", sessionId: "s1" }), + }); + }); +}); diff --git a/src/gateway/session-transcript-paths.ts b/src/gateway/session-transcript-paths.ts index 3e55b26dbf8..4da70edb9de 100644 --- a/src/gateway/session-transcript-paths.ts +++ b/src/gateway/session-transcript-paths.ts @@ -1,51 +1,11 @@ -import path from "node:path"; import { createSqliteSessionTranscriptLocator, isSqliteSessionTranscriptLocator, - resolveSessionFilePath, } from "../config/sessions/paths.js"; -function normalizeTranscriptLocator(value: string): string { - return isSqliteSessionTranscriptLocator(value) ? value.trim() : path.resolve(value); -} - -function classifySessionTranscriptCandidate( - sessionId: string, - sessionFile?: string, -): "current" | "stale" | "custom" { - const transcriptSessionId = extractGeneratedTranscriptSessionId(sessionFile); - if (!transcriptSessionId) { - return "custom"; - } - return transcriptSessionId === sessionId ? "current" : "stale"; -} - -function extractGeneratedTranscriptSessionId(sessionFile?: string): string | undefined { - const trimmed = sessionFile?.trim(); - if (!trimmed) { - return undefined; - } - const base = path.basename(trimmed); - if (!base.endsWith(".jsonl")) { - return undefined; - } - const withoutExt = base.slice(0, -".jsonl".length); - const topicIndex = withoutExt.indexOf("-topic-"); - if (topicIndex > 0) { - const topicSessionId = withoutExt.slice(0, topicIndex); - return looksLikeGeneratedSessionId(topicSessionId) ? topicSessionId : undefined; - } - const forkMatch = withoutExt.match( - /^(\d{4}-\d{2}-\d{2}T[\w-]+(?:Z|[+-]\d{2}(?:-\d{2})?)?)_(.+)$/, - ); - if (forkMatch?.[2]) { - return looksLikeGeneratedSessionId(forkMatch[2]) ? forkMatch[2] : undefined; - } - return looksLikeGeneratedSessionId(withoutExt) ? withoutExt : undefined; -} - -function looksLikeGeneratedSessionId(value: string): boolean { - return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); +function normalizeTranscriptLocator(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed && isSqliteSessionTranscriptLocator(trimmed) ? trimmed : undefined; } export function resolveSessionTranscriptCandidates( @@ -54,7 +14,6 @@ export function resolveSessionTranscriptCandidates( agentId?: string, ): string[] { const candidates: string[] = []; - const sessionFileState = classifySessionTranscriptCandidate(sessionId, sessionFile); const pushCandidate = (resolve: () => string): void => { try { candidates.push(resolve()); @@ -63,29 +22,13 @@ export function resolveSessionTranscriptCandidates( } }; - if (sessionFile) { - if (agentId) { - if (sessionFileState === "custom") { - const trimmed = sessionFile.trim(); - if (trimmed) { - candidates.push(normalizeTranscriptLocator(trimmed)); - } - } else if (sessionFileState !== "stale") { - pushCandidate(() => resolveSessionFilePath(sessionId, { sessionFile }, { agentId })); - } - } else { - const trimmed = sessionFile.trim(); - if (trimmed) { - candidates.push(normalizeTranscriptLocator(trimmed)); - } - } + const normalizedSessionFile = normalizeTranscriptLocator(sessionFile); + if (normalizedSessionFile) { + candidates.push(normalizedSessionFile); } if (agentId) { pushCandidate(() => createSqliteSessionTranscriptLocator({ sessionId, agentId })); - if (sessionFile && sessionFileState === "stale") { - pushCandidate(() => resolveSessionFilePath(sessionId, { sessionFile }, { agentId })); - } } return Array.from(new Set(candidates)); @@ -96,9 +39,9 @@ export function resolveStableSessionEndTranscript(params: { sessionFile?: string; agentId?: string; }): { sessionFile?: string } { - const stablePath = params.sessionFile?.trim(); + const stablePath = normalizeTranscriptLocator(params.sessionFile); if (stablePath) { - return { sessionFile: normalizeTranscriptLocator(stablePath) }; + return { sessionFile: stablePath }; } const [candidate] = resolveSessionTranscriptCandidates( @@ -106,5 +49,5 @@ export function resolveStableSessionEndTranscript(params: { params.sessionFile, params.agentId, ); - return candidate ? { sessionFile: normalizeTranscriptLocator(candidate) } : {}; + return candidate ? { sessionFile: candidate } : {}; }