From 339d3759b0338e3966086dc6b52df6e50d16afaa Mon Sep 17 00:00:00 2001 From: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:30:51 -0400 Subject: [PATCH] fix(auto-reply): skip replay when retained window is assistant-only Codex follow-up review on #70898: the `startIdx < kept.length - 1` guard still wrote an assistant-first transcript when every turn inside the retained window was an assistant (e.g. a tail of consecutive assistant messages). That re-creates the exact role-ordering hazard the reset path is recovering from. Advance through the full window and, if no user turn remains, return 0 without touching the target transcript. Missing a few turns of DM continuity is strictly better than re-triggering the conflict. Locked in with a new assertion on the existing unit test. Made-with: Cursor --- src/auto-reply/reply/session-transcript-replay.test.ts | 9 +++++++++ src/auto-reply/reply/session-transcript-replay.ts | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) 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 });