From 7d094e4596d295a265a5e681cd61c2d358a640f0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 9 May 2026 21:15:19 +0100 Subject: [PATCH] fix: canonicalize legacy codex binding import --- .../agent-command.live-model-switch.test.ts | 2 +- .../doctor-session-transcripts.test.ts | 12 +++-- src/commands/doctor-session-transcripts.ts | 52 +++++++++++++------ src/commands/doctor-state-integrity.test.ts | 7 --- 4 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/agents/agent-command.live-model-switch.test.ts b/src/agents/agent-command.live-model-switch.test.ts index 6da0d0c9de3..e36616cc4f4 100644 --- a/src/agents/agent-command.live-model-switch.test.ts +++ b/src/agents/agent-command.live-model-switch.test.ts @@ -255,7 +255,7 @@ vi.mock("../terminal/ansi.js", () => ({ vi.mock("../trajectory/runtime.js", () => ({ createTrajectoryRuntimeRecorder: () => ({ enabled: true, - filePath: "/tmp/session.trajectory.jsonl", + runtimeLocator: "sqlite:default:trajectory:session-1", recordEvent: (...args: unknown[]) => state.trajectoryRecordEventMock(...args), flush: () => state.trajectoryFlushMock(), }), diff --git a/src/commands/doctor-session-transcripts.test.ts b/src/commands/doctor-session-transcripts.test.ts index 4ca85db6a30..20bb2c98a4b 100644 --- a/src/commands/doctor-session-transcripts.test.ts +++ b/src/commands/doctor-session-transcripts.test.ts @@ -178,8 +178,12 @@ describe("doctor session transcript repair", () => { it("imports legacy Codex app-server binding sidecars during repair mode", async () => { const sessionsDir = path.join(root, "agents", "main", "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); - const sessionFile = path.join(sessionsDir, "session.jsonl"); - const sidecarPath = `${sessionFile}.codex-app-server.json`; + const legacyTranscriptPath = path.join(sessionsDir, "session.jsonl"); + await fs.writeFile( + legacyTranscriptPath, + `${JSON.stringify({ type: "session", version: 3, id: "session-1", cwd: root })}\n`, + ); + const sidecarPath = `${legacyTranscriptPath}.codex-app-server.json`; await fs.writeFile( sidecarPath, JSON.stringify({ @@ -193,10 +197,10 @@ describe("doctor session transcript repair", () => { await noteSessionTranscriptHealth({ shouldRepair: true, sessionDirs: [sessionsDir] }); await expect(fs.access(sidecarPath)).rejects.toThrow(); - expect(readOpenClawStateKvJson("codex_app_server_thread_bindings", sessionFile)).toMatchObject({ + expect(readOpenClawStateKvJson("codex_app_server_thread_bindings", "session-1")).toMatchObject({ schemaVersion: 1, threadId: "thread-123", - sessionFile, + sessionId: "session-1", cwd: root, model: "gpt-5.5", }); diff --git a/src/commands/doctor-session-transcripts.ts b/src/commands/doctor-session-transcripts.ts index 11e9c6c5da7..31c4755768c 100644 --- a/src/commands/doctor-session-transcripts.ts +++ b/src/commands/doctor-session-transcripts.ts @@ -43,7 +43,8 @@ type TranscriptMigrationResult = TranscriptRepairResult & { type CodexAppServerBindingMigrationResult = { filePath: string; - sessionFile: string; + legacyTranscriptPath: string; + sessionId: string; imported: boolean; removedSource: boolean; reason?: string; @@ -296,12 +297,28 @@ async function listCodexAppServerBindingSidecars(sessionDirs: string[]): Promise return files.toSorted((a, b) => a.localeCompare(b)); } -function resolveCodexAppServerBindingSessionFile(sidecarPath: string): string { +function resolveCodexAppServerBindingTranscriptPath(sidecarPath: string): string { return sidecarPath.slice(0, -CODEX_APP_SERVER_BINDING_SIDECAR_SUFFIX.length); } +async function resolveCodexAppServerBindingSessionId( + legacyTranscriptPath: string, +): Promise { + try { + const raw = await fs.readFile(legacyTranscriptPath, "utf-8"); + const sessionId = getSessionId(parseTranscriptEntries(raw)); + if (sessionId) { + return sessionId; + } + } catch { + // Fall back to the legacy filename when only the sidecar survived. + } + const basename = path.basename(legacyTranscriptPath); + return basename.endsWith(".jsonl") ? basename.slice(0, -".jsonl".length) : basename; +} + function normalizeCodexAppServerBindingPayload( - sessionFile: string, + sessionId: string, value: unknown, ): OpenClawStateJsonValue | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) { @@ -317,7 +334,7 @@ function normalizeCodexAppServerBindingPayload( } return { schemaVersion: 1, - sessionFile, + sessionId, threadId: parsed.threadId, cwd: typeof parsed.cwd === "string" ? parsed.cwd : "", authProfileId: typeof parsed.authProfileId === "string" ? parsed.authProfileId : undefined, @@ -339,14 +356,16 @@ async function migrateCodexAppServerBindingSidecar(params: { filePath: string; shouldRepair: boolean; }): Promise { - const sessionFile = resolveCodexAppServerBindingSessionFile(params.filePath); + const legacyTranscriptPath = resolveCodexAppServerBindingTranscriptPath(params.filePath); + const sessionId = await resolveCodexAppServerBindingSessionId(legacyTranscriptPath); try { const raw = await fs.readFile(params.filePath, "utf-8"); - const payload = normalizeCodexAppServerBindingPayload(sessionFile, JSON.parse(raw)); + const payload = normalizeCodexAppServerBindingPayload(sessionId, JSON.parse(raw)); if (!payload) { return { filePath: params.filePath, - sessionFile, + legacyTranscriptPath, + sessionId, imported: false, removedSource: false, reason: "invalid binding payload", @@ -355,23 +374,26 @@ async function migrateCodexAppServerBindingSidecar(params: { if (!params.shouldRepair) { return { filePath: params.filePath, - sessionFile, + legacyTranscriptPath, + sessionId, imported: false, removedSource: false, }; } - writeOpenClawStateKvJson(CODEX_APP_SERVER_BINDING_KV_SCOPE, sessionFile, payload); + writeOpenClawStateKvJson(CODEX_APP_SERVER_BINDING_KV_SCOPE, sessionId, payload); await fs.rm(params.filePath, { force: true }); return { filePath: params.filePath, - sessionFile, + legacyTranscriptPath, + sessionId, imported: true, removedSource: true, }; } catch (error) { return { filePath: params.filePath, - sessionFile, + legacyTranscriptPath, + sessionId, imported: false, removedSource: false, reason: String(error), @@ -398,14 +420,14 @@ export async function noteSessionTranscriptHealth(params?: { return; } - const results: TranscriptMigrationResult[] = []; - for (const filePath of files) { - results.push(await migrateSessionTranscriptFileToSqlite({ filePath, shouldRepair })); - } const codexBindingResults: CodexAppServerBindingMigrationResult[] = []; for (const filePath of codexBindingSidecars) { codexBindingResults.push(await migrateCodexAppServerBindingSidecar({ filePath, shouldRepair })); } + const results: TranscriptMigrationResult[] = []; + for (const filePath of files) { + results.push(await migrateSessionTranscriptFileToSqlite({ filePath, shouldRepair })); + } const broken = results.filter((result) => result.broken); const imported = results.filter((result) => result.imported); const failed = results.filter((result) => result.reason && !result.imported); diff --git a/src/commands/doctor-state-integrity.test.ts b/src/commands/doctor-state-integrity.test.ts index f8119da322f..5aa2d46a9c3 100644 --- a/src/commands/doctor-state-integrity.test.ts +++ b/src/commands/doctor-state-integrity.test.ts @@ -580,12 +580,6 @@ describe("doctor state integrity oauth dir checks", () => { it("moves a heartbeat-poisoned main session and clears stale TUI restore pointers", async () => { const cfg: OpenClawConfig = {}; setupSessionState(process.env, tempHome); - const sessionsDir = resolveLegacySessionTranscriptsDirForAgent( - "main", - process.env, - () => tempHome, - ); - const heartbeatTranscriptPath = path.join(sessionsDir, "heartbeat-session.jsonl"); replaceSqliteSessionTranscriptEvents({ agentId: "main", sessionId: "heartbeat-session", @@ -597,7 +591,6 @@ describe("doctor state integrity oauth dir checks", () => { await writeSessionStore(cfg, { "agent:main:main": { sessionId: "heartbeat-session", - sessionFile: heartbeatTranscriptPath, updatedAt: Date.now(), }, });