fix: preserve virtual session rotation locators

This commit is contained in:
Peter Steinberger
2026-05-08 12:56:13 +01:00
parent 29e6c925bc
commit 8fc499a71a
2 changed files with 84 additions and 0 deletions

View File

@@ -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<string, SessionEntry> = {
[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);
});
});

View File

@@ -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;
}
}