From af7dde1050adc319ac35507b3bdd7ef17fb62db7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 9 May 2026 08:05:04 +0100 Subject: [PATCH] refactor: remove transcript locator from session rows --- src/agents/acp-spawn.test.ts | 12 +++++++--- src/agents/acp-spawn.ts | 1 - src/agents/subagent-spawn.ts | 1 - src/agents/tools/sessions-list-tool.ts | 6 +---- src/auto-reply/reply/agent-runner-memory.ts | 2 +- src/auto-reply/reply/session-updates.ts | 5 ++++- .../doctor-heartbeat-main-session-repair.ts | 10 ++------- src/config/sessions/paths.ts | 2 +- src/config/sessions/session-locator.ts | 7 ++++-- src/config/sessions/store-backend.sqlite.ts | 4 +++- src/config/sessions/types.ts | 5 ++--- src/cron/isolated-agent/run-executor.ts | 18 +++++---------- src/cron/isolated-agent/run-session-state.ts | 2 +- src/cron/isolated-agent/run.ts | 7 ------ src/gateway/server-methods/chat.ts | 4 ++-- src/plugins/session-entry-slot-keys.ts | 1 - src/status/status-message.ts | 2 +- src/talk/agent-consult-runtime.ts | 22 +++++-------------- src/tui/embedded-backend.ts | 2 +- 19 files changed, 45 insertions(+), 68 deletions(-) diff --git a/src/agents/acp-spawn.test.ts b/src/agents/acp-spawn.test.ts index 723bc7e2dde..e3530777dbc 100644 --- a/src/agents/acp-spawn.test.ts +++ b/src/agents/acp-spawn.test.ts @@ -759,10 +759,12 @@ describe("spawnAcpDirect", () => { sessionKey: accepted.childSessionKey, entry: expect.objectContaining({ sessionId: "sess-123", - sessionFile: "sess-123", }), }), ); + for (const call of hoisted.upsertSessionEntryMock.mock.calls) { + expect(call[0].entry).not.toHaveProperty("transcriptLocator"); + } }); it("allows ACP resume IDs recorded for the requester session", async () => { @@ -1567,10 +1569,12 @@ describe("spawnAcpDirect", () => { agentId: "codex", entry: expect.objectContaining({ sessionId: "sess-123", - sessionFile: "sess-123", }), }), ); + expect(hoisted.upsertSessionEntryMock.mock.calls[0]?.[0].entry).not.toHaveProperty( + "transcriptLocator", + ); }); it("binds ACP sessions through the configured default account when accountId is omitted", async () => { @@ -1931,10 +1935,12 @@ describe("spawnAcpDirect", () => { agentId: "codex", entry: expect.objectContaining({ sessionId: "sess-123", - sessionFile: "sess-123", }), }), ); + expect(hoisted.upsertSessionEntryMock.mock.calls.at(-1)?.[0].entry).not.toHaveProperty( + "transcriptLocator", + ); } expectAgentGatewayCall(expectedAgentCall); }); diff --git a/src/agents/acp-spawn.ts b/src/agents/acp-spawn.ts index e56c12eb061..401e6ed28e3 100644 --- a/src/agents/acp-spawn.ts +++ b/src/agents/acp-spawn.ts @@ -521,7 +521,6 @@ async function persistAcpSpawnSessionRowBestEffort(params: { sessionStartedAt: now, }), sessionId: params.sessionId, - sessionFile: params.sessionId, }; upsertSessionEntry({ agentId: params.agentId, diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index ae9bb790188..b78c889d931 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -366,7 +366,6 @@ async function prepareSubagentSessionContext(params: { } const nextChildEntry = mergeSessionEntry(childEntry, { sessionId: forked.sessionId, - transcriptLocator: forked.transcriptLocator, forkedFromParent: true, }); await subagentSpawnDeps.upsertSessionEntry({ diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts index 3a14dda9f2e..fae55b9f39b 100644 --- a/src/agents/tools/sessions-list-tool.ts +++ b/src/agents/tools/sessions-list-tool.ts @@ -141,7 +141,6 @@ export function createSessionsListTool(opts?: { row: SessionListRow; titleEntry: SessionEntry; sessionId: string; - sessionFile?: string; agentId: string; }> = []; @@ -217,8 +216,6 @@ export function createSessionsListTool(opts?: { }); const sessionId = readStringValue(entry.sessionId); - const sessionFileRaw = (entry as { sessionFile?: unknown }).sessionFile; - const sessionFile = readStringValue(sessionFileRaw); const resolvedAgentId = resolveAgentIdFromSessionKey(key); const row: SessionListRow = { @@ -314,7 +311,6 @@ export function createSessionsListTool(opts?: { updatedAt: typeof row.updatedAt === "number" ? row.updatedAt : 0, }, sessionId, - ...(sessionFile ? { sessionFile } : {}), agentId: resolvedAgentId, }); } @@ -342,7 +338,7 @@ export function createSessionsListTool(opts?: { const target = titleTargets[next]; const fields = await readSessionTitleFieldsFromTranscriptAsync( target.sessionId, - target.sessionFile, + undefined, target.agentId, ); if (includeDerivedTitles && !target.row.derivedTitle) { diff --git a/src/auto-reply/reply/agent-runner-memory.ts b/src/auto-reply/reply/agent-runner-memory.ts index fc100e9d26c..7d029832024 100644 --- a/src/auto-reply/reply/agent-runner-memory.ts +++ b/src/auto-reply/reply/agent-runner-memory.ts @@ -554,7 +554,7 @@ export async function runPreflightCompactionIfNeeded(params: { }) ?? resolveSessionLogPath( entry.sessionId, - { ...entry, transcriptLocator: params.followupRun.run.transcriptLocator }, + entry, params.sessionKey ?? params.followupRun.run.sessionKey, ); const result = await memoryDeps.compactEmbeddedPiSession({ diff --git a/src/auto-reply/reply/session-updates.ts b/src/auto-reply/reply/session-updates.ts index 1ffdd22e6e4..c3ac74e0a07 100644 --- a/src/auto-reply/reply/session-updates.ts +++ b/src/auto-reply/reply/session-updates.ts @@ -285,7 +285,10 @@ export async function incrementCompactionCount(params: { updates.cacheRead = undefined; updates.cacheWrite = undefined; } - const { transcriptLocator: _derivedTranscriptLocator, ...entryWithoutLocator } = entry; + const { transcriptLocator: _derivedTranscriptLocator, ...entryWithoutLocator } = + entry as SessionEntry & { + transcriptLocator?: unknown; + }; sessionStore[sessionKey] = { ...entryWithoutLocator, ...updates, diff --git a/src/commands/doctor-heartbeat-main-session-repair.ts b/src/commands/doctor-heartbeat-main-session-repair.ts index d90331b0d3f..bcfb7986cb8 100644 --- a/src/commands/doctor-heartbeat-main-session-repair.ts +++ b/src/commands/doctor-heartbeat-main-session-repair.ts @@ -197,7 +197,7 @@ export async function repairHeartbeatPoisonedMainSession(params: { try { transcriptPath = resolveSessionTranscriptLocator( mainEntry.sessionId, - mainEntry, + undefined, params.sessionPathOpts, ); } catch { @@ -207,13 +207,7 @@ export async function repairHeartbeatPoisonedMainSession(params: { resolveHeartbeatMainSessionRepairCandidate({ entry, transcriptPath, - }) ?? - (mainEntry.sessionFile && mainEntry.sessionFile !== transcriptPath - ? resolveHeartbeatMainSessionRepairCandidate({ - entry, - transcriptPath: mainEntry.sessionFile, - }) - : null); + }); const candidate = resolveCandidate(mainEntry); if (!candidate) { return; diff --git a/src/config/sessions/paths.ts b/src/config/sessions/paths.ts index 94be10e993e..8d43e13eb46 100644 --- a/src/config/sessions/paths.ts +++ b/src/config/sessions/paths.ts @@ -73,7 +73,7 @@ export function isSqliteSessionTranscriptLocator(locator: string | undefined): b export function resolveSessionTranscriptLocator( sessionId: string, - entry?: { transcriptLocator?: string }, + entry?: unknown, opts?: SessionTranscriptLocatorOptions, ): string { void entry; diff --git a/src/config/sessions/session-locator.ts b/src/config/sessions/session-locator.ts index e8fb42402a3..c3232b7ebfb 100644 --- a/src/config/sessions/session-locator.ts +++ b/src/config/sessions/session-locator.ts @@ -34,8 +34,11 @@ export async function resolveAndPersistSessionTranscriptIdentity(params: { sessionStartedAt: baseEntry.sessionId === sessionId ? (baseEntry.sessionStartedAt ?? now) : now, }; const { transcriptLocator: _derivedTranscriptLocator, ...entryWithoutDerivedLocator } = - persistedEntry; - if (baseEntry.sessionId !== sessionId || baseEntry.transcriptLocator) { + persistedEntry as SessionEntry & { transcriptLocator?: unknown }; + if ( + baseEntry.sessionId !== sessionId || + "transcriptLocator" in (baseEntry as SessionEntry & { transcriptLocator?: unknown }) + ) { upsertSessionEntry({ agentId, sessionKey, diff --git a/src/config/sessions/store-backend.sqlite.ts b/src/config/sessions/store-backend.sqlite.ts index 5ecd5fa285e..6fb4f33567e 100644 --- a/src/config/sessions/store-backend.sqlite.ts +++ b/src/config/sessions/store-backend.sqlite.ts @@ -57,7 +57,9 @@ function stripDerivedTranscriptLocator(entry: SessionEntry | null): SessionEntry if (!entry) { return null; } - const { transcriptLocator: _derivedTranscriptLocator, ...rest } = entry; + const { transcriptLocator: _derivedTranscriptLocator, ...rest } = entry as SessionEntry & { + transcriptLocator?: unknown; + }; return rest; } diff --git a/src/config/sessions/types.ts b/src/config/sessions/types.ts index 7f12e4ffab7..e9d06e61b7d 100644 --- a/src/config/sessions/types.ts +++ b/src/config/sessions/types.ts @@ -90,7 +90,7 @@ export type SessionCompactionCheckpointReason = export type SessionCompactionTranscriptReference = { sessionId: string; - sessionFile?: string; + transcriptLocator?: string; leafId?: string; entryId?: string; }; @@ -195,7 +195,6 @@ export type SessionEntry = { pluginNextTurnInjections?: Record; sessionId: string; updatedAt: number; - sessionFile?: string; /** Parent session key that spawned this session (used for sandbox session-tool scoping). */ spawnedBy?: string; /** Workspace inherited by spawned sessions and reused on later turns for the same child session. */ @@ -486,7 +485,7 @@ function normalizeMergedUpdatedAt(value: number | undefined, now: number): numbe } function stripDerivedSessionEntryFields(entry: T): T { - delete entry.transcriptLocator; + delete (entry as T & { transcriptLocator?: unknown }).transcriptLocator; return entry; } diff --git a/src/cron/isolated-agent/run-executor.ts b/src/cron/isolated-agent/run-executor.ts index 79220e950b2..fa9d55f0661 100644 --- a/src/cron/isolated-agent/run-executor.ts +++ b/src/cron/isolated-agent/run-executor.ts @@ -106,16 +106,10 @@ export function createCronPromptExecutor(params: { Partial>, ) => void; }) { - const sessionFile = - params.cronSession.sessionEntry.sessionFile?.trim() || - createSqliteSessionTranscriptLocator({ - sessionId: params.cronSession.sessionEntry.sessionId, - agentId: params.agentId, - }); - // Fallback for callers that bypass prepareCronRunContext before persisting retries. - if (!params.cronSession.sessionEntry.sessionFile?.trim()) { - params.cronSession.sessionEntry.sessionFile = sessionFile; - } + const transcriptLocator = createSqliteSessionTranscriptLocator({ + sessionId: params.cronSession.sessionEntry.sessionId, + agentId: params.agentId, + }); const cronFallbacksOverride = resolveCronFallbacksOverride({ cfg: params.cfg, job: params.job, @@ -162,7 +156,7 @@ export function createCronPromptExecutor(params: { agentId: params.agentId, trigger: "cron", jobId: params.job.id, - sessionFile, + transcriptLocator, workspaceDir: params.workspaceDir, config: params.cfgWithAgentDefaults, prompt: promptText, @@ -210,7 +204,7 @@ export function createCronPromptExecutor(params: { messageTo: params.resolvedDelivery.to, messageThreadId: params.resolvedDelivery.threadId, currentChannelId, - sessionFile, + transcriptLocator, agentDir: params.agentDir, workspaceDir: params.workspaceDir, config: params.cfgWithAgentDefaults, diff --git a/src/cron/isolated-agent/run-session-state.ts b/src/cron/isolated-agent/run-session-state.ts index 200e99726d5..31da27b2989 100644 --- a/src/cron/isolated-agent/run-session-state.ts +++ b/src/cron/isolated-agent/run-session-state.ts @@ -31,7 +31,7 @@ function cronTranscriptExists(params: { sessionKey: string; entry: SessionEntry } function toNonResumableCronSessionEntry(entry: SessionEntry): SessionEntry { - const next = { ...entry } as Partial; + const next = { ...entry } as Partial & { transcriptLocator?: unknown }; delete next.sessionId; delete next.sessionFile; delete next.sessionStartedAt; diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 6124b38dcfe..0dcbbca1bb3 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -66,7 +66,6 @@ import { resolveHookExternalContentSource, isThinkingLevelSupported, resolveSupportedThinkingLevel, - createSqliteSessionTranscriptLocator, resolveThinkingDefault, setSessionRuntimeModel, } from "./run.runtime.js"; @@ -536,12 +535,6 @@ async function prepareCronRunContext(params: { forceNew: input.job.sessionTarget === "isolated", }); const runSessionId = cronSession.sessionEntry.sessionId; - if (!cronSession.sessionEntry.sessionFile?.trim()) { - cronSession.sessionEntry.sessionFile = createSqliteSessionTranscriptLocator({ - sessionId: runSessionId, - agentId, - }); - } const runSessionKey = baseSessionKey.startsWith("cron:") ? `${agentSessionKey}:run:${runSessionId}` : agentSessionKey; diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index af76954d6af..b701d854541 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -2385,7 +2385,7 @@ export const chatHandlers: GatewayRequestHandlers = { message: transcriptReply, ...(persistedContentForAppend?.length ? { content: persistedContentForAppend } : {}), sessionId, - transcriptLocator: resolvedTranscriptLocator, + transcriptLocator: resolvedTranscriptLocator ?? undefined, agentId, createIfMissing: true, idempotencyKey: `${clientRunId}:assistant-media`, @@ -2614,7 +2614,7 @@ export const chatHandlers: GatewayRequestHandlers = { ? { content: persistedContentForAppend } : {}), sessionId, - transcriptLocator: resolvedTranscriptLocator, + transcriptLocator: resolvedTranscriptLocator ?? undefined, agentId, createIfMissing: true, cfg, diff --git a/src/plugins/session-entry-slot-keys.ts b/src/plugins/session-entry-slot-keys.ts index 3683a52d6b7..449d60e10ae 100644 --- a/src/plugins/session-entry-slot-keys.ts +++ b/src/plugins/session-entry-slot-keys.ts @@ -13,7 +13,6 @@ const SESSION_ENTRY_RESERVED_SLOT_KEY_LIST = [ "pluginNextTurnInjections", "sessionId", "updatedAt", - "sessionFile", "spawnedBy", "spawnedWorkspaceDir", "parentSessionKey", diff --git a/src/status/status-message.ts b/src/status/status-message.ts index 006a0f7c1bb..b93cf4b1024 100644 --- a/src/status/status-message.ts +++ b/src/status/status-message.ts @@ -265,7 +265,7 @@ const readUsageFromSessionLog = ( agentId ?? (sessionKey ? resolveAgentIdFromSessionKey(sessionKey) : undefined); const snapshot = readRecentSessionUsageFromTranscript( sessionId, - sessionEntry?.sessionFile, + undefined, resolvedAgentId, 256 * 1024, ); diff --git a/src/talk/agent-consult-runtime.ts b/src/talk/agent-consult-runtime.ts index 6c63a581d07..d62aa4ac70a 100644 --- a/src/talk/agent-consult-runtime.ts +++ b/src/talk/agent-consult-runtime.ts @@ -4,10 +4,7 @@ import { forkSessionFromParent, resolveParentForkDecision, } from "../auto-reply/reply/session-fork.js"; -import { - createSqliteSessionTranscriptLocator, - isSqliteSessionTranscriptLocator, -} from "../config/sessions/paths.js"; +import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js"; import { parseSessionThreadInfoFast } from "../config/sessions/thread-info.js"; import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -167,7 +164,6 @@ async function resolveRealtimeVoiceAgentConsultSessionEntry(params: { ...existing, ...deliveryFields, sessionId: fork.sessionId, - sessionFile: fork.sessionFile, spawnedBy: requesterSessionKey, forkedFromParent: true, updatedAt: now, @@ -220,7 +216,6 @@ export async function consultRealtimeVoiceAgent(params: { provider?: RunEmbeddedPiAgentParams["provider"]; model?: RunEmbeddedPiAgentParams["model"]; thinkLevel?: RunEmbeddedPiAgentParams["thinkLevel"]; - fastMode?: RunEmbeddedPiAgentParams["fastMode"]; timeoutMs?: number; toolsAllow?: string[]; extraSystemPrompt?: string; @@ -250,14 +245,10 @@ export async function consultRealtimeVoiceAgent(params: { resolvedDeliveryContext ?? deliveryContextFromSession(sessionEntry); const sessionId = sessionEntry.sessionId; - const persistedSessionFile = sessionEntry.sessionFile?.trim(); - const sessionFile = - persistedSessionFile && isSqliteSessionTranscriptLocator(persistedSessionFile) - ? persistedSessionFile - : createSqliteSessionTranscriptLocator({ - agentId, - sessionId, - }); + const transcriptLocator = createSqliteSessionTranscriptLocator({ + agentId, + sessionId, + }); const result = await params.agentRuntime.runEmbeddedPiAgent({ sessionId, sessionKey: params.sessionKey, @@ -273,7 +264,7 @@ export async function consultRealtimeVoiceAgent(params: { consultDeliveryContext?.threadId != null ? String(consultDeliveryContext.threadId) : undefined, - sessionFile, + transcriptLocator, workspaceDir, config: params.cfg, prompt: buildRealtimeVoiceAgentConsultPrompt({ @@ -287,7 +278,6 @@ export async function consultRealtimeVoiceAgent(params: { provider: params.provider, model: params.model, thinkLevel: params.thinkLevel ?? "high", - fastMode: params.fastMode, verboseLevel: "off", reasoningLevel: "off", toolResultFormat: "plain", diff --git a/src/tui/embedded-backend.ts b/src/tui/embedded-backend.ts index 2ac480d2fc6..d1acb0a1ee5 100644 --- a/src/tui/embedded-backend.ts +++ b/src/tui/embedded-backend.ts @@ -207,7 +207,7 @@ export class EmbeddedTuiBackend implements TuiBackend { const max = Math.min(1000, typeof opts.limit === "number" ? opts.limit : 200); const maxHistoryBytes = getMaxChatHistoryMessagesBytes(); const localMessages = sessionId - ? await readSessionMessagesAsync(sessionId, entry?.sessionFile, { + ? await readSessionMessagesAsync(sessionId, undefined, { agentId: sessionAgentId, mode: "recent", maxMessages: max,