Clarify, warn, and recover message-tool-only finals#80949
Conversation
|
Codex review: needs real behavior proof before merge. Summary Reproducibility: yes. by source inspection: current group/channel turns resolve to message_tool_only and dispatch suppresses ordinary final payloads, so a final answer without message(action=send) stays private. I did not run a live channel repro in this read-only review. Real behavior proof Next step before merge Security Review findings
Review detailsBest possible solution: Keep the prompt guidance and privacy-safe audit, but make same-turn auto-delivery diagnostic-only unless maintainers explicitly approve a visible-delivery policy or config gate with live Discord/Telegram proof. Do we have a high-confidence way to reproduce the issue? Yes by source inspection: current group/channel turns resolve to message_tool_only and dispatch suppresses ordinary final payloads, so a final answer without message(action=send) stays private. I did not run a live channel repro in this read-only review. Is this the best way to solve the issue? No. The guidance and audit warning are narrow, but default auto-delivery of normal finals is not the safest maintainable fix without an explicit policy/config gate and live channel proof. Full review comments:
Overall correctness: patch is incorrect Security concerns:
Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 4e10969aded0. |
|
I took another pass over the I also checked current
I have a tested source-only alternative branch that implements a stricter runtime fallback instead of warning-only behavior:
Design summary:
Validation on the branch:
I am not opening a separate PR yet because this overlaps with the policy decision in #80949. The key decision seems to be:
My current read is that warning-only is safer, but it still leaves the user-visible workflow stalled until an operator notices the diagnostic. The fallback branch is a concrete implementation if maintainers want the runtime to repair the visible delivery miss in the same turn rather than only audit it. |
47056e8 to
bfdaff5
Compare
07c4068 to
88708d4
Compare
88708d4 to
5a05fad
Compare
|
Thanks for pushing this forward and for the detailed runtime harness proof. We landed the maintainer policy version in #82609 instead: 5759364. That keeps the current group/channel I am closing this PR because the default same-turn auto-delivery fallback here is the part we are not accepting right now. It can expose final text on surfaces configured for tool-only visible replies, and we want that behavior to remain an explicit policy/config choice rather than an implicit recovery path. The audit/logging/docs portion was useful and is represented by #82609. Thanks again for the careful branch and proof. |
Summary
message_tool_onlyconversations use the message tool instead of private normal final textmessage_tool_onlysource deliverymessage_tool_onlynormal finals through the normal final delivery path when no committed message-tool delivery evidence existssendPolicy: denyauthoritative and avoid duplicate visible posts when the message tool already deliveredWhy
On message-tool-only group/channel surfaces, normal assistant final text is intentionally private. If an agent completes work but puts the user-facing status/result in the normal final instead of calling the message tool, the channel can look stalled even though the model produced a useful answer.
This PR consolidates the prompt-only guidance from #80050 with the runtime audit/recovery work here so there is one review surface for the final-visibility issue.
The fix now handles three layers:
message(action=send)for visible source replies;Runtime policy
The fallback is intentionally narrow:
sourceReplyDeliveryMode === "message_tool_only"messagingToolSentTexts, media URLs, or committed targets)sendPolicy: denydeliverDespiteSourceReplySuppression) intactTests
git diff --checkcorepack pnpm install --frozen-lockfile --ignore-scriptscorepack pnpm exec vitest run src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts src/auto-reply/reply/dispatch-from-config.test.ts(119 passed)corepack pnpm exec vitest run src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts --config test/vitest/vitest.e2e.config.ts(50 passed)corepack pnpm exec vitest run src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts src/auto-reply/reply/dispatch-from-config.test.ts src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts src/auto-reply/reply/groups.test.ts src/agents/system-prompt.test.ts src/agents/tools/message-tool.test.ts src/auto-reply/reply/dispatch-acp.test.ts(339 passed in the default multi-project config; the e2e file is also covered separately by the command above)OPENCLAW_OXLINT_SKIP_PREPARE=1 node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.core.json src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts(0 warnings / 0 errors after the lint-only follow-up commit)Real behavior proof
message_tool_onlyfinal-visibility recovery: a normal final reply that missed the message tool is either audited as suppressed without leaking text, or, when explicitly marked as the guarded fallback and policy allows it, delivered through the normal final path.88708d4ce7on Linux arm64 / Node 22. The harness used the current source dispatch runtime with an isolated session store andplugins.enabled: false; no live Gateway, live Discord/Telegram channel, channel credentials, or watched config were used.tsx message-tool-final-fallback-proof.mts, a terminal runtime harness that imports the current sourcedispatchReplyFromConfig,setReplyPayloadMetadata, and diagnostics event path, then exercises threemessage_tool_onlyfinal-delivery cases.{ "proof": "message-tool-only final fallback terminal runtime harness", "liveGatewayUsed": false, "liveChannelUsed": false, "sourceMode": "message_tool_only", "results": [ { "case": "marked fallback delivers through final path", "queuedFinal": true, "sourceReplyDeliveryMode": "message_tool_only", "queuedCounts": { "tool": 0, "block": 0, "final": 1 }, "finalPayloads": [ { "textLen": 42, "hasFallbackMarker": true, "hasSuppressionBypassMarker": false } ], "warningCount": 0, "warnings": [] }, { "case": "unmarked final is suppressed and audited without text", "queuedFinal": false, "sourceReplyDeliveryMode": "message_tool_only", "queuedCounts": { "tool": 0, "block": 0, "final": 0 }, "finalPayloads": [], "warningCount": 1, "warnings": [ { "level": "warning", "loggerName": "auto-reply.dispatch", "suppressedFinalCount": 1, "suppressedFinalTextChars": 42, "sendPolicyDenied": false, "hasSuppressedText": false } ] }, { "case": "sendPolicy deny still blocks marked fallback", "queuedFinal": false, "sourceReplyDeliveryMode": "message_tool_only", "queuedCounts": { "tool": 0, "block": 0, "final": 0 }, "finalPayloads": [], "warningCount": 0, "warnings": [] } ] }suppressedFinalCount, text length, policy flag) withhasSuppressedText: false;sendPolicy: denystayed authoritative and blocked even a marked fallback.Scope
This is a source/runtime delivery fix only. It does not change installed dist artifacts, live gateway config, or channel credentials.