refactor: rename session transcript file helpers

This commit is contained in:
Peter Steinberger
2026-05-08 17:53:50 +01:00
parent af1febcdb3
commit aca3105dbf
13 changed files with 85 additions and 80 deletions

View File

@@ -251,10 +251,10 @@ The remaining cleanup is mostly consolidation and deletion:
Zalo Personal, QA Channel, Microsoft Teams, Mattermost, Synology Chat, Tlon,
Twitch, and QQBot recording paths now read updated-at metadata and record
inbound session rows through SQLite identity.
- Transcript path persistence no longer uses `sessions.json` to find the
sibling JSONL location. `resolveSessionTranscriptFile` and
`resolveAndPersistSessionFile` derive transcript identity from `agentId`,
`sessionId`, and the stored SQLite session row.
- Transcript locator persistence no longer uses `sessions.json` to find a
sibling JSONL location. `resolveSessionTranscriptTarget` and
`resolveAndPersistSessionTranscriptLocator` derive transcript identity from
`agentId`, `sessionId`, and the stored SQLite session row.
- Cron persistence now reconciles SQLite `cron_jobs` rows instead of
deleting/reinserting the whole job table on each save. Plugin target
writebacks update matching cron rows directly and keep `cron.jobs.state` in
@@ -271,16 +271,16 @@ The remaining cleanup is mostly consolidation and deletion:
- Gateway transcript reader helpers now live in
`src/gateway/session-transcript-readers.ts` instead of the old
`session-utils.fs` module name. The fallback retry history check is named for
SQLite transcript content instead of session-file content.
SQLite transcript content instead of transcript-file content.
- Bootstrap continuation detection now checks SQLite transcript locators through
`hasCompletedBootstrapTranscriptTurn`; it no longer exposes a
session-file-shaped helper name.
`hasCompletedBootstrapTranscriptTurn`; it no longer exposes a file-shaped
helper name.
- Embedded-runner tests now use virtual SQLite transcript locators, and opening
a new locator without a duplicate `sessionId` uses the locator's session id
as the database row identity.
- Memory indexing helpers now use SQLite transcript terminology end to end:
host exports list/build session transcript entries, targeted sync queues
`sessionTranscripts`, and QMD/builtin indexers no longer expose session-file
`sessionTranscripts`, and QMD/builtin indexers no longer expose file-shaped
helper names.
- The generic plugin SDK persistent-dedupe helper no longer exposes file-shaped
options. Callers provide SQLite scope keys and durable dedupe rows live in
@@ -317,7 +317,7 @@ The remaining cleanup is mostly consolidation and deletion:
files. It imports the active branch into SQLite and removes the legacy source.
- Session-memory hook transcript lookup and context-engine transcript rewrite
helpers are now named around SQLite transcript paths/state instead of runtime
session-file reads or file rewrites.
transcript-file reads or file rewrites.
- Codex app-server conversation bindings now key SQLite plugin state by
OpenClaw session key when available, with transcript-path lookups kept only as
a legacy fallback for existing bindings.

View File

