fix: canonicalize embedded reply payloads#80075
Conversation
|
Codex review: needs changes before merge. Summary Reproducibility: yes. for the source-level bug: current main selects every non-empty accumulated assistantTexts entry, and final dispatch sends each payload item. I did not live-test Telegram, but the deterministic helper path plus linked issue evidence are enough to reproduce the code path. Real behavior proof Next step before merge Security Review findings
Review detailsBest possible solution: Update the canonical-selection predicate to treat raw final answers with media or audio directives as deliverable even when cleaned text is empty, then add a regression for accumulated progress plus silent media/voice. Do we have a high-confidence way to reproduce the issue? Yes for the source-level bug: current main selects every non-empty accumulated assistantTexts entry, and final dispatch sends each payload item. I did not live-test Telegram, but the deterministic helper path plus linked issue evidence are enough to reproduce the code path. Is this the best way to solve the issue? No, not yet. The shared payload boundary is the right place to solve this, but the current predicate excludes media-only canonical final replies and should prefer raw media/audio directives even without parsed text. Full review comments:
Overall correctness: patch is incorrect Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against c61abfab302f. |
23214e2 to
512c314
Compare
|
Landed via rebase onto
Thanks @nonzeroclaw and @dudaefj for the reports. |
Summary
Real behavior proof
assistantTextsentries were emitted as separate final payloads; this covers Telegram direct replies can duplicate because accumulated assistantTexts are emitted as separate messages after context-engine maintenance #79621 duplicate replies and the [Bug] Subagent announce leaks raw tool output to Telegram and duplicates parent progress reply #79986 raw path/status leakage shape.node --import tsx, branchcodex/chat-delivery-hygieneat23214e22bd1d6cdb7635ae1f4ebd818352468662.node --import tsx -e 'import { buildEmbeddedRunPayloads } from "./src/agents/pi-embedded-runner/run/payloads.ts"; const payloads = buildEmbeddedRunPayloads({ assistantTexts:["Done.", "Background task done: Context engine turn maintenance. Rewrote 0 transcript entries and freed 0 bytes."], toolMetas:[], lastAssistant:{ role:"assistant", stopReason:"stop", content:[{ type:"text", text:"Done." }] }, sessionKey:"session:telegram", inlineToolResultsAllowed:false, verboseLevel:"off", reasoningLevel:"off", toolResultFormat:"plain" }); console.log(JSON.stringify(payloads));'[ { "text": "Done.", "replyToTag": false, "audioAsVoice": false } ](JSON minified by the command as[{"text":"Done.","replyToTag":false,"audioAsVoice":false}])./root/openclaw/...protocol-schemas.ts:181...repro shape.assistantTextsentry, which would hand Telegram two final sends for the status-text repro and two final sends for the raw path-like repro.Root Cause
buildEmbeddedRunPayloadstreated every non-emptyassistantTextsentry as a separately deliverable final reply. The subscriber intentionally accumulates intermediate assistant text for streaming/block-reply bookkeeping, and Telegram then delivered each entry as its own outbound message. That made pre-tool progress, internal maintenance/status strings, or raw-looking subagent/tool fragments visible as duplicate Telegram replies.What Went Well
Verification
pnpm docs:listpnpm test src/agents/pi-embedded-runner/run/payloads.test.ts src/agents/pi-embedded-runner/run.incomplete-turn.test.ts src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.tspnpm test:changedgit diff --checkpnpm check:changedontbx_01kr7yhs5tfp3xxk4cp7f73rfcnode --import tsxrepro: raw path/status accumulated text now produces one final payloadFixes #79621.
Fixes #79986.