diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts index 6978f88fc..f583daf6a 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import type { SessionEntry } from "../../config/sessions.js"; +import * as sessions from "../../config/sessions.js"; import type { TypingMode } from "../../config/types.js"; import type { TemplateContext } from "../templating.js"; import type { GetReplyOptions } from "../types.js"; @@ -127,11 +128,14 @@ describe("runReplyAgent typing (heartbeat)", () => { try { const sessionId = "session"; const storePath = path.join(stateDir, "sessions", "sessions.json"); - const sessionEntry = { sessionId, updatedAt: Date.now() }; + const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId); + const sessionEntry = { sessionId, updatedAt: Date.now(), sessionFile: transcriptPath }; const sessionStore = { main: sessionEntry }; await fs.mkdir(path.dirname(storePath), { recursive: true }); await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8"); + await fs.mkdir(path.dirname(transcriptPath), { recursive: true }); + await fs.writeFile(transcriptPath, "ok", "utf-8"); runEmbeddedPiAgentMock .mockImplementationOnce(async () => { @@ -175,11 +179,14 @@ describe("runReplyAgent typing (heartbeat)", () => { try { const sessionId = "session"; const storePath = path.join(stateDir, "sessions", "sessions.json"); - const sessionEntry = { sessionId, updatedAt: Date.now() }; + const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId); + const sessionEntry = { sessionId, updatedAt: Date.now(), sessionFile: transcriptPath }; const sessionStore = { main: sessionEntry }; await fs.mkdir(path.dirname(storePath), { recursive: true }); await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8"); + await fs.mkdir(path.dirname(transcriptPath), { recursive: true }); + await fs.writeFile(transcriptPath, "ok", "utf-8"); runEmbeddedPiAgentMock .mockImplementationOnce(async () => ({ @@ -229,11 +236,14 @@ describe("runReplyAgent typing (heartbeat)", () => { try { const sessionId = "session"; const storePath = path.join(stateDir, "sessions", "sessions.json"); - const sessionEntry = { sessionId, updatedAt: Date.now() }; + const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId); + const sessionEntry = { sessionId, updatedAt: Date.now(), sessionFile: transcriptPath }; const sessionStore = { main: sessionEntry }; await fs.mkdir(path.dirname(storePath), { recursive: true }); await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8"); + await fs.mkdir(path.dirname(transcriptPath), { recursive: true }); + await fs.writeFile(transcriptPath, "ok", "utf-8"); runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({ payloads: [{ text: "Message ordering conflict - please try again.", isError: true }], @@ -260,6 +270,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); expect(payload.text?.toLowerCase()).toContain("reset"); expect(sessionStore.main.sessionId).not.toBe(sessionId); + await expect(fs.access(transcriptPath)).rejects.toBeDefined(); const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")); expect(persisted.main.sessionId).toBe(sessionStore.main.sessionId); diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index 215756967..42ca85111 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -1,4 +1,5 @@ import crypto from "node:crypto"; +import fs from "node:fs"; import { setCliSessionId } from "../../agents/cli-session.js"; import { lookupContextTokens } from "../../agents/context.js"; import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js"; @@ -8,6 +9,7 @@ import { queueEmbeddedPiMessage } from "../../agents/pi-embedded.js"; import { hasNonzeroUsage } from "../../agents/usage.js"; import { resolveAgentIdFromSessionKey, + resolveSessionFilePath, resolveSessionTranscriptPath, type SessionEntry, updateSessionStore, @@ -247,6 +249,8 @@ export async function runReplyAgent(params: { }; const resetSessionAfterRoleOrderingConflict = async (reason: string): Promise => { if (!sessionKey || !activeSessionStore || !storePath) return false; + const prevEntry = activeSessionStore[sessionKey] ?? activeSessionEntry; + const prevSessionId = prevEntry?.sessionId; const nextSessionId = crypto.randomUUID(); const nextEntry: SessionEntry = { ...(activeSessionStore[sessionKey] ?? activeSessionEntry), @@ -279,6 +283,19 @@ export async function runReplyAgent(params: { defaultRuntime.error( `Role ordering conflict (${reason}). Restarting session ${sessionKey} -> ${nextSessionId}.`, ); + if (prevSessionId) { + const transcriptCandidates = new Set(); + const resolved = resolveSessionFilePath(prevSessionId, prevEntry, { agentId }); + if (resolved) transcriptCandidates.add(resolved); + transcriptCandidates.add(resolveSessionTranscriptPath(prevSessionId, agentId)); + for (const candidate of transcriptCandidates) { + try { + fs.unlinkSync(candidate); + } catch { + // Best-effort cleanup. + } + } + } return true; }; try {