@@ -186,7 +186,7 @@ vi.mock("../config/sessions.js", () => ({
}));
vi.mock("../config/sessions/transcript-resolve.runtime.js", () => ({
resolveSessionTranscriptFile: async () => ({
resolveSessionTranscriptTarget: async () => ({
sessionFile: "/tmp/session.jsonl",
sessionEntry: { sessionId: "session-1", updatedAt: Date.now() },
}),

View File

@@ -908,10 +908,10 @@ async function agentCommandInternal(
sessionKey,
workspaceDir,
});
const { resolveSessionTranscriptFile } = await loadTranscriptResolveRuntime();
const { resolveSessionTranscriptTarget } = await loadTranscriptResolveRuntime();
let sessionFile: string | undefined;
if (sessionStore && sessionKey) {
const resolvedSessionFile = await resolveSessionTranscriptFile({
const resolvedTranscriptTarget = await resolveSessionTranscriptTarget({
sessionId,
sessionKey,
sessionStore,
@@ -919,19 +919,19 @@ async function agentCommandInternal(
agentId: sessionAgentId,
threadId: opts.threadId,
});
sessionFile = resolvedSessionFile.sessionFile;
sessionEntry = resolvedSessionFile.sessionEntry;
sessionFile = resolvedTranscriptTarget.sessionFile;
sessionEntry = resolvedTranscriptTarget.sessionEntry;
}
if (!sessionFile) {
const resolvedSessionFile = await resolveSessionTranscriptFile({
const resolvedTranscriptTarget = await resolveSessionTranscriptTarget({
sessionId,
sessionKey: sessionKey ?? sessionId,
sessionEntry,
agentId: sessionAgentId,
threadId: opts.threadId,
});
sessionFile = resolvedSessionFile.sessionFile;
sessionEntry = resolvedSessionFile.sessionEntry;
sessionFile = resolvedTranscriptTarget.sessionFile;
sessionEntry = resolvedTranscriptTarget.sessionEntry;
}
const startedAt = Date.now();

View File

@@ -3,7 +3,7 @@ import type { ThinkLevel, VerboseLevel } from "../../auto-reply/thinking.js";
import { appendSessionTranscriptMessage } from "../../config/sessions/transcript-append.js";
import {
readTailAssistantTextFromSessionTranscript,
resolveSessionTranscriptFile,
resolveSessionTranscriptTarget,
} from "../../config/sessions/transcript.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
@@ -192,7 +192,7 @@ async function persistTextTurnTranscript(
return params.sessionEntry;
}
const { sessionFile, sessionEntry } = await resolveSessionTranscriptFile({
const { sessionFile, sessionEntry } = await resolveSessionTranscriptTarget({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
sessionEntry: params.sessionEntry,

View File

@@ -369,11 +369,11 @@ async function estimatePromptTokensFromSessionTranscript(params: {
if (!sessionId) {
return undefined;
}
const fallbackSessionFile = normalizeOptionalString(params.sessionFile);
const fallbackTranscriptLocator = normalizeOptionalString(params.sessionFile);
const sessionEntryForTranscript =
params.sessionEntry?.sessionFile || !fallbackSessionFile
params.sessionEntry?.sessionFile || !fallbackTranscriptLocator
? params.sessionEntry
: ({ ...params.sessionEntry, sessionFile: fallbackSessionFile } as SessionEntry);
: ({ ...params.sessionEntry, sessionFile: fallbackTranscriptLocator } as SessionEntry);
try {
const snapshot = await readSessionLogSnapshot({
sessionId,

View File

@@ -19,8 +19,8 @@ import {
resolveThreadFlag,
type SessionFreshness,
} from "../../config/sessions/reset.js";
import { resolveAndPersistSessionFile } from "../../config/sessions/session-file.js";
import { resolveSessionKey } from "../../config/sessions/session-key.js";
import { resolveAndPersistSessionTranscriptLocator } from "../../config/sessions/session-locator.js";
import {
getSessionEntry,
listSessionEntries,
@@ -725,28 +725,28 @@ export async function initSessionState(params: {
sessionEntry.sessionId = forked.sessionId;
sessionEntry.sessionFile = forked.sessionFile;
sessionEntry.forkedFromParent = true;
log.warn(`forked session created: file=${forked.sessionFile}`);
log.warn(`forked session created: transcript=${forked.sessionFile}`);
}
}
}
const threadIdFromSessionKey = parseSessionThreadInfoFast(
sessionCtxForState.SessionKey ?? sessionKey,
).threadId;
const fallbackSessionFile = !sessionEntry.sessionFile
const fallbackTranscriptLocator = !sessionEntry.sessionFile
? createSqliteSessionTranscriptLocator({
sessionId: sessionEntry.sessionId,
agentId,
topicId: ctx.MessageThreadId ?? threadIdFromSessionKey,
})
: undefined;
const resolvedSessionFile = await resolveAndPersistSessionFile({
const resolvedTranscript = await resolveAndPersistSessionTranscriptLocator({
sessionId: sessionEntry.sessionId,
sessionKey,
sessionEntry,
agentId,
fallbackSessionFile,
fallbackTranscriptLocator,
});
sessionEntry = resolvedSessionFile.sessionEntry;
sessionEntry = resolvedTranscript.sessionEntry;
if (isNewSession) {
sessionEntry.compactionCount = 0;
sessionEntry.memoryFlushCompactionCount = undefined;

View File

@@ -226,11 +226,15 @@ vi.mock("../config/sessions/transcript-resolve.runtime.js", () => {
}
return normalizedParts.join(separator);
};
const resolveSessionFile = (sessionId: string, agentId: string, sessionsDir?: string): string =>
const resolveTranscriptLocator = (
sessionId: string,
agentId: string,
sessionsDir?: string,
): string =>
joinPath(sessionsDir ?? ".openclaw", "agents", agentId, "sessions", `${sessionId}.jsonl`);
return {
resolveSessionTranscriptFile: vi.fn(
resolveSessionTranscriptTarget: vi.fn(
async (params: {
sessionId: string;
sessionKey: string;
@@ -240,10 +244,11 @@ vi.mock("../config/sessions/transcript-resolve.runtime.js", () => {
threadId?: string | number;
}) => {
const sessionFileFromStorePath =
params.sessionEntry?.sessionFile ?? resolveSessionFile(params.sessionId, params.agentId);
params.sessionEntry?.sessionFile ??
resolveTranscriptLocator(params.sessionId, params.agentId);
const sessionFile = params.sessionEntry?.sessionFile
? sessionFileFromStorePath
: resolveSessionFile(params.sessionId, params.agentId);
: resolveTranscriptLocator(params.sessionId, params.agentId);
let sessionEntry = params.sessionEntry;
if (params.sessionStore && params.sessionKey) {
const existingEntry =

View File

@@ -16,7 +16,7 @@ export * from "./sessions/session-key.js";
export * from "./sessions/store.js";
export * from "./sessions/types.js";
export * from "./sessions/transcript.js";
export * from "./sessions/session-file.js";
export * from "./sessions/session-locator.js";
export * from "./sessions/delivery-info.js";
export * from "./sessions/targets.js";
export * from "./sessions/agent-purge.js";

View File

@@ -3,13 +3,13 @@ import { createSqliteSessionTranscriptLocator, isSqliteSessionTranscriptLocator
import { getSessionEntry, upsertSessionEntry } from "./store.js";
import type { SessionEntry } from "./types.js";
export async function resolveAndPersistSessionFile(params: {
export async function resolveAndPersistSessionTranscriptLocator(params: {
sessionId: string;
sessionKey: string;
sessionEntry?: SessionEntry;
agentId?: string;
fallbackSessionFile?: string;
}): Promise<{ sessionFile: string; sessionEntry: SessionEntry }> {
fallbackTranscriptLocator?: string;
}): Promise<{ transcriptLocator: string; sessionEntry: SessionEntry }> {
const { sessionId, sessionKey } = params;
const now = Date.now();
const agentId = params.agentId ?? resolveAgentIdFromSessionKey(sessionKey);
@@ -25,26 +25,26 @@ export async function resolveAndPersistSessionFile(params: {
const persistedSessionFile = baseEntry.sessionFile?.trim();
const shouldReusePersistedSessionFile =
baseEntry.sessionId === sessionId && isSqliteSessionTranscriptLocator(persistedSessionFile);
const fallbackSessionFile = params.fallbackSessionFile?.trim();
const sessionFile = shouldReusePersistedSessionFile
const fallbackTranscriptLocator = params.fallbackTranscriptLocator?.trim();
const transcriptLocator = shouldReusePersistedSessionFile
? persistedSessionFile!
: fallbackSessionFile && isSqliteSessionTranscriptLocator(fallbackSessionFile)
? fallbackSessionFile
: fallbackTranscriptLocator && isSqliteSessionTranscriptLocator(fallbackTranscriptLocator)
? fallbackTranscriptLocator
: createSqliteSessionTranscriptLocator({ agentId, sessionId });
const persistedEntry: SessionEntry = {
...baseEntry,
sessionId,
updatedAt: now,
sessionStartedAt: baseEntry.sessionId === sessionId ? (baseEntry.sessionStartedAt ?? now) : now,
sessionFile,
sessionFile: transcriptLocator,
};
if (baseEntry.sessionId !== sessionId || baseEntry.sessionFile !== sessionFile) {
if (baseEntry.sessionId !== sessionId || baseEntry.sessionFile !== transcriptLocator) {
upsertSessionEntry({
agentId,
sessionKey,
entry: persistedEntry,
});
return { sessionFile, sessionEntry: persistedEntry };
return { transcriptLocator, sessionEntry: persistedEntry };
}
return { sessionFile, sessionEntry: persistedEntry };
return { transcriptLocator, sessionEntry: persistedEntry };
}

View File

@@ -12,7 +12,7 @@ import {
validateSessionId,
} from "./paths.js";
import { evaluateSessionFreshness, resolveSessionResetPolicy } from "./reset.js";
import { resolveAndPersistSessionFile } from "./session-file.js";
import { resolveAndPersistSessionTranscriptLocator } from "./session-locator.js";
import {
getSessionEntry,
listSessionEntries,
@@ -517,8 +517,8 @@ describe("SQLite session store patch retries", () => {
});
});
describe("resolveAndPersistSessionFile", () => {
const fixture = useTempSessionsFixture("session-file-test-");
describe("resolveAndPersistSessionTranscriptLocator", () => {
const fixture = useTempSessionsFixture("session-locator-test-");
function readFixtureSessionEntries(): Record<string, SessionEntry> {
return Object.fromEntries(
@@ -543,53 +543,53 @@ describe("resolveAndPersistSessionFile", () => {
};
seedFixtureSessionEntries(store);
const sessionStore = readFixtureSessionEntries();
const fallbackSessionFile = createSqliteSessionTranscriptLocator({
const fallbackTranscriptLocator = createSqliteSessionTranscriptLocator({
agentId: "main",
sessionId,
topicId: 456,
});
const result = await resolveAndPersistSessionFile({
const result = await resolveAndPersistSessionTranscriptLocator({
sessionId,
sessionKey,
sessionEntry: sessionStore[sessionKey],
agentId: "main",
fallbackSessionFile,
fallbackTranscriptLocator,
});
expect(result.sessionFile).toBe(fallbackSessionFile);
expect(result.transcriptLocator).toBe(fallbackTranscriptLocator);
const saved = readFixtureSessionEntries();
expect(saved[sessionKey]?.sessionFile).toBe(fallbackSessionFile);
expect(saved[sessionKey]?.sessionFile).toBe(fallbackTranscriptLocator);
});
it("creates and persists a SQLite locator when session is not yet present", async () => {
const sessionId = "new-session-id";
const sessionKey = "agent:main:telegram:group:123";
const fallbackSessionFile = path.join(fixture.sessionsDir(), `${sessionId}.jsonl`);
const expectedSessionFile = createSqliteSessionTranscriptLocator({
const legacyFallbackPath = path.join(fixture.sessionsDir(), `${sessionId}.jsonl`);
const expectedTranscriptLocator = createSqliteSessionTranscriptLocator({
agentId: "main",
sessionId,
});
const result = await resolveAndPersistSessionFile({
const result = await resolveAndPersistSessionTranscriptLocator({
sessionId,
sessionKey,
agentId: "main",
fallbackSessionFile,
fallbackTranscriptLocator: legacyFallbackPath,
});
expect(result.sessionFile).toBe(expectedSessionFile);
expect(result.transcriptLocator).toBe(expectedTranscriptLocator);
expect(result.sessionEntry.sessionId).toBe(sessionId);
const saved = readFixtureSessionEntries();
expect(saved[sessionKey]?.sessionFile).toBe(expectedSessionFile);
expect(saved[sessionKey]?.sessionFile).toBe(expectedTranscriptLocator);
});
it("normalizes legacy stored transcript paths to SQLite locators", async () => {
const sessionId = "legacy-path-session-id";
const sessionKey = "agent:main:telegram:group:456";
const legacySessionFile = path.join(fixture.sessionsDir(), `${sessionId}.jsonl`);
const expectedSessionFile = createSqliteSessionTranscriptLocator({
const expectedTranscriptLocator = createSqliteSessionTranscriptLocator({
agentId: "main",
sessionId,
});
@@ -602,16 +602,16 @@ describe("resolveAndPersistSessionFile", () => {
});
const sessionStore = readFixtureSessionEntries();
const result = await resolveAndPersistSessionFile({
const result = await resolveAndPersistSessionTranscriptLocator({
sessionId,
sessionKey,
sessionEntry: sessionStore[sessionKey],
agentId: "main",
});
expect(result.sessionFile).toBe(expectedSessionFile);
expect(result.sessionEntry.sessionFile).toBe(expectedSessionFile);
expect(readFixtureSessionEntries()[sessionKey]?.sessionFile).toBe(expectedSessionFile);
expect(result.transcriptLocator).toBe(expectedTranscriptLocator);
expect(result.sessionEntry.sessionFile).toBe(expectedTranscriptLocator);
expect(readFixtureSessionEntries()[sessionKey]?.sessionFile).toBe(expectedTranscriptLocator);
});
it("rotates to a new SQLite locator when sessionId changes on the same session key", async () => {
@@ -619,7 +619,7 @@ describe("resolveAndPersistSessionFile", () => {
const nextSessionId = "new-session-id";
const sessionKey = "agent:main:telegram:group:123";
const previousSessionFile = path.join(fixture.sessionsDir(), `${previousSessionId}.jsonl`);
const expectedNextSessionFile = createSqliteSessionTranscriptLocator({
const expectedNextTranscriptLocator = createSqliteSessionTranscriptLocator({
agentId: "main",
sessionId: nextSessionId,
});
@@ -633,18 +633,18 @@ describe("resolveAndPersistSessionFile", () => {
seedFixtureSessionEntries(store);
const sessionStore = readFixtureSessionEntries();
const result = await resolveAndPersistSessionFile({
const result = await resolveAndPersistSessionTranscriptLocator({
sessionId: nextSessionId,
sessionKey,
sessionEntry: sessionStore[sessionKey],
agentId: "main",
});
expect(result.sessionFile).toBe(expectedNextSessionFile);
expect(result.sessionFile).not.toBe(previousSessionFile);
expect(result.sessionEntry.sessionFile).toBe(expectedNextSessionFile);
expect(result.transcriptLocator).toBe(expectedNextTranscriptLocator);
expect(result.transcriptLocator).not.toBe(previousSessionFile);
expect(result.sessionEntry.sessionFile).toBe(expectedNextTranscriptLocator);
const saved = readFixtureSessionEntries();
expect(saved[sessionKey]?.sessionFile).toBe(expectedNextSessionFile);
expect(saved[sessionKey]?.sessionFile).toBe(expectedNextTranscriptLocator);
});
});

View File

@@ -1 +1 @@
export { resolveSessionTranscriptFile } from "./transcript.js";
export { resolveSessionTranscriptTarget } from "./transcript.js";

View File

@@ -8,7 +8,7 @@ import {
import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
import { extractAssistantVisibleText } from "../../shared/chat-message-content.js";
import { createSqliteSessionTranscriptLocator } from "./paths.js";
import { resolveAndPersistSessionFile } from "./session-file.js";
import { resolveAndPersistSessionTranscriptLocator } from "./session-locator.js";
import { getSessionEntry, normalizeSessionRowKey } from "./store.js";
import { parseSessionThreadInfo } from "./thread-info.js";
import { appendSessionTranscriptMessage } from "./transcript-append.js";
@@ -98,7 +98,7 @@ function parseAssistantTranscriptEventText(event: unknown): AssistantTranscriptT
};
}
export async function resolveSessionTranscriptFile(params: {
export async function resolveSessionTranscriptTarget(params: {
sessionId: string;
sessionKey: string;
sessionEntry: SessionEntry | undefined;
@@ -109,22 +109,22 @@ export async function resolveSessionTranscriptFile(params: {
let sessionEntry = params.sessionEntry;
const threadIdFromSessionKey = parseSessionThreadInfo(params.sessionKey).threadId;
const fallbackSessionFile = !sessionEntry?.sessionFile
const fallbackTranscriptLocator = !sessionEntry?.sessionFile
? createSqliteSessionTranscriptLocator({
sessionId: params.sessionId,
agentId: params.agentId,
topicId: params.threadId ?? threadIdFromSessionKey,
})
: undefined;
const resolvedSessionFile = await resolveAndPersistSessionFile({
const resolvedTranscript = await resolveAndPersistSessionTranscriptLocator({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
sessionEntry,
agentId: params.agentId,
fallbackSessionFile,
fallbackTranscriptLocator,
});
const sessionFile = resolvedSessionFile.sessionFile;
sessionEntry = resolvedSessionFile.sessionEntry;
const sessionFile = resolvedTranscript.transcriptLocator;
sessionEntry = resolvedTranscript.sessionEntry;
if (params.sessionStore) {
params.sessionStore[params.sessionKey] = sessionEntry;
}
@@ -247,13 +247,13 @@ export async function appendExactAssistantMessageToSessionTranscript(params: {
let sessionFile: string;
try {
const resolvedSessionFile = await resolveAndPersistSessionFile({
const resolvedTranscript = await resolveAndPersistSessionTranscriptLocator({
sessionId: entry.sessionId,
sessionKey: normalizedKey,
sessionEntry: entry,
agentId,
});
sessionFile = resolvedSessionFile.sessionFile;
sessionFile = resolvedTranscript.transcriptLocator;
} catch (err) {
return {
ok: false,

View File

@@ -2,7 +2,7 @@
export { resolveSessionRowEntry } from "../config/sessions/store-entry.js";
export { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
export { resolveAndPersistSessionFile } from "../config/sessions/session-file.js";
export { resolveAndPersistSessionTranscriptLocator } from "../config/sessions/session-locator.js";
export { resolveSessionKey } from "../config/sessions/session-key.js";
export { resolveGroupSessionKey } from "../config/sessions/group.js";
export { canonicalizeMainSessionAlias } from "../config/sessions/main-session.js";