refactor: preserve custom session transcript paths

This commit is contained in:
Peter Steinberger
2026-05-08 18:52:17 +01:00
parent 9a0f9cc54b
commit f82d80e5a2
4 changed files with 39 additions and 48 deletions

View File

@@ -319,16 +319,14 @@ describe("forkSessionFromParentRuntime", () => {
if (fork === null) {
throw new Error("Expected forked session");
}
const agentSessionsDir = path.join(root, "agents", "main", "sessions");
expect(fork.sessionFile).toBe(fork.sessionId);
expect(fork.sessionId).not.toBe(parentSessionId);
const forkedEntries = readTranscript("main", fork.sessionId) as Array<Record<string, unknown>>;
const resolvedParentSessionFile = path.join(agentSessionsDir, `${parentSessionId}.jsonl`);
expect(forkedEntries[0]).toMatchObject({
type: "session",
id: fork.sessionId,
cwd,
parentSession: resolvedParentSessionFile,
parentSession: path.resolve(parentSessionFile),
});
expect(forkedEntries.map((entry) => entry.type)).toEqual([
"session",
@@ -377,17 +375,10 @@ describe("forkSessionFromParentRuntime", () => {
}
const entries = readTranscript("main", fork.sessionId) as Array<Record<string, unknown>>;
expect(entries).toHaveLength(1);
const resolvedParentSessionFile = path.join(
root,
"agents",
"main",
"sessions",
`${parentSessionId}.jsonl`,
);
expect(entries[0]).toMatchObject({
type: "session",
id: fork.sessionId,
parentSession: resolvedParentSessionFile,
parentSession: path.resolve(parentSessionFile),
});
});
});

View File

@@ -1,4 +1,5 @@
import crypto from "node:crypto";
import path from "node:path";
import {
CURRENT_SESSION_VERSION,
migrateSessionEntries,
@@ -54,11 +55,7 @@ async function estimateParentTranscriptTokensFromSqlite(params: {
agentId: string;
}): Promise<number | undefined> {
try {
const filePath = resolveSessionFilePath(
params.parentEntry.sessionId,
params.parentEntry,
resolveSessionFilePathOptions({ agentId: params.agentId }),
);
const filePath = resolveForkParentSessionFile(params.parentEntry, params.agentId);
const scope = resolveSqliteSessionTranscriptScope({
agentId: params.agentId,
sessionId: params.parentEntry.sessionId,
@@ -77,6 +74,18 @@ async function estimateParentTranscriptTokensFromSqlite(params: {
}
}
function resolveForkParentSessionFile(parentEntry: StoreSessionEntry, agentId: string): string {
const sessionFile = parentEntry.sessionFile?.trim();
if (sessionFile && path.isAbsolute(sessionFile)) {
return path.resolve(sessionFile);
}
return resolveSessionFilePath(
parentEntry.sessionId,
parentEntry,
resolveSessionFilePathOptions({ agentId }),
);
}
export async function resolveParentForkTokenCountRuntime(params: {
parentEntry: StoreSessionEntry;
agentId: string;
@@ -298,11 +307,7 @@ export async function forkSessionFromParentRuntime(params: {
parentEntry: StoreSessionEntry;
agentId: string;
}): Promise<{ sessionId: string; sessionFile: string } | null> {
const parentSessionFile = resolveSessionFilePath(
params.parentEntry.sessionId,
params.parentEntry,
{ agentId: params.agentId },
);
const parentSessionFile = resolveForkParentSessionFile(params.parentEntry, params.agentId);
if (!parentSessionFile) {
return null;
}

View File

@@ -119,9 +119,7 @@ afterAll(async () => {
async function makeCaseDir(prefix: string): Promise<string> {
const stateDir = path.join(suiteRoot, `${prefix}${++suiteCase}`);
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
const sessionsDir = path.join(stateDir, "agents", "main", "sessions");
await fs.mkdir(sessionsDir, { recursive: true });
return sessionsDir;
return path.join(stateDir, "transcript-fixtures", "main");
}
type TestSessionRowsTarget = {
@@ -353,11 +351,10 @@ describe("initSessionState thread forking", () => {
it("forks a new session from the parent session file", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const root = await makeCaseDir("openclaw-thread-session-");
const sessionsDir = path.join(root, "sessions");
await fs.mkdir(sessionsDir);
const transcriptDir = path.join(root, "thread-transcripts");
const parentSessionId = "parent-session";
const parentSessionFile = path.join(sessionsDir, "parent.jsonl");
const parentSessionFile = path.join(transcriptDir, "parent.jsonl");
const header = {
type: "session",
version: 3,
@@ -438,11 +435,10 @@ describe("initSessionState thread forking", () => {
it("forks from parent when thread session key already exists but was not forked yet", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const root = await makeCaseDir("openclaw-thread-session-existing-");
const sessionsDir = path.join(root, "sessions");
await fs.mkdir(sessionsDir);
const transcriptDir = path.join(root, "thread-transcripts");
const parentSessionId = "parent-session";
const parentSessionFile = path.join(sessionsDir, "parent.jsonl");
const parentSessionFile = path.join(transcriptDir, "parent.jsonl");
const header = {
type: "session",
version: 3,
@@ -520,11 +516,10 @@ describe("initSessionState thread forking", () => {
it("skips fork and creates fresh session when parent tokens exceed threshold", async () => {
const root = await makeCaseDir("openclaw-thread-session-overflow-");
const sessionsDir = path.join(root, "sessions");
await fs.mkdir(sessionsDir);
const transcriptDir = path.join(root, "thread-transcripts");
const parentSessionId = "parent-overflow";
const parentSessionFile = path.join(sessionsDir, "parent.jsonl");
const parentSessionFile = path.join(transcriptDir, "parent.jsonl");
const header = {
type: "session",
version: 3,
@@ -590,11 +585,10 @@ describe("initSessionState thread forking", () => {
it("skips fork when resolved parent token estimate exceeds threshold", async () => {
const root = await makeCaseDir("openclaw-thread-session-overflow-estimated-");
const sessionsDir = path.join(root, "sessions");
await fs.mkdir(sessionsDir);
const transcriptDir = path.join(root, "thread-transcripts");
const parentSessionId = "parent-overflow-estimated";
const parentSessionFile = path.join(sessionsDir, "parent.jsonl");
const parentSessionFile = path.join(transcriptDir, "parent.jsonl");
replaceSqliteSessionTranscriptEvents({
agentId: "main",
sessionId: parentSessionId,
@@ -1244,15 +1238,13 @@ describe("initSessionState RawBody", () => {
it("uses the default per-agent sessions store when config store is unset", async () => {
const root = await makeCaseDir("openclaw-session-store-default-");
const stateDir = path.join(root, ".openclaw");
const stateDir = path.dirname(path.dirname(root));
const agentId = "worker1";
const sessionKey = `agent:${agentId}:telegram:12345`;
const sessionId = "sess-worker-1";
const sessionFile = path.join(stateDir, "agents", agentId, "sessions", `${sessionId}.jsonl`);
const sessionRowsTarget = createSessionRowsTargetFromSessionsDir(
path.join(stateDir, "agents", agentId, "sessions"),
agentId,
);
const transcriptDir = path.join(stateDir, "transcript-fixtures", agentId);
const sessionFile = path.join(transcriptDir, `${sessionId}.jsonl`);
const sessionRowsTarget = createSessionRowsTargetFromSessionsDir(transcriptDir, agentId);
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
try {

View File

@@ -33,13 +33,16 @@ export async function resolveAndPersistSessionFile(params: {
: !baseEntry.sessionFile && fallbackSessionFile
? { ...baseEntry, sessionFile: fallbackSessionFile }
: baseEntry;
const entrySessionFile = entryForResolve.sessionFile?.trim();
const sessionFile =
fallbackSessionFile && !params.sessionsDir
? path.resolve(fallbackSessionFile)
: resolveSessionFilePath(sessionId, entryForResolve, {
agentId: params.agentId,
sessionsDir: params.sessionsDir,
});
!params.sessionsDir && entrySessionFile && path.isAbsolute(entrySessionFile)
? path.resolve(entrySessionFile)
: fallbackSessionFile && !params.sessionsDir
? path.resolve(fallbackSessionFile)
: resolveSessionFilePath(sessionId, entryForResolve, {
agentId: params.agentId,
sessionsDir: params.sessionsDir,
});
const persistedEntry: SessionEntry = {
...baseEntry,
sessionId,