diff --git a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts index 6a4191ecc49..88546d6a4c3 100644 --- a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts +++ b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts @@ -17,6 +17,7 @@ import { DEFAULT_REASONING_ONLY_RETRY_LIMIT, EMPTY_RESPONSE_RETRY_INSTRUCTION, extractPlanningOnlyPlanDetails, + hasCommittedUserVisibleToolDelivery, isLikelyExecutionAckPrompt, PLANNING_ONLY_RETRY_INSTRUCTION, REASONING_ONLY_RETRY_INSTRUCTION, @@ -919,6 +920,73 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { expect(incompleteTurnText).toContain("verify before retrying"); }); + it("does not treat empty committed messaging arrays as user-visible delivery", () => { + expect( + hasCommittedUserVisibleToolDelivery({ + messagingToolSentTexts: [" "], + messagingToolSentMediaUrls: [], + }), + ).toBe(false); + }); + + it("treats committed messaging media as user-visible delivery", () => { + expect( + hasCommittedUserVisibleToolDelivery({ + messagingToolSentTexts: [], + messagingToolSentMediaUrls: ["file:///tmp/render.png"], + }), + ).toBe(true); + }); + + it("treats committed messaging text as replay-invalid side effect metadata", () => { + expect( + buildAttemptReplayMetadata({ + toolMetas: [], + didSendViaMessagingTool: false, + messagingToolSentTexts: ["Delivered through the message tool."], + messagingToolSentMediaUrls: [], + }), + ).toEqual({ hadPotentialSideEffects: true, replaySafe: false }); + }); + + it("treats committed messaging media as replay-invalid side effect metadata", () => { + expect( + buildAttemptReplayMetadata({ + toolMetas: [], + didSendViaMessagingTool: false, + messagingToolSentTexts: [], + messagingToolSentMediaUrls: ["file:///tmp/render.png"], + }), + ).toEqual({ hadPotentialSideEffects: true, replaySafe: false }); + }); + + it("leaves committed delivery plus tool errors to the tool-error payload path", () => { + const incompleteTurnText = resolveIncompleteTurnPayloadText({ + payloadCount: 0, + aborted: false, + timedOut: false, + attempt: makeAttemptResult({ + assistantTexts: [], + didSendViaMessagingTool: true, + messagingToolSentTexts: ["Delivered through the message tool."], + lastToolError: { + toolName: "message", + meta: "send", + error: "delivery failed for second target", + }, + lastAssistant: { + role: "assistant", + stopReason: "error", + provider: "openai", + model: "gpt-5.4", + content: [], + } as unknown as EmbeddedRunAttemptResult["lastAssistant"], + }), + }); + + expect(incompleteTurnText).toBeNull(); + }); + it("does not retry reasoning-only GPT turns after side effects", () => { const retryInstruction = resolveReasoningOnlyRetryInstruction({ provider: "openai",