Skip to content

fix: treat empty-string optional integer tool params as unset#100273

Closed
snotty wants to merge 1 commit into
openclaw:mainfrom
snotty:fix/optional-int-param-empty-string
Closed

fix: treat empty-string optional integer tool params as unset#100273
snotty wants to merge 1 commit into
openclaw:mainfrom
snotty:fix/optional-int-param-empty-string

Conversation

@snotty

@snotty snotty commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Closes #100269

What Problem This Solves

Fixes an issue where agent-initiated and scheduled (cron) Telegram messages would silently fail to deliver when the driving model filled the optional replyTo/threadId (or replyToMessageId/messageThreadId) fields with an empty-string default. The user never receives the message — most painfully, time-sensitive cron reminders that run, draft correctly, then fail only at the delivery step.

Modern tool-calling models routinely emit every optional schema field with an empty ("") or zero ("0") default even when the caller isn't setting it. For those Telegram routing hints the empty default caused ToolInputError: replyTo must be a positive integer. (and the threadId variant); the agent then retried with more invalid defaults and gave up. This reproduced identically across two independent providers (an MAI Flash-family model and GitHub Copilot gpt-5-mini), confirming it is model-agnostic and rooted in the shared parameter reader rather than any one model.

Why This Change Was Made

readPositiveIntegerParam / readNonNegativeIntegerParam in src/agents/tools/common.ts threw whenever the parsed value was undefined but the raw param was not null. An empty/whitespace-only string parses to undefined yet is != null, so it threw — even though an empty string is semantically "not provided". These readers now treat a blank string as unset (return undefined) while still rejecting genuinely invalid present values (numeric 0, "42.5", "-3", over-max). This mirrors how readStringParam/readNumberParam already ignore empty/whitespace strings, and fixes every channel that reads routing params through these helpers (Telegram, plus the same pattern in Discord/Slack/Matrix and the core message-action-runner) in a single place. The existing "timeoutMs: 0 throws" contract is preserved.

User Impact

Agent- and cron-initiated messages (Telegram especially) now deliver reliably regardless of which model drives the message tool. Empty optional routing-param defaults are ignored instead of aborting the send, so scheduled reminders and notifications stop silently disappearing. No API or behavior change for callers that pass real values; invalid present values are still rejected exactly as before.

Evidence

Targeted unit run of the touched test file (19 pre-existing + 2 new cases), all green:

$ npx vitest run src/agents/tools/common.params.test.ts
 ✓  unit-fast  src/agents/tools/common.params.test.ts (21 tests) 9ms
 Test Files  1 passed (1)
      Tests  21 passed (21)

New tests assert both the fix and the preserved strictness:

expect(readPositiveIntegerParam({ replyTo: "" }, "replyTo")).toBeUndefined();
expect(readPositiveIntegerParam({ threadId: "   " }, "threadId")).toBeUndefined();
expect(() => readPositiveIntegerParam({ replyTo: "0" }, "replyTo")).toThrow("replyTo must be a positive integer");
expect(() => readPositiveIntegerParam({ replyTo: 0 }, "replyTo")).toThrow("replyTo must be a positive integer");
expect(() => readPositiveIntegerParam({ replyTo: "-3" }, "replyTo")).toThrow("replyTo must be a positive integer");

Real-world before/after (Telegram cron send via the message tool, model emitting replyTo:"" / threadId:""):

  • Before: ToolInputError: replyTo must be a positive integer. ×N, delivery fails, no message received.
  • After (with this change applied): { "ok": true, "messageId": "…" }, message delivered, zero ToolInputError.

Full pnpm build && pnpm check && pnpm test was not run locally (the monorepo-wide typecheck exhausted memory on my constrained ARM/WSL box); I relied on the scoped vitest lane above and am leaving CI to validate the wider suite. Happy to adjust if a maintainer wants a different validation lane.

Optional positive-integer tool params (e.g. Telegram replyTo/threadId)
threw ToolInputError when a tool-calling model populated them with an
empty-string or whitespace-only default. Those defaults carry no value,
so readPositiveIntegerParam/readNonNegativeIntegerParam now treat a
blank string as unset (undefined) instead of throwing, while still
rejecting genuinely invalid present values (0, "42.5", "-3"). This
prevents silent message-delivery failures when models emit empty
routing-param defaults. Adds unit tests covering blank vs invalid.
@clawsweeper

clawsweeper Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 5, 2026, 8:41 AM ET / 12:41 UTC.

Summary
The PR changes shared positive/non-negative integer tool parameter readers to treat blank strings as unset and adds focused regression tests.

PR surface: Source +19, Tests +31. Total +50 across 2 files.

Reproducibility: yes. Current main ignores blank strings in readNumberParam but the integer wrappers still throw because the raw value is non-null, and Telegram routes reply/thread params through those wrappers; Mantis also captured the before/after Telegram path.

Review metrics: 1 noteworthy metric.

  • Exported reader behavior changed: 2 helpers changed. readPositiveIntegerParam and readNonNegativeIntegerParam are exported through plugin SDK subpaths, so maintainers should notice the contract change before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #100269
Summary: This PR is the linked implementation candidate for the open source-repro issue about blank optional Telegram routing params causing message delivery failure.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster ✨ media proof bonus
Patch quality: 🐚 platinum hermit
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:

  • Have a maintainer accept or narrow the exported integer-reader contract change.
  • [P2] Rerun or resolve the failing checks-fast-bundled-protocol check before merge.

Risk before merge

  • [P1] The PR relaxes exported plugin-SDK integer-reader behavior, so existing plugin-style callers that relied on blank strings throwing will instead see them as absent values.
  • The current PR head still has a failing checks-fast-bundled-protocol CI check; merge should wait for a rerun or a concrete explanation/fix.

Maintainer options:

  1. Accept the shared helper contract (recommended)
    Merge after maintainer acceptance that blank optional integer strings are absent values for exported SDK-style readers, with the added tests as the regression contract.
  2. Narrow before merge
    Move blank-string normalization into Telegram/message-routing readers if maintainers do not want exported integer-reader behavior relaxed globally.

Next step before merge

  • [P2] This active PR needs maintainer acceptance of the exported helper contract and current CI cleanup rather than a separate automated repair branch.

Maintainer decision needed

  • Question: Should blank strings be treated as absent by the exported shared integer parameter readers, or should this fix be scoped to Telegram/channel routing call sites only?
  • Rationale: The shared helper change is the cleanest owner-boundary fix for the bug, but it changes exported plugin-SDK behavior beyond Telegram.
  • Likely owner: steipete — Current blame and recent plugin SDK history point to Peter as the best available owner for accepting or narrowing the exported helper contract.
  • Options:
    • Accept shared-reader relaxation (recommended): Treat blank strings as unset for optional integer readers across SDK-style callers, matching existing blank handling in readNumberParam and avoiding per-channel sanitizers.
    • Scope the fix to routing call sites: Keep exported integer readers strict and normalize blank reply/thread ids only in Telegram or message-routing paths, reducing compatibility surface at the cost of duplicated policy.

Security
Cleared: The diff only changes parameter parsing and focused tests; I found no concrete security or supply-chain concern.

Review details

Best possible solution:

Land the shared-reader fix after maintainers accept blank strings as unset for optional integer helpers and exact-head CI is green.

Do we have a high-confidence way to reproduce the issue?

Yes. Current main ignores blank strings in readNumberParam but the integer wrappers still throw because the raw value is non-null, and Telegram routes reply/thread params through those wrappers; Mantis also captured the before/after Telegram path.

Is this the best way to solve the issue?

Yes, if maintainers accept the SDK behavior change. The shared helper is the narrowest owner-boundary fix; channel-local sanitizers are the safer compatibility alternative but would duplicate the invariant.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against deac98eb7204.

Label changes

Label changes:

  • add proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. The PR body includes redacted before/after live output, and Mantis posted Telegram Desktop before/after artifacts showing the candidate delivery path.

Label justifications:

  • P1: The linked bug can drop scheduled or agent-initiated Telegram/channel messages before users receive them.
  • merge-risk: 🚨 compatibility: The PR relaxes behavior in exported plugin-SDK integer readers, which can affect plugin callers that currently rely on blank strings being rejected.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (screenshot): The PR body includes redacted before/after live output, and Mantis posted Telegram Desktop before/after artifacts showing the candidate delivery path.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes redacted before/after live output, and Mantis posted Telegram Desktop before/after artifacts showing the candidate delivery path.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. The PR body includes redacted before/after live output, and Mantis posted Telegram Desktop before/after artifacts showing the candidate delivery path.
  • mantis: telegram-visible-proof: Mantis should capture Telegram visible proof. The PR changes visible Telegram delivery behavior for blank reply/thread routing params, and the existing Mantis proof is the right label fit.
Evidence reviewed

PR surface:

Source +19, Tests +31. Total +50 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 23 4 +19
Tests 1 31 0 +31
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 54 4 +50

What I checked:

  • Current main rejects blank optional integer params: On current main, readNumberParam ignores blank strings, then readPositiveIntegerParam/readNonNegativeIntegerParam throw because the raw param is non-null. (src/agents/tools/common.ts:247, deac98eb7204)
  • PR changes only the wrapper error path: The PR adds isBlankParamValue and only suppresses the wrapper error when the raw value is a blank string; nonblank invalid values still throw. (src/agents/tools/common.ts:260, 86f91748a868)
  • Regression tests cover blank versus invalid values: The PR adds cases for blank/whitespace positive and non-negative integer params while preserving rejection of zero, negative, fractional, and over-max values where applicable. (src/agents/tools/common.params.test.ts:135, 86f91748a868)
  • Telegram uses the affected reader: Telegram reply/thread action parsing calls readPositiveIntegerParam for messageThreadId/threadId and replyToMessageId/replyTo, so the current-main wrapper can abort delivery before send. (extensions/telegram/src/action-runtime.ts:130, deac98eb7204)
  • Reader is exported SDK surface: The changed helpers are exported through plugin-sdk channel action and parameter-reader subpaths, so the fix is broader than Telegram-only runtime behavior. (src/plugin-sdk/param-readers.ts:4, deac98eb7204)
  • Real Telegram proof is present: Mantis posted before/after Telegram Desktop artifacts for this PR; the inspected candidate screenshot shows the blank-route delivery proof message after the candidate run, and the PR body includes redacted before/after live output. (86f91748a868)

Likely related people:

  • steipete: PR 100223 is the current blame source for the shared reader and Telegram action-runtime lines, and Peter also has recent plugin SDK surface history. (role: recent area contributor and merger; confidence: high; commits: 4deb63c9791c, f2d7a825b122; files: src/agents/tools/common.ts, extensions/telegram/src/action-runtime.ts, src/plugin-sdk/param-readers.ts)
  • gumadeiras: Git history shows Gustavo carried major message-action discovery/plugin SDK work and earlier Telegram action-runtime movement around the same owner boundary. (role: adjacent message-action and plugin SDK contributor; confidence: medium; commits: a32c7e16d204, 951f3f992b68, c3386d34d290; files: src/infra/outbound/message-action-runner.ts, extensions/telegram/src/action-runtime.ts, src/plugin-sdk/channel-actions.ts)
  • vincentkoc: CONTRIBUTING lists Vincent for Agents, and recent history includes a release commit touching the same shared reader and Telegram action-runtime files. (role: adjacent agents owner; confidence: medium; commits: e085fa1a3ffd; files: src/agents/tools/common.ts, extensions/telegram/src/action-runtime.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.
Review history (1 earlier review cycle)
  • reviewed 2026-07-05T10:30:42.590Z sha 86f9174 :: needs maintainer review before merge. :: none

@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. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. labels Jul 5, 2026
@clawsweeper
clawsweeper Bot temporarily deployed to qa-live-shared July 5, 2026 10:33 Inactive
@openclaw-mantis

Copy link
Copy Markdown
Contributor

Mantis Telegram Desktop Proof

Summary: Mantis captured native Telegram Desktop before/after GIFs showing the blank routing-field send missing on Main and delivered in this PR.

Main screenshot This PR screenshot
Baseline native Telegram Desktop screenshot Candidate native Telegram Desktop screenshot
Main This PR
Baseline native Telegram Desktop proof GIF Candidate native Telegram Desktop proof GIF

Motion-trimmed clips:

Raw QA files: https://artifacts.openclaw.ai/mantis/telegram-desktop/pr-100273/run-28737816659-1/index.json

@steipete

steipete commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Thank you — this fix landed through maintainer batch PR #100399 in commit b22c36f, with contributor credit and changelog thanks preserved. Closing this source PR as superseded.

@steipete steipete closed this Jul 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling mantis: telegram-visible-proof Mantis should capture Telegram visible proof. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P1 High-priority user-facing bug, regression, or broken workflow. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: optional Telegram replyTo/threadId tool params throw on empty-string defaults, causing silent message delivery failure

2 participants