fix(outbound): stop schema-padded poll modifiers from blocking send#89601
Conversation
`hasPollCreationParams` previously treated *any* of the shared poll params (`pollQuestion`, `pollOption`, `pollDurationHours`, `pollMulti`) as poll intent if present with a non-zero / non-default value. The shared `message` tool schema exposes those last two for both `send` and `poll` actions, and agent models routinely echo the schema's implied defaults (`1` for the `minimum: 1` integer, `false` for the boolean) on every plain `send` call alongside the rest of the schema-padded slots. The previous logic interpreted `pollDurationHours: 1` (or any non-zero duration / `pollMulti: true`) on its own as "the agent meant to create a poll", so `runMessageAction` threw Poll fields require action "poll"; use action "poll" instead of "send". on routine sends, blocking thread replies from agents whose tool calls include the full schema-padded param surface. Narrow the shared-param branch to the content-bearing fields (`pollQuestion`, `pollOption`). Channel-extra `poll*` params (`pollDurationSeconds`, `pollPublic`, `pollAnonymous`, ...) remain strict because they're not part of the shared tool-schema surface, so an explicit value still signals deliberate intent. Add a unit test covering the schema-pad shape and an integration test on `runMessageAction` confirming `send` is no longer rejected when only schema-padded modifier defaults are present.
|
Codex review: passed. Reviewed June 2, 2026, 8:29 PM ET / 00:29 UTC. Summary PR surface: Source -2, Tests +40. Total +38 across 3 files. Reproducibility: yes. Source inspection shows current main and Review metrics: none identified. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Next step before merge
Security Review detailsBest possible solution: Land the bounded validator fix after exact-head CI and automerge gates pass; keep any deeper action-aware schema pruning as a separate follow-up. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main and Is this the best way to solve the issue? Yes. The PR fixes the central validator that all channel sends pass through while preserving strict handling for poll content fields and channel-specific poll extras; action-aware schema pruning is plausible but broader than needed for this regression. AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against 1f35ad12b3d6. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source -2, Tests +40. Total +38 across 3 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 automerge |
|
🦞✅ Source: What merged:
Automerge notes:
The automerge loop is complete. Automerge progress:
|
…penclaw#89601) Summary: - The PR changes shared poll-intent detection so `pollDurationHours` and `pollMulti` alone no longer make `send` actions fail, with focused unit and outbound validation coverage. - PR surface: Source -2, Tests +40. Total +38 across 3 files. - Reproducibility: yes. Source inspection shows current main and `v2026.5.28` expose `pollDurationHours` throu ... d message schema, classify non-zero shared duration as poll intent, and throw before a `send` can dispatch. Automerge notes: - No ClawSweeper repair was needed after automerge opt-in. Validation: - ClawSweeper review passed for head 0fd9575. - Required merge gates passed before the squash merge. Prepared head SHA: 0fd9575 Review: openclaw#89601 (comment) Co-authored-by: Gabriel Fratica <gabriel@codez.ro> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
…89601) Summary: - The PR changes shared poll-intent detection so `pollDurationHours` and `pollMulti` alone no longer make `send` actions fail, with focused unit and outbound validation coverage. - PR surface: Source -2, Tests +40. Total +38 across 3 files. - Reproducibility: yes. Source inspection shows current main and `v2026.5.28` expose `pollDurationHours` throu ... d message schema, classify non-zero shared duration as poll intent, and throw before a `send` can dispatch. Automerge notes: - No ClawSweeper repair was needed after automerge opt-in. Validation: - ClawSweeper review passed for head 0fd9575. - Required merge gates passed before the squash merge. Prepared head SHA: 0fd9575 Review: #89601 (comment) Co-authored-by: Gabriel Fratica <gabriel@codez.ro> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com> (cherry picked from commit 8b546fa)
…penclaw#89601) Summary: - The PR changes shared poll-intent detection so `pollDurationHours` and `pollMulti` alone no longer make `send` actions fail, with focused unit and outbound validation coverage. - PR surface: Source -2, Tests +40. Total +38 across 3 files. - Reproducibility: yes. Source inspection shows current main and `v2026.5.28` expose `pollDurationHours` throu ... d message schema, classify non-zero shared duration as poll intent, and throw before a `send` can dispatch. Automerge notes: - No ClawSweeper repair was needed after automerge opt-in. Validation: - ClawSweeper review passed for head 0fd9575. - Required merge gates passed before the squash merge. Prepared head SHA: 0fd9575 Review: openclaw#89601 (comment) Co-authored-by: Gabriel Fratica <gabriel@codez.ro> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
Summary
hasPollCreationParamspreviously treated any of the shared poll params (pollQuestion,pollOption,pollDurationHours,pollMulti) as poll intent if present with a non-zero / non-default value.The shared
messagetool schema exposes those last two for bothsendandpollactions, and agent models routinely echo the schema's implied defaults (1for theminimum: 1integer,falsefor the boolean) on every plainsendcall alongside the rest of the schema-padded slots. The validator interpretedpollDurationHours: 1on its own as "the agent meant to create a poll", sorunMessageActionthrew:…on routine sends, blocking agent replies from any channel whose outbound goes through
runMessageAction(i.e. all of them). I first observed this on the Slack channel — Slack is where the regression surfaced for our deployment — but the bug is in shared/core code (src/poll-params.ts, used bysrc/agents/tools/message-tool.tsandsrc/infra/outbound/message-action-runner.ts), so the same tool-call shape would hit Telegram, Discord, or any other channel amessage-tool agent talks to.This PR narrows the shared-param branch to the content-bearing fields (
pollQuestion,pollOption). Channel-extrapoll*params (pollDurationSeconds,pollPublic,pollAnonymous, …) remain strict because they aren't part of the shared tool-schema surface, so an explicit value still signals deliberate intent.What changed
src/poll-params.ts— split shared-param handling: onlypollQuestion/pollOptioncount as standalone intent. Channel-extra detection is unchanged.src/poll-params.test.ts— updated two existing assertions and added a new test for the schema-pad shape.src/infra/outbound/message-action-runner.send-validation.test.ts— added an integration case assertingrunMessageActionaccepts asendwith schema-paddedpollDurationHours: 1/pollMulti: false.Behavior change for existing assertions
Two unit-level assertions changed semantics, both intentionally:
{ pollMulti: true }alone{ pollDurationHours: -1 }aloneChannel-extra equivalents (
pollAnonymous: true,pollPublic: true,pollDurationSeconds: -5) are unchanged and still count as intent, because they are not part of the schema-padded surface that agents echo on every send.Alternative considered
A more invasive fix would be to make the shared
messagetool schema action-aware: droppollDurationHours/pollMultifrom thesendaction's parameter surface entirely so agents never see them when callingsend. That addresses the root cause ("LLMs echo schema-implied defaults on every call") instead of catching it at the validator. It's a bigger schema-side change touchingbuildMessageToolPollPropsand the action-to-schema mapping. Happy to take that direction if maintainers prefer it; this PR is the minimal locality-of-change fix at the validator gate.Real behavior proof
Behavior or issue addressed:
hasPollCreationParamsreturnedtruefor{ pollDurationHours: 1 }alone — the value LLMs schema-pad onto everysendtool call (the schema isType.Optional(Type.Integer({ minimum: 1 }))).runMessageActionthen threwPoll fields require action "poll"; use action "poll" instead of "send", blocking the send. First surfaced as an agent that could no longer reply in any Slack thread it was @mentioned in; the same tool-call shape would affect every channel that routes throughrunMessageAction.Real environment tested: OpenClaw
2026.5.28(npmopenclaw@2026.5.28), running in a production containeropenclaw-…on a Coolify host. Agent modelopenrouter/openrouter/auto(thinking=high). Slack channel via socket mode. Same bundled validator code path as upstreammain:dist/message-action-runner-*.jscallinghasPollCreationParamsfrom the bundledpoll-paramsmodule.Exact steps or command run after this patch: I applied the equivalent semantic narrowing in the running container's bundled
dist/message-action-runner-*.jsvia a smallnodepatch script (guard the throw onpollQuestionnon-empty ORpollOptionnon-empty),docker restart-ed the container, then @mentioned the agent in a Slack thread to elicit the same tool-call shape that previously failed.Evidence after fix: redacted runtime logs and copied live output from the patched container. Failing tool-call shape captured from the pre-patch agent session transcript (10 occurrences in a single failed session, one shown, sensitive IDs redacted to
…):{ "action": "send", "channel": "slack", "target": "channel:C…", "message": "<@U…> hi", "threadId": "…", "pollQuestion": "", "pollOption": [], "pollDurationHours": 1, "pollMulti": false }Before patch: every such call threw
Poll fields require action "poll"…and no reply was delivered. After patch +docker restart, the gateway's runtime log showed clean startup and the message went out without the validator error:The same tool-call shape (with
pollDurationHours: 1, pollMulti: false) then went throughrunMessageActionand produced a delivered Slack message — noPoll fields require action "poll"thrown, no rejection in the outbound path.Observed result after fix: agent replied in-thread in Slack with the user-facing text. Outbound delivery completed with the workspace plugin returning a Slack message id. The validator no longer blocks schema-padded
sendcalls.What was not tested: I did not write a full end-to-end test against a real Slack workspace inside this PR's CI — the regression observed in the live container is the proof. I also did not exercise channels other than Slack (Telegram, Discord, …) for the same shape, though the failing code path is channel-agnostic and the integration test in
message-action-runner.send-validation.test.tsuses the channel-agnosticrunDrySendhelper.Local test results on this PR
Vitest 4.1.7, node 22.18.0, pnpm 11.2.2:
Changed-files lanes (
node scripts/test-projects.mjs --changed origin/main):vitest.unit-fast.config.ts: 1/1 file, 11/11 tests passvitest.infra.config.ts: 1/1 file, 24/24 tests pass (includes the new integration assertion)Shared-surface lanes (
pnpm test:contracts):vitest.contracts-channel-registry.config.ts: 11/11 files, 29/29 tests passvitest.contracts-channel-session.config.ts: 11/11 files, 34/34 tests passThe plugin-contracts portion of
pnpm test:contracts(vitest.contracts-plugin.config.ts) stalled with no output on my machine and was killed by the runner's watchdog; the touched files are not referenced by that lane, but flagging it for CI to confirm.AI assistance disclosure
truebefore still returnstrue, except shared-modifier-only inputs that match the schema-pad shape.