diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 8ea0f8d25f1..f2647210207 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -806,6 +806,8 @@ export async function runEmbeddedPiAgent( let activeSessionId = params.sessionId; let activeSessionFile = params.sessionFile; let suppressNextUserMessagePersistence = params.suppressNextUserMessagePersistence ?? false; + // Pi owns JSONL persistence; this marker only lets the outer retry avoid + // replaying the same inbound channel message after overflow compaction. let lastPersistedCurrentMessageId: string | number | undefined; const onUserMessagePersisted: RunEmbeddedPiAgentParams["onUserMessagePersisted"] = ( message, @@ -1648,6 +1650,9 @@ export async function runEmbeddedPiAgent( params.currentMessageId !== undefined && params.currentMessageId === lastPersistedCurrentMessageId ) { + // The first attempt reached Pi far enough to persist this user turn. + // Retrying the original prompt would replay it, so resume from the + // compacted transcript and suppress the next user append. nextAttemptPromptOverride = MID_TURN_PRECHECK_CONTINUATION_PROMPT; suppressNextUserMessagePersistence = true; } diff --git a/src/gateway/server-methods/chat.directive-tags.test.ts b/src/gateway/server-methods/chat.directive-tags.test.ts index f68b4316e60..72b69c582d7 100644 --- a/src/gateway/server-methods/chat.directive-tags.test.ts +++ b/src/gateway/server-methods/chat.directive-tags.test.ts @@ -755,6 +755,8 @@ describe("chat directive tag stripping for non-streaming final payloads", () => update.message !== null && (update.message as { role?: unknown }).role === "assistant", ); + // Agent-run delivery is a live projection; Pi message_end owns persisted + // assistant transcript entries, including stale media/text final payloads. expect(assistantUpdates).toEqual([]); const transcriptLines = fs .readFileSync(mockState.transcriptPath, "utf-8") @@ -804,6 +806,8 @@ describe("chat directive tag stripping for non-streaming final payloads", () => update.message !== null && (update.message as { role?: unknown }).role === "assistant", ); + // Normal agent-run final text must not be mirrored into JSONL by WebChat; + // Pi persists the model-visible assistant turn from message_end. expect(assistantUpdates).toEqual([]); const transcriptLines = fs .readFileSync(mockState.transcriptPath, "utf-8")