diff --git a/src/auto-reply/reply/session-transcript-replay.test.ts b/src/auto-reply/reply/session-transcript-replay.test.ts index e670dec4c66..aede8e95541 100644 --- a/src/auto-reply/reply/session-transcript-replay.test.ts +++ b/src/auto-reply/reply/session-transcript-replay.test.ts @@ -47,6 +47,15 @@ describe("replayRecentUserAssistantMessages", () => { expect(["user", "assistant"]).toContain(r.message.role); } expect(await call(path.join(root, "missing.jsonl"), path.join(root, "out.jsonl"))).toBe(0); + + const assistantSource = path.join(root, "all-assistant.jsonl"); + const assistantTarget = path.join(root, "all-assistant-out.jsonl"); + const onlyAssistants = Array.from({ length: 3 }, () => + j({ message: { role: "assistant", content: "x" } }), + ).join(""); + await fs.writeFile(assistantSource, onlyAssistants, "utf8"); + expect(await call(assistantSource, assistantTarget)).toBe(0); + await expect(fs.stat(assistantTarget)).rejects.toThrow(); }); it("skips header for pre-existing targets and aligns the tail to a user turn", async () => { diff --git a/src/auto-reply/reply/session-transcript-replay.ts b/src/auto-reply/reply/session-transcript-replay.ts index 5af998e0c8d..dfc5661810b 100644 --- a/src/auto-reply/reply/session-transcript-replay.ts +++ b/src/auto-reply/reply/session-transcript-replay.ts @@ -47,9 +47,14 @@ export async function replayRecentUserAssistantMessages(params: { return 0; } let startIdx = Math.max(0, kept.length - max); - while (startIdx < kept.length - 1 && kept[startIdx].role === "assistant") { + while (startIdx < kept.length && kept[startIdx].role === "assistant") { startIdx += 1; } + if (startIdx === kept.length) { + // Retained window is assistant-only; replaying would re-create the same + // role-ordering hazard this reset path is recovering from. + return 0; + } const tail = kept.slice(startIdx).map((entry) => entry.line); if (!fs.existsSync(params.targetTranscript)) { await fsp.mkdir(path.dirname(params.targetTranscript), { recursive: true });