From 8fc499a71a52ea045bb3f2b5e38190083320bdcf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 8 May 2026 12:56:13 +0100 Subject: [PATCH] fix: preserve virtual session rotation locators --- .../reply/session-updates.lifecycle.test.ts | 53 +++++++++++++++++++ src/auto-reply/reply/session-updates.ts | 31 +++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/auto-reply/reply/session-updates.lifecycle.test.ts b/src/auto-reply/reply/session-updates.lifecycle.test.ts index efb88f3e8f2..4261cb4cb84 100644 --- a/src/auto-reply/reply/session-updates.lifecycle.test.ts +++ b/src/auto-reply/reply/session-updates.lifecycle.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; +import { createSqliteSessionTranscriptLocator } from "../../config/sessions.js"; import type { SessionEntry } from "../../config/sessions.js"; import { upsertSessionEntry } from "../../config/sessions/store.js"; import { replaceSqliteSessionTranscriptEvents } from "../../config/sessions/transcript-store.sqlite.js"; @@ -128,4 +129,56 @@ describe("session-updates lifecycle hooks", () => { agentId: "main", }); }); + + it("keeps SQLite transcript locators virtual when compaction rotates topic sessions", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-updates-sqlite-")); + tempDirs.push(root); + if (!previousStateDirCaptured) { + previousStateDir = process.env.OPENCLAW_STATE_DIR; + previousStateDirCaptured = true; + } + process.env.OPENCLAW_STATE_DIR = root; + const sessionKey = "agent:main:forum:direct:compaction:topic:456"; + const sessionFile = createSqliteSessionTranscriptLocator({ + agentId: "main", + sessionId: "s1", + topicId: 456, + }); + replaceSqliteSessionTranscriptEvents({ + agentId: "main", + sessionId: "s1", + transcriptPath: sessionFile, + events: [{ type: "message" }], + }); + const entry = { + sessionId: "s1", + sessionFile, + updatedAt: Date.now(), + compactionCount: 0, + } as SessionEntry; + const sessionStore: Record = { + [sessionKey]: entry, + }; + upsertSessionEntry({ agentId: "main", sessionKey, entry }); + const cfg = { session: {} } as OpenClawConfig; + + await incrementCompactionCount({ + cfg, + sessionEntry: entry, + sessionStore, + sessionKey, + newSessionId: "s2", + }); + + const expectedNextFile = createSqliteSessionTranscriptLocator({ + agentId: "main", + sessionId: "s2", + topicId: 456, + }); + expect(sessionStore[sessionKey]?.sessionFile).toBe(expectedNextFile); + expect(sessionStore[sessionKey]?.sessionFile).toContain("sqlite-transcript://"); + expect(sessionStore[sessionKey]?.sessionFile).not.toMatch(/^sqlite-transcript:\/[^/]/u); + const [endEvent] = hookRunnerMocks.runSessionEnd.mock.calls[0] ?? []; + expect(endEvent?.sessionFile).toBe(sessionFile); + }); }); diff --git a/src/auto-reply/reply/session-updates.ts b/src/auto-reply/reply/session-updates.ts index 1326ce61084..68e89f60f56 100644 --- a/src/auto-reply/reply/session-updates.ts +++ b/src/auto-reply/reply/session-updates.ts @@ -12,10 +12,13 @@ import { import { ensureSkillsWatcher } from "../../agents/skills/refresh.js"; import { hydrateResolvedSkills } from "../../agents/skills/snapshot-hydration.js"; import { + createSqliteSessionTranscriptLocator, resolveSessionFilePath, resolveSessionFilePathOptions, getSessionEntry, + isSqliteSessionTranscriptLocator, mergeSessionEntry, + parseSqliteSessionTranscriptLocator, type SessionEntry, upsertSessionEntry, } from "../../config/sessions.js"; @@ -382,6 +385,14 @@ function rewriteSessionFileForNewSessionId(params: { if (!trimmed) { return undefined; } + const sqliteScope = parseSqliteSessionTranscriptLocator(trimmed); + if (sqliteScope) { + return createSqliteSessionTranscriptLocator({ + agentId: sqliteScope.agentId, + sessionId: params.nextSessionId, + topicId: extractSqliteTranscriptTopicId(trimmed, params.previousSessionId), + }); + } const base = path.basename(trimmed); if (!base.endsWith(".jsonl")) { return undefined; @@ -404,3 +415,23 @@ function rewriteSessionFileForNewSessionId(params: { } return undefined; } + +function extractSqliteTranscriptTopicId( + locator: string, + previousSessionId: string, +): string | undefined { + if (!isSqliteSessionTranscriptLocator(locator)) { + return undefined; + } + try { + const url = new URL(locator.trim()); + const fileName = decodeURIComponent(url.pathname.replace(/^\/+/u, "")); + const topicPrefix = `${previousSessionId}-topic-`; + if (!fileName.endsWith(".jsonl") || !fileName.startsWith(topicPrefix)) { + return undefined; + } + return fileName.slice(topicPrefix.length, -".jsonl".length); + } catch { + return undefined; + } +}