Skip to content

fix(outbound): stop schema-padded poll modifiers from blocking send#89601

Merged
clawsweeper[bot] merged 1 commit into
openclaw:mainfrom
codezz:fix/poll-params-schema-padded-defaults
Jun 3, 2026
Merged

fix(outbound): stop schema-padded poll modifiers from blocking send#89601
clawsweeper[bot] merged 1 commit into
openclaw:mainfrom
codezz:fix/poll-params-schema-padded-defaults

Conversation

@codezz

@codezz codezz commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

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 validator interpreted pollDurationHours: 1 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 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 by src/agents/tools/message-tool.ts and src/infra/outbound/message-action-runner.ts), so the same tool-call shape would hit Telegram, Discord, or any other channel a message-tool agent talks to.

This PR narrows the shared-param branch to the content-bearing fields (pollQuestion, pollOption). Channel-extra poll* 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: only pollQuestion / pollOption count 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 asserting runMessageAction accepts a send with schema-padded pollDurationHours: 1 / pollMulti: false.

Behavior change for existing assertions

Two unit-level assertions changed semantics, both intentionally:

Input Old New Why
{ pollMulti: true } alone poll intent not intent A boolean modifier with no question or options is not a valid poll; in practice this combination only appears from schema-padding.
{ pollDurationHours: -1 } alone poll intent not intent Same rationale. The shared-schema modifier alone is no longer enough.

Channel-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 message tool schema action-aware: drop pollDurationHours / pollMulti from the send action's parameter surface entirely so agents never see them when calling send. 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 touching buildMessageToolPollProps and 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: hasPollCreationParams returned true for { pollDurationHours: 1 } alone — the value LLMs schema-pad onto every send tool call (the schema is Type.Optional(Type.Integer({ minimum: 1 }))). runMessageAction then threw Poll 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 through runMessageAction.

Real environment tested: OpenClaw 2026.5.28 (npm openclaw@2026.5.28), running in a production container openclaw-… on a Coolify host. Agent model openrouter/openrouter/auto (thinking=high). Slack channel via socket mode. Same bundled validator code path as upstream main: dist/message-action-runner-*.js calling hasPollCreationParams from the bundled poll-params module.

Exact steps or command run after this patch: I applied the equivalent semantic narrowing in the running container's bundled dist/message-action-runner-*.js via a small node patch script (guard the throw on pollQuestion non-empty OR pollOption non-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:

[gateway] http server listening (10 plugins: …, slack, …; 3.7s)
[slack] [default] starting provider
[slack] channels resolved: … (+21)
[slack] socket mode connected
[gateway] ready

The same tool-call shape (with pollDurationHours: 1, pollMulti: false) then went through runMessageAction and produced a delivered Slack message — no Poll 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 send calls.

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.ts uses the channel-agnostic runDrySend helper.

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 pass
  • vitest.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 pass
  • vitest.contracts-channel-session.config.ts: 11/11 files, 34/34 tests pass

The 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

  • AI-assisted (Claude / Claude Code). I read the failing transcript, the validator, the tool-schema callsite, and the existing tests; chose the narrowing scope; wrote the fix and tests; ran the targeted Vitest shards and the channels-contracts lane.
  • Real-behavior proof above is from my own OpenClaw deployment, not from AI-generated mocks.
  • I understand what the code does — the change is a strict subset of the previous logic for content-bearing inputs: any input that returned true before still returns true, except shared-modifier-only inputs that match the schema-pad shape.

