fix: A2A handoffs can duplicate message-tool replies#97259
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 10, 2026, 8:23 AM ET / 12:23 UTC. Summary PR surface: Source +106, Tests +650, Other +140. Total +896 across 11 files. Reproducibility: yes. Current main can select assistant-shaped transcript artifacts, while the branch's focused tests and real Gateway/qa-channel scenario construct that path and observe one visible delivery after the fix. Review metrics: none identified. Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review detailsBest possible solution: Land the centralized artifact-aware reply boundary with its regression and QA scenario once the run-wait/A2A owners confirm that already-delivered transcript artifacts must terminate waited reply extraction. Do we have a high-confidence way to reproduce the issue? Yes. Current main can select assistant-shaped transcript artifacts, while the branch's focused tests and real Gateway/qa-channel scenario construct that path and observe one visible delivery after the fix. Is this the best way to solve the issue? Yes, subject to maintainer acceptance of the shared boundary. Centralized artifact-aware extraction is narrower and more maintainable than channel-specific dedupe, and avoids stale fallback as well as duplicate delivery. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 831f4630faff. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +106, Tests +650, Other +140. Total +896 across 11 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (9 earlier review cycles; latest 8 shown)
|
da1a789 to
cf7726e
Compare
|
I checked current On current Latest PR checks are green. |
cf7726e to
0a88f03
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
97d3b93 to
6576b4f
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
6576b4f to
8221d42
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
8221d42 to
b9a871d
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
b9a871d to
7633466
Compare
7633466 to
479495f
Compare
479495f to
ae7c8b1
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae7c8b1c2b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const details = readRecord(message.details); | ||
| return details?.sourceReplySink === "internal-ui" ? "internal-ui" : undefined; |
There was a problem hiding this comment.
Preserve source-reply details for Codex mirrors
For Codex-backed runs, this new marker is never set because extensions/codex/src/app-server/event-projector.ts:2566-2587 persists tool-result transcript rows with role/tool ids/content/timestamp but no details, while this path only reads message.details. When a message-tool-only source reply is meant for the outer A2A flow, run-wait sees the resulting mirror as a generic openclawMessageToolMirror artifact rather than an internal-ui source reply, so the waited A2A selector drops it and the requester never receives the legitimate reply. Propagate the tool result details into the Codex transcript row or derive the marker from the persisted result content before relying on it here.
Useful? React with 👍 / 👎.
|
Land-ready at exact head
The proof cycle caught and fixed an over-strict history assertion plus two incomplete typed payload fixtures before the final rerun. Known proof gap: no production WhatsApp message was sent; the deterministic QA Gateway/channel scenario provides the redacted end-to-end behavior proof. |
|
Merged via squash.
|
* fix: stop A2A source-reply mirror duplicates * fix(agents): harden A2A reply extraction * fix(agents): preserve internal A2A source replies * test(gateway): assert A2A mirror projection precisely * test(agents): complete A2A payload fixtures --------- Co-authored-by: Peter Steinberger <[email protected]>
Related: #39476
Related: #91161
Related: #93293
What Problem This Solves
Fixes duplicate visible output from an A2A handoff when the target session replies through
message(action="send")and the waited A2A path later mistakes the resulting assistant-shaped transcript mirror for a new reply.This was triggered by a real duplicate delivery in a local WhatsApp/Main Merid thread. Private message content is omitted, but the observed sequence was one correct requester-visible reply followed by a second visible delivery from the same A2A/source-reply flow.
This is a narrow companion to #39476. It does not replace #91161's reverse
sessions_sendguard or change the compaction replay behavior covered by #93293.Why This Change Was Made
chat.historycan project visible message-tool mirrors and forwardedsessions_sendrows as assistant messages. The existing waited-reply selector accepts those rows after only stripping tool messages, so A2A announce delivery can treat an already-delivered transcript artifact as a fresh model reply.This patch keeps waited A2A reply extraction on model-authored assistant replies by rejecting inter-session input rows, raw OpenClaw delivery mirrors, externally delivered message-tool mirrors, and Gateway-injected assistant rows. Internal source replies remain eligible only when the successful message-tool result proves the
internal-uisink and its originating transcript sequence follows the current request boundary. At a blocking artifact boundary the selector stops instead of falling back to an older reply. Structured non-presentationalincomplete_turnfailures stay silent, while trusted terminal presentation remains deliverable.No production config, secrets, channel contract, tool schema, delivery API, runtime permission, dependency, package metadata, or changelog file changes.
User Impact
A2A handoffs that reply visibly through the message tool no longer turn already-delivered source replies or forwarded
sessions_sendtranscript rows into a second visible message. Normal model-authored replies retain the existing waited-reply, ping-pong, and announce behavior.Evidence
Exact reviewed head:
ae7c8b1c2b8e03760c0a985e1465612c451b70a6, based on0b9c2e6f9532b14e1eacd88737768897989d4736. Fresh whole-branch autoreview found no remaining findings (confidence 0.82).Redacted Gateway/channel proof using the repository QA suite:
The scenario runs a real Gateway child with
qa-channelandmock-openai, exercises the A2A source-reply path, and observes exactly one requester-visible marker after the duplicate window.Exact-head Blacksmith Testbox proof (
tbx_01kx6e4n27jw4gjfqw75ynzceb, run 29107923271):corepack pnpm check:changedpassed every selected lane on Testbox, including all-project tsgo, core/extensions/scripts lint, runtime import cycles, dependency and boundary guards. Exact-head hosted CI is green in run 29109056235, including production/test types, lint, build artifacts, QA smoke, contract shards, and changed-surface checks.The proof cycle caught and fixed two test-quality gaps before landing: an over-strict whole-history assertion was replaced with an exact singleton visible-assistant assertion, and two
OutboundPayloadJsonfixtures received their requiredmediaUrlfield. The focused suite, all-lanes gate, QA scenario, and autoreview were rerun after those fixes.No live production WhatsApp message was sent for this proof. The behavior proof uses the repository's deterministic QA Gateway/channel harness with redacted markers.
AI-assisted: yes. Prepared with Codex, and I understand the code change.