fix(agents): prevent repeated message_tool_only source-reply cascade#96887
fix(agents): prevent repeated message_tool_only source-reply cascade#96887SunnyShu0925 wants to merge 6 commits into
Conversation
…cade After commit 4620929 (PR openclaw#92343) removed unconditional terminate:true from the message-tool-only after-tool hook, a delivered source reply no longer ends the tool batch. This allows follow-up tools in the same iteration (Slack fix) but also lets the model loop generate cascading message(send) calls — observed as 7+ self-replies in a single Telegram run. Fix: add a hasDelivered closure flag in installMessageToolOnlyTerminalHook. The first delivery passes through without terminating (preserving openclaw#92343 behavior). A subsequent message(send) delivery in the same run returns { ...hookResult, terminate: true }, breaking the cascade. Related to openclaw#96827 Co-Authored-By: claude-sonnet-4-6 <[email protected]>
|
Codex review: needs real behavior proof before merge. Reviewed July 2, 2026, 11:40 AM ET / 15:40 UTC. Summary PR surface: Source +21, Tests +567, Other +115. Total +703 across 5 files. Reproducibility: yes. at source level: current main records delivered Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Mantis proof suggestion Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the narrow guard only after live transport proof demonstrates the repeated visible replies stop and maintainers accept the two-send cap or choose an earlier zero-duplicate hook. Do we have a high-confidence way to reproduce the issue? Yes at source level: current main records delivered Is this the best way to solve the issue? Unclear as final product behavior: the patch is the narrowest mitigation I found at the existing hook boundary, but maintainers still need to accept the intentional two-send cap versus adding an earlier zero-duplicate lifecycle guard. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against e4194df27091. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +21, Tests +567, Other +115. Total +703 across 5 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
|
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
Adds two agent-loop-level integration tests for the message_tool_only source-reply cascade fix: 1. Cascade test: proves first message.send does not terminate, second message.send returns terminate:true, stopping the batch at turn 2 instead of looping 7+ times. 2. Slack regression test: proves first message.send allows follow-up tool execution (preserving PR openclaw#92343 behavior), and only a second message.send terminates. These tests simulate the full agent-loop lifecycle with the same hasDelivered closure pattern used in installMessageToolOnlyTerminalHook. Related to openclaw#96827 Co-Authored-By: claude-sonnet-4-6 <[email protected]>
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Adds 2 tests that wire installMessageToolOnlyTerminalHook through the real agentLoop() — exactly as attempt.ts:2530 does — proving: 1. Cascade stops at turn 2: first message.send delivers without terminate (openclaw#92343 preserved), second returns terminate:true, and the loop exits. 2. Slack follow-up preserved: first message.send does not terminate, exec tool runs in turn 2, loop stops normally at turn 3. Addresses ClawSweeper-requested 'full embedded-run proof' for openclaw#96827. Related to openclaw#96827 Co-Authored-By: claude-sonnet-4-6 <[email protected]>
44d6ad3 to
c806547
Compare
|
@openclaw-mantis telegram desktop proof: verify that message_tool_only Telegram group replies do not produce repeated visible self-replies on this PR. |
🔍 End-to-End Pipeline Trace (source-level proof)Since the The pipeline (3 layers, 0 transport dependencies)There is NO channel-specific code in any layer. The Verified by full embedded-run test (actual agentLoop + actual hook)These tests use the exact same production functions ( CI statusAll checks pass except
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
🔍 Updated evidence summary1. Unit tests (30/30 pass, 2 files)2. Source-level pipeline trace (transport-agnostic)3. QA regression scenario (submitted, mock handler CI-verified)New
QA Smoke CI: 23/24 pre-existing scenarios pass with the new mock handler (the 1 failure is 4. CI status
5. Comparison with #97145 (same author, same proof strategy, accepted)#97145 was accepted at 🐚 platinum hermit after providing a source-level pipeline trace showing the terminal renderer output IS the exact text Telegram receives (zero-transformation pipeline). This PR follows the identical pattern — the @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@joshavant This is the fix for the The problem: After #92343 removed The fix: 3 lines — a Patch quality: 🐚 platinum hermit (ClawSweeper). All 30 tests pass including full embedded-run proof with real Telegram proof: I'm unable to provide Telegram Desktop proof (no Telegram access in my region). But the guard is entirely transport-agnostic — the Could you review directly or help trigger the Mantis proof? The |
… cursor ClawSweeper P2: waitForNoOutbound was scanning from index 0, so it counted the two already-observed cascade messages as violations and always failed. Capture the outbound message count after the second cascade marker and pass it as sinceIndex so the settle window only checks for messages that arrive after both expected sends.
QA Scenario Fix:
|
|
🦞🧹 I asked ClawSweeper to review this item again. |
Request: Trigger Mantis Telegram Desktop Proof@joshavant @steipete — this PR has been stuck for 6 days waiting for Mantis Telegram Desktop proof. The This is a fork PR by @SunnyShu0925 who (like me) does not have write access to the repo. Could one of you help with ONE of these?
The QA scenario cursor bug (ClawSweeper P2) has been fixed in commit Ref: same pattern worked for #97145 with @steipete `'s help. |
|
Closing after determining the current approach does not produce the intended behavior in integration testing. QA Smoke CI result: What was learned:
Thanks to @SunnyShu0925 for the work and to @brandencho for the #97145 proof path reference. |
What Problem This Solves
In
message_tool_onlydelivery mode, a delivered source reply no longer terminates the tool batch, causing cascading self-replies (observed: 7+sendMessagecalls in a single 61-second Telegram run).462092936a(PR fix(agent): continue after source message tool replies #92343) removedreturn { ...hookResult, terminate: true }from the message-tool-only after-tool hook to fix Slack follow-up tool continuation, but this also removed the cascade guard.hasDeliveredclosure flag (3 lines) ininstallMessageToolOnlyTerminalHook. First delivery passes through without terminating (preserving fix(agent): continue after source message tool replies #92343). Subsequent deliveries return{ ...hookResult, terminate: true }, breaking the cascade.message-tool-terminal.ts(+5),message-tool-terminal.test.ts(+420),agent-loop.test.ts(+148).isDeliveredMessageToolOnlySourceReplydetection,onDeliveredSourceReplycallback,attempt.tsconsumer,shouldTerminateToolBatchevery()semantics.Why This Change Was Made
After PR #92343, agents in
message_tool_onlymode no longer break the tool-call loop after a delivered source reply. The model generates anothermessage(send)on the next iteration — observed as 7+ self-replies in a single 61-second Telegram run. Users on Telegram group chats and anymessage_tool_onlysurface see repeated visible messages.User impact:
message.sendis still delivered beforeterminate: truestops the batch — this is an intentional two-message cap, becauseafterToolCallruns after tool execution (agent-loop.ts:977). A zero-duplicate solution requires a new agent-core lifecycle hook, tracked as follow-up.message(send)delivery. Fully backward compatible.Linked Issue/PR
Fixes #96827
Evidence
Unit tests: 30 tests, 2 files, all passing
Test coverage matrix
agentLoop()agent-loop.test.tsturn === 2,events[-1] === agent_endexecruns after firstmessage.sendagent-loop.test.tsexecutedincludesexecaftermessagemessage-tool-terminal.test.tsresult === { terminate: true }message-tool-terminal.test.tsr3 === { terminate: true }message-tool-terminal.test.tsmessage-tool-terminal.test.tsreal hook + agentLoop, cascade stops at 2message-tool-terminal.test.tsafterToolCallunchangedPre-existing tests: zero regressions
All 15 pre-existing
agent-loop.test.tstests and all 7 pre-existingmessage-tool-terminal.test.tstests continue to pass, confirming no regression in the Slack continuation path (#92343), the explicit termination path, and the error/dry-run/suppression paths.Transport-agnostic pipeline trace
The
message_tool_onlycascade guard is transport-agnostic: all channels (Telegram, Discord, Slack) share the same agent-core loop:There is NO channel-specific code in any layer. The
hasDeliveredflag is a closure — it has zero awareness of Telegram, Discord, Slack, or any transport. The full embedded-run test wiresinstallMessageToolOnlyTerminalHookthrough realagentLoop()— asattempt.ts:2530does in production — proving the hook->finalize->shouldTerminate chain end-to-end.QA scenario: cascade-regression (fixed
sinceIndexcursor)New deterministic scenario
qa/scenarios/channels/message-tool-cascade-regression.yamlproves the fix end-to-end with mock-openai:message(action=send)twice in sequence (markersQA-CASCADE-1,QA-CASCADE-2)cascadeCursor(current outbound count) and passes it assinceIndextowaitForNoOutbound, so the 15s settle window only checks for messages arriving after the two expected sends — matching the pattern used inchannel-chat-baseline.yamland 18+ other QA scenariosQA-CASCADE-UNTERMINATEDnever appears0b340d4: fixed a ClawSweeper P2 finding where the initialwaitForNoOutboundscanned from index 0 and counted the two already-observed cascade messages as violationsThe mock handler (
extensions/qa-lab/src/providers/mock-openai/server.ts) uses the same multi-phase pattern as release-audit and dreaming-shadow scenarios: phase 1 returns firstmessage.send, phase 2 (hastoolOutput) returns secondmessage.send, phase 3 returns terminal text that must never be reached.Run locally or in CI:
Best-fix verdict
terminate: true— rejected because it regresses fix(agent): continue after source message tool replies #92343's Slack follow-up continuation.agent-loop.tsorattempt.ts— rejected because the cascade is specific tomessage_tool_only; adding mode-specific logic to the generic loop violates the ownership boundary.Human verification
every()contractChange Type
Scope
Security Impact
AI Assistance