`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.
@openclaw-barnacle openclaw-barnacle Bot added size: S triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 2, 2026
@clawsweeper

clawsweeper Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Codex review: passed. Reviewed June 2, 2026, 8:29 PM ET / 00:29 UTC.

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 through the shared message schema, classify non-zero shared duration as poll intent, and throw before a send can dispatch.

Review metrics: none identified.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Next step before merge

  • [P2] No repair job is needed because the patch has no blocking findings; the remaining work is exact-head CI, mergeability, and automerge gating.

Security
Cleared: The diff only changes TypeScript validation logic and tests; it does not touch workflows, dependencies, lockfiles, secrets, package metadata, or code execution surfaces.

Review details

Best 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 v2026.5.28 expose pollDurationHours through the shared message schema, classify non-zero shared duration as poll intent, and throw before a send can dispatch.

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 changes

Label changes:

  • add status: 🚀 automerge armed: This PR is in ClawSweeper's automerge lane. Sufficient (logs): The PR body includes redacted production Slack proof with the failing schema-padded tool call, runtime logs/live output after patching, and an observed delivered reply after the validator change.
  • remove status: 👀 ready for maintainer look: Current PR status label is status: 🚀 automerge armed.

Label justifications:

  • P1: The PR fixes a shared outbound validator false positive that can block real agent replies across channels when schema-padded sends include poll modifiers.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🦞 diamond lobster.
  • status: 🚀 automerge armed: This PR is in ClawSweeper's automerge lane. Sufficient (logs): The PR body includes redacted production Slack proof with the failing schema-padded tool call, runtime logs/live output after patching, and an observed delivered reply after the validator change.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes redacted production Slack proof with the failing schema-padded tool call, runtime logs/live output after patching, and an observed delivered reply after the validator change.
Evidence reviewed

PR surface:

Source -2, Tests +40. Total +38 across 3 files.

View PR surface stats
Area Files Added Removed Net
Source 1 23 25 -2
Tests 2 48 8 +40
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 3 71 33 +38

What I checked:

  • Repository policy read: Read the full root AGENTS.md plus scoped outbound and agent-tool guides; the review applied the read-beyond-diff, proof, scoped test, and no-mutation requirements. (AGENTS.md:1, 1f35ad12b3d6)
  • Current main rejects schema-padded duration: On current main, hasPollCreationParams iterates every shared poll param and returns true for any finite non-zero pollDurationHours, so the schema-padded value 1 is classified as poll creation intent. (src/poll-params.ts:77, 1f35ad12b3d6)
  • Current main send guard makes the helper user-visible: runMessageAction throws Poll fields require action "poll" for action === "send" when hasPollCreationParams(params) returns true, blocking outbound sends before channel dispatch. (src/infra/outbound/message-action-runner.ts:1449, 1f35ad12b3d6)
  • Shared message schema exposes the padded modifier: The message tool builds poll properties from SHARED_POLL_CREATION_PARAM_NAMES; pollDurationHours is emitted through the optional positive-integer schema, matching the PR's reported padded 1 shape. (src/agents/tools/message-tool.ts:540, 1f35ad12b3d6)
  • Latest release has the same behavior: Tag v2026.5.28 contains the same shared-param loop and send guard, so the reported behavior is not only a current-main artifact. (src/poll-params.ts:77, e93216080aa1)
  • PR patch narrows the validator at the central gate: The patch adds content-bearing shared poll detection for pollQuestion and pollOption, while leaving channel-specific poll* params strict through the existing unknown-poll-value path. (src/poll-params.ts:85, 0fd95756cd4a)

Likely related people:

  • steipete: GitHub file history shows repeated recent work on src/poll-params.ts, src/agents/tools/message-tool.ts, and outbound message routing, including poll-duration validation and message numeric schema alignment. (role: recent area contributor; confidence: high; commits: d2fbc8c0e70b, 36b0b12971a4, f5cb6177e4c1; files: src/poll-params.ts, src/agents/tools/message-tool.ts, src/infra/outbound/message-action-runner.ts)
  • gumadeiras: GitHub history shows poll-param ownership around scoping Telegram poll extras to plugin schema, adjacent to the shared/channel-extra distinction this PR preserves. (role: poll validation contributor; confidence: medium; commits: d8b95d2315eb; files: src/poll-params.ts, src/agents/tools/message-tool.ts)
  • Bartok9: The merged zero-valued poll-param fix changed the same helper behavior for schema/default noise and is directly adjacent to this regression's non-zero shared modifier case. (role: prior related fix author; confidence: medium; commits: c70ae1c96e7e; files: src/poll-params.ts, src/poll-params.test.ts)
  • frankekn: The merged zero-valued poll-param fix records review by this handle, making them a useful routing candidate for the same helper semantics. (role: prior related reviewer; confidence: medium; commits: c70ae1c96e7e; files: src/poll-params.ts, src/poll-params.test.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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 keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 High-priority user-facing bug, regression, or broken workflow. labels Jun 2, 2026
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 2, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 2, 2026
@Takhoffman

Copy link
Copy Markdown
Contributor

@clawsweeper automerge

@clawsweeper

clawsweeper Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🦞✅
ClawSweeper merged this PR after the passing review.

Source: clawsweeper[bot]
Feedback: structured ClawSweeper verdict: pass (sha=0fd95756cd4a67cdbc28453a0a51c38616b9b699)
Merge status: merged by ClawSweeper automerge
Merged at: 2026-06-03T00:30:02Z
Merge commit: 8b546facaf49

What merged:

  • 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.

The automerge loop is complete.

Automerge progress:

  • 2026-06-03 00:24:31 UTC review queued 0fd95756cd4a (queued)
  • 2026-06-03 00:29:50 UTC review passed 0fd95756cd4a (structured ClawSweeper verdict: pass (sha=0fd95756cd4a67cdbc28453a0a51c38616b9b...)
  • 2026-06-03 00:30:05 UTC merged 0fd95756cd4a (merged by ClawSweeper automerge)

@clawsweeper clawsweeper Bot added clawsweeper:automerge Maintainer opted this PR into bounded ClawSweeper-reviewed automerge status: 🚀 automerge armed This PR is in ClawSweeper's automerge lane. and removed status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jun 3, 2026
@clawsweeper clawsweeper Bot merged commit 8b546fa into openclaw:main Jun 3, 2026
258 of 276 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 3, 2026
…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>
vincentkoc pushed a commit that referenced this pull request Jun 3, 2026
…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)
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clawsweeper:automerge Maintainer opted this PR into bounded ClawSweeper-reviewed automerge P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 🚀 automerge armed This PR is in ClawSweeper's automerge lane.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants