refactor: remove transcript locator from session rows

This commit is contained in:
Peter Steinberger
2026-05-09 08:05:04 +01:00
parent 5099a02fce
commit af7dde1050
19 changed files with 45 additions and 68 deletions

View File

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

View File

@@ -521,7 +521,6 @@ async function persistAcpSpawnSessionRowBestEffort(params: {
sessionStartedAt: now,
}),
sessionId: params.sessionId,
sessionFile: params.sessionId,
};
upsertSessionEntry({
agentId: params.agentId,

View File

@@ -366,7 +366,6 @@ async function prepareSubagentSessionContext(params: {
}
const nextChildEntry = mergeSessionEntry(childEntry, {
sessionId: forked.sessionId,
transcriptLocator: forked.transcriptLocator,
forkedFromParent: true,
});
await subagentSpawnDeps.upsertSessionEntry({

View File

@@ -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) {

View File

@@ -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({

View File

@@ -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,

View File

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

View File

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

View File

@@ -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,

View File

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

View File

@@ -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<string, SessionPluginNextTurnInjection[]>;
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<T extends SessionEntry>(entry: T): T {
delete entry.transcriptLocator;
delete (entry as T & { transcriptLocator?: unknown }).transcriptLocator;
return entry;
}

View File

@@ -106,16 +106,10 @@ export function createCronPromptExecutor(params: {
Partial<Omit<CronAgentExecutionPhaseUpdate, "jobId" | "phase">>,
) => 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,

View File

@@ -31,7 +31,7 @@ function cronTranscriptExists(params: { sessionKey: string; entry: SessionEntry
}
function toNonResumableCronSessionEntry(entry: SessionEntry): SessionEntry {
const next = { ...entry } as Partial<SessionEntry>;
const next = { ...entry } as Partial<SessionEntry> & { transcriptLocator?: unknown };
delete next.sessionId;
delete next.sessionFile;
delete next.sessionStartedAt;

View File

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

View File

@@ -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,

View File

@@ -13,7 +13,6 @@ const SESSION_ENTRY_RESERVED_SLOT_KEY_LIST = [
"pluginNextTurnInjections",
"sessionId",
"updatedAt",
"sessionFile",
"spawnedBy",
"spawnedWorkspaceDir",
"parentSessionKey",

View File

@@ -265,7 +265,7 @@ const readUsageFromSessionLog = (
agentId ?? (sessionKey ? resolveAgentIdFromSessionKey(sessionKey) : undefined);
const snapshot = readRecentSessionUsageFromTranscript(
sessionId,
sessionEntry?.sessionFile,
undefined,
resolvedAgentId,
256 * 1024,
);

View File

@@ -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",

View File

@@ -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,