fix(cli): reject non-numeric --timeout-seconds instead of passing NaN downstream - #73
Conversation
… downstream
Number("30s") is NaN, and the two HTTP paths disagree about what to do with it.
http.js:17 writes Number(timeoutSeconds || 15), so NaN is falsy and it falls back
to 15 -- safe by accident. mcp-client.js:145 wrote config.timeoutSeconds * 1000
with no guard, and Math.max(NaN, 1000) stays NaN. Node does not throw on
setTimeout(fn, NaN); it warns and substitutes 1ms, so requestJsonRpc aborted its
own AbortController before the request left and surfaced a timeout error.
The asymmetry is what makes it expensive to diagnose: broker and auth calls go
through http.js and keep working, so `calle auth status` is fine while every MCP
call dies instantly and blames the network.
--timeout-seconds config mcp-client:145 http.js:17
"30" 30 30000 30000
"30s" NaN NaN 15000
(unset) 15 15000 15000
Validate at the edge in resolveRuntimeConfig, naming the flag, and apply it to
all four second-valued options, which share the same Number(x || DEFAULT) shape.
Falsy values still fall back to the default, so existing behaviour is unchanged.
Also give mcp-client.js:145 the same `|| default` guard the rest of the codebase
already uses -- it is the only one of four that was missing it:
http.js:17 Number(timeoutSeconds || 15) guarded
telemetry.js:123 Number(config.telemetryTimeoutSeconds || 1.5) guarded
broker-client.js:157 Number(config.pollTimeoutSeconds || 300) guarded
mcp-client.js:145 config.timeoutSeconds * 1000 <- was not
Ray-56
left a comment
There was a problem hiding this comment.
The NaN diagnosis is valid and the local CLI/Core checks and tests pass, but the shared validator introduces two correctness gaps and the release metadata is missing.
[P1] Preserve the existing --min-ttl-seconds 0 behavior. Current main resolves { minTtlSeconds: "0" } to numeric zero, which is useful for disabling the minimum remaining-lifetime window. This PR rejects it because all four settings share a strictly-positive validator. Please make the constraints option-specific: allow a non-negative value for min TTL while keeping timer/request durations positive, and add a regression test for zero.
[P2] Bound timer-backed values as well as checking that they are finite. Node sets any setTimeout delay greater than 2,147,483,647 ms to 1 ms, so a value such as --timeout-seconds 2147484 passes this validator and recreates the same immediate-abort failure class that the PR is intended to prevent. Add a maximum for timeout-backed settings or safely clamp/reject them, with a boundary test. Node documents this behavior here: https://nodejs.org/api/timers.html#settimeoutcallback-delay-args
[P2] Add patch changesets for both @call-e/cli and @call-e/core. Both published packages change behavior in this PR. Verify with pnpm run check:versions and both package dry-runs.
Addresses the review on CALLE-AI#73. [P1] --min-ttl-seconds 0 works again. Zero disables the minimum remaining lifetime window, and the shared strictly-positive validator had taken that away. The bounds are now per option, and a numeric 0 is no longer swallowed by the falsy check on the incoming value. [P2] Timer-backed durations are capped at 2147483 seconds. setTimeout holds its delay in a signed 32-bit int and silently replaces anything larger with 1ms, so --timeout-seconds 2147484 passed the validator and reproduced the immediate abort it was added to prevent. --min-ttl-seconds never reaches a timer, so it is not capped. [P2] Patch changesets added for @call-e/cli and @call-e/core. Reverting to the shared validator fails 3 of the 39 CLI tests.
|
All three addressed. [P1] The bounds are per option now, so [P2] Good catch on the 32-bit delay. Timer-backed durations are capped at 2147483 seconds, with a boundary test at the limit and one past it. [P2] Patch changesets added for Reverting to the shared validator fails 3 of the 39 CLI tests, so the new ones do exercise the change. Core is 12/12 and One thing I could not verify: |
Ray-56
left a comment
There was a problem hiding this comment.
The current head resolves all previous findings: @call-e/cli preserves both string and numeric min-TTL zero, timer-backed values are bounded below Node's maximum delay, and patch changesets are present for both @call-e/cli and @call-e/core. GitHub CI is green, the branch remains clean against the latest main, and local CLI/Core checks, tests, version validation, and package dry-runs all pass. I did not find another merge blocker.
What
--timeout-seconds 30smakes every MCP call fail instantly and report a timeout, while auth and broker calls keep working. This validates the flag at the edge and fixes the one unguarded timeout conversion.Why
Number("30s")isNaN, and the two HTTP paths disagree about what to do with it:packages/core/lib/http.js:17—Math.max(Math.ceil(Number(timeoutSeconds || 15) * 1000), 1000).NaNis falsy, so it falls back to 15. Safe by accident.packages/core/lib/mcp-client.js:145—Math.max(Math.ceil(config.timeoutSeconds * 1000), 1000).Math.ceil(NaN)isNaN,Math.max(NaN, 1000)isNaN. No fallback.Node does not throw on
setTimeout(fn, NaN)— it warns and substitutes 1 ms:So
requestJsonRpcaborts its ownAbortControllerbefore the request leaves, and the user sees a timeout error.Run through
resolveRuntimeConfigas it is on main:--timeout-seconds"30""30s""1m"The asymmetry is what makes this expensive to diagnose. Broker and auth calls go through
http.jsand keep working, socalle auth statusis fine. Every MCP call dies instantly and reports a timeout, which reads as "the MCP server is down" — so you look at the server, the network and the token, everywhere except a typo in your own flag.How
Validate at the edge. A
secondsOption(value, fallback, flag)helper inconfig.js, applied to the four second-valued options that share theNumber(x || DEFAULT)shape (timeoutSeconds,pollTimeoutSeconds,minTtlSeconds,telemetryTimeoutSeconds). It keeps the||fallback exactly as before, so falsy values still take the default and nothing that works today changes:Guard the one line that drifted.
mcp-client.js:145now uses the same|| defaultform as its three siblings. This is not a new convention — it is the only one of four that was missing it:http.js:17Number(timeoutSeconds || 15)— guardedtelemetry.js:123Number(config.telemetryTimeoutSeconds || 1.5)— guardedbroker-client.js:157Number(config.pollTimeoutSeconds || 300)— guardedmcp-client.js:145config.timeoutSeconds * 1000— was notTests
Two added to
packages/cli/test/cli.test.js: one asserting each of the four flags is rejected with its own name in the message, one asserting valid values and defaults are unaffected.Note for reviewers running this on Windows:
packages/codex-pluginandpackages/cursor-pluginfail their frontmatter checks, andpnpm run checkfails with them. I verified those are pre-existing by stashing this branch and re-running against unmodifiedmain— same failures. They are the CRLF issue that #70 addresses, and are unrelated to this change.