Skip to content

fix(cli): give plan_call its own request timeout ceiling - #78

Closed
zkasuran wants to merge 4 commits into
CALLE-AI:mainfrom
zkasuran:fix/plan-call-request-timeout
Closed

fix(cli): give plan_call its own request timeout ceiling#78
zkasuran wants to merge 4 commits into
CALLE-AI:mainfrom
zkasuran:fix/plan-call-request-timeout

Conversation

@zkasuran

@zkasuran zkasuran commented Aug 4, 2026

Copy link
Copy Markdown

Summary

calle call plan fails with MCP request timed out for tools/call under the default configuration. plan_call regularly runs for about as long as the shared 15 second request ceiling allows, so a plan that is working normally can finish just after the ceiling every other request uses. The reporter recovered by rerunning with --timeout-seconds 120.

Planning now carries its own default ceiling of 120 seconds. The MCP session handshake and every other tool keep 15, so a genuinely hung request still fails fast instead of waiting two minutes.

Why not raise DEFAULT_TIMEOUT_SECONDS

Raising the shared default would fix planning by making every unresponsive request wait two minutes, the initialize handshake included, which is what fails when a server is unreachable. The two requests want different ceilings, so the slow one gets its own:

  • callMcpTool takes a per-call timeoutSeconds that covers the tools/call request only. openMcpSession keeps config.timeoutSeconds.
  • The CLI passes it for plan_call alone, in calle call plan, calle call run and calle mcp call plan_call.
  • DEFAULT_PLAN_TIMEOUT_SECONDS is 120 and sits beside the other CLI defaults.
  • An explicit --timeout-seconds is the ceiling for every request, planning included, so the flag still means what it says.
  • A per-call value is used only when it is finite, above zero and no greater than MAX_TIMER_SECONDS (2147483). Anything else falls back to the shared ceiling the session already computed, because setTimeout collapses a delay it cannot store into 1ms and would abort the request before it left. That bound now lives in @call-e/core/constants, so the --timeout-seconds validator and the transport share one number.

The timeout message now names the ceiling that ran out, since two are in play: MCP request timed out for tools/call after 120s. That covers the other half of the report, where the error gave no hint which value to change.

Documentation

  • docs/install/troubleshooting.md gains a section for the error string with the symptom, the cause and both ways to recover.
  • packages/cli/docs/cli-reference.md carries the planning default beside the shared one.
  • calle --help prints Default: 15, 120 for plan_call.

Tests

packages/core/test/tool-timeout.test.js covers the transport: a tool call carrying a longer ceiling than the handshake, a call without an override keeping the shared ceiling, tools/list unaffected, thirteen unusable overrides falling back to the shared ceiling, four readable ones used as given up to the timer maximum and a wall clock case proving an overflowing override waits the shared ceiling instead of aborting after 1ms. packages/cli/test/cli.test.js covers the wiring, that planning gets the longer default and that an explicit flag wins.

They fail without the change. Reverting only the source files while keeping the new tests:

packages/core  not ok 3 - an unusable per-call timeout falls back to the shared ceiling
                   timeoutSeconds "120s" must fall back to the shared ceiling
               not ok 5 - an overflowing per-call timeout waits the shared ceiling instead of aborting at once
                   timeoutSeconds "120s" must report the shared ceiling
               # tests 5  # pass 3  # fail 2

  Expected values to be strictly equal:
  + actual - expected
  + 'MCP request timed out for tools/call after NaNs'
  - 'MCP request timed out for tools/call after 15s'

packages/cli   not ok 1 - test/cli.test.js
               # tests 8  # pass 6  # fail 1

No CALL-E account or live call is involved. Every case drives an injected fetchImpl.

Verification

From the repository root on Node 22.22.2 with pnpm 10.18.3.

pnpm run check:versions   exit 0
pnpm check                exit 0
pnpm test                 exit 0   137 tests, 136 pass, 1 skipped   (130, 129, 1 on main)
pnpm pack:dry-run         exit 0   call-e-core-0.2.5.tgz, call-e-cli-0.3.9.tgz

The single skip is the pre-existing Windows-only case.

AI disclosure

AI assistance (Claude, Anthropic) was used in developing this change. It is submitted under my name and I can answer for it. Verified locally before submitting: the four commands above, the pre-fix run quoted under Tests, a reproduction of every rejected timeout value against an injected fetchImpl and a calle --help render of the new default line.

Closes CALLE-AI/awesome-phone-call-agents#79
Closes CALLE-AI/awesome-phone-call-agents#80

plan_call regularly runs for about as long as the shared 15 second request
timeout allows, so `calle call plan` failed with a timeout on a call that was
working normally. Planning requests now use 120 seconds while the MCP session
handshake and the other tools keep 15, so a genuinely hung request still fails
fast. An explicit --timeout-seconds wins for every request, planning included.

callMcpTool takes a per-call `timeoutSeconds` that covers the tools/call request
only. The timeout error also names the ceiling that ran out, since two are now
in play.

Closes CALLE-AI/awesome-phone-call-agents#79
The CLI reference listed only the shared 15 second default. The troubleshooting
guide did not connect `MCP request timed out for tools/call` to the request
timeout at all, so a user had to find the boundary alone. The reference now
carries the planning default beside the shared one. `calle --help` prints both.
The guide has a new section with the symptom, the cause and the two ways to
recover.

Closes CALLE-AI/awesome-phone-call-agents#80

@Ray-56 Ray-56 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful split between the session timeout and the plan_call timeout. The overall direction is sound, and local check/test/pack validation passes, but two P2 issues still block merge.

[P2] Validate the new public Core timeout override before arming setTimeout.

timeoutSeconds currently uses truthiness plus Number(), so truthy invalid or oversized values reach the timer. I reproduced:

  • "120s" -> MCP request timed out for tools/call after NaNs
  • Infinity -> Node resets the delay to 1 ms and reports after Infinitys
  • 2147484 -> Node resets the delay to 1 ms
  • Negative values clamp to one second instead of falling back

Require a finite value greater than zero and no greater than Math.floor(2_147_483_647 / 1000). When the per-call override is invalid, fall back to the already computed shared timeoutMs/config.timeoutSeconds rather than a hard-coded 15-second default. Add regression tests for a junk string, a negative value, Infinity, and one value past the maximum.

[P2] Fix the troubleshooting examples.

calle call plan --to-phone +15551234567 is incomplete because --goal is required. Add a harmless goal to both examples, for example --goal "Confirm the appointment", so the documented recovery command can actually run.

The patch changeset for @call-e/cli and @call-e/core is appropriate. After fixing these issues, please run:

  • pnpm run check:versions
  • pnpm check
  • pnpm test
  • pnpm pack:dry-run

GitHub CI has not executed for this first-time-contributor head (action_required, zero jobs), so it should be run before merge.

callMcpTool took its timeoutSeconds override on truthiness and handed
Number(seconds) straight to setTimeout, so a value that looked readable could
still collapse the timer. "120s" armed NaN, Infinity and 2147484 seconds
overflowed the signed 32 bit delay, all three abort after 1ms, and a negative
value clamped to the one second floor. The reported ceiling claimed otherwise.

A duration is used only when it is finite, above zero and no greater than
MAX_TIMER_SECONDS. Anything else falls back to the shared ceiling the session
already computed rather than a fixed default. MAX_TIMER_SECONDS moves to
@call-e/core/constants so the --timeout-seconds validator and the transport
share one bound.
calle call plan requires --goal, so both recovery commands in the timeout
section exited with "Missing required --goal" instead of running.
@zkasuran

zkasuran commented Aug 6, 2026

Copy link
Copy Markdown
Author

Both P2s are fixed on 0034797.

[P2] Validate the new public Core timeout override before arming setTimeout

callMcpTool no longer tests the override for truthiness. usableTimeoutMs in packages/core/lib/mcp-client.js accepts a duration only when it is finite, above zero and no greater than MAX_TIMER_SECONDS, which is Math.floor(2_147_483_647 / 1000) or 2147483. Anything else returns null and the request uses timeoutMs, the ceiling openMcpSession already computed from config.timeoutSeconds:

timeoutMs: usableTimeoutMs(timeoutSeconds) ?? timeoutMs,

So the per-call path never reaches for a hard-coded default. The shared session ceiling keeps its own DEFAULT_TIMEOUT_SECONDS fallback for an unreadable config.timeoutSeconds, which is unchanged from before.

The bound moved into @call-e/core/constants as MAX_TIMER_DELAY_MS and MAX_TIMER_SECONDS, so the --timeout-seconds validator from #73 and the transport share one number instead of declaring it twice. The flag still rejects an out of range value; the transport falls back.

I reproduced your four inputs first, against a fake fetchImpl that answers the handshake then leaves tools/call open. config.timeoutSeconds is 2 in that harness, so a fallback shows up as elapsed time rather than only as a string.

override before after
"120s" aborted after 16ms, MCP request timed out for tools/call after NaNs aborted after 2015ms, ... after 2s
-5 aborted after 1001ms, ... after 1s aborted after 2000ms, ... after 2s
Infinity aborted after 2ms, ... after Infinitys, TimeoutOverflowWarning aborted after 2001ms, ... after 2s, no warning
2147484 aborted after 2ms, ... after 2147484s, TimeoutOverflowWarning aborted after 2000ms, ... after 2s, no warning
2147483 no abort within 3500ms no abort within 3500ms
120 no abort within 3500ms no abort within 3500ms

The 16ms on the first row is process warm-up on the first request of the run. The armed delay was 1ms in all three overflow cases, which is what the two TimeoutOverflowWarnings say. The last two rows are controls: a value sitting on the maximum and the planning default still arm the long timer.

Regression tests, all in packages/core/test/tool-timeout.test.js:

  • an unusable per-call timeout falls back to the shared ceiling covers the junk string, the negative, Infinity and 2147484 (written as MAX_TIMEOUT_SECONDS + 1), plus "", NaN, -Infinity, -120, 0, Number.MAX_SAFE_INTEGER, null, undefined and {}. Each has to report the shared 15s ceiling.
  • a readable per-call timeout is used as given, up to the timer maximum pins the other side so the validator cannot swallow a good value: 2147483, 120, "45" and 0.25, the last of which takes the transport's one second floor.
  • an overflowing per-call timeout waits the shared ceiling instead of aborting at once measures elapsed time against a 1.5 second shared ceiling. A collapsed delay does not show up in the message, so this is the one case that waits for a real timer.

Reverting only the source files while keeping the new tests:

packages/core  ok 1 - a tool call can carry a longer ceiling than the session handshake
               ok 2 - a tool call without an override keeps the shared ceiling
               not ok 3 - an unusable per-call timeout falls back to the shared ceiling
                   timeoutSeconds "120s" must fall back to the shared ceiling
               ok 4 - a readable per-call timeout is used as given, up to the timer maximum
               not ok 5 - an overflowing per-call timeout waits the shared ceiling instead of aborting at once
                   timeoutSeconds "120s" must report the shared ceiling
               # tests 5  # pass 3  # fail 2

[P2] Fix the troubleshooting examples

Both examples in docs/install/troubleshooting.md now read calle call plan --to-phone +15551234567 --goal "Confirm the appointment", the second one with --timeout-seconds 180 after it.

I swept the whole repository for the incomplete shape with find | xargs grep rather than a gitignore-aware grep. Those two were the only calle call plan invocations missing --goal. packages/cli/docs/cli-reference.md, packages/cli/README.md, skills/calle/references/commands.md and the three plugin skill copies already carry it, so no synchronized copy needed an edit.

Then I ran both documented commands against an empty --cache-root, so nothing was dialled:

before   {"code":"invalid_arguments","message":"Missing required --goal"}                exit 2
after    {"code":"auth_required","message":"A usable CALL-E auth token is required."}    exit 1

auth_required is the step after argument parsing, so the documented command now parses and runs as far as the token check.

Gates on 0034797

Node 22.22.2, pnpm 10.18.3, from the repository root.

gate result
pnpm run check:versions exit 0, package metadata and install references in sync
pnpm check exit 0, 7 projects, syntax plus tsc --noEmit --strict
pnpm test exit 0, 137 tests, 136 pass, 1 skipped
pnpm pack:dry-run exit 0, call-e-core-0.2.5.tgz and call-e-cli-0.3.9.tgz

The skip is the pre-existing Windows-only case from #71. The count is 137 rather than 135 because of the two new core tests. The changeset now mentions the validation, since it changes published behaviour.

One gate outside your list and outside CONTRIBUTING: pnpm run check:examples fails here with OAuthClientProvider.__init__() got an unexpected keyword argument 'timeout' from the Python example client. It fails identically at the parent commit and this branch touches nothing under examples/, so it is a local dependency mismatch rather than something the PR moved. Flagging it so the four green gates above are not read as more than they are.

On CI: refs/pull/78/head still carries one github-actions check suite with conclusion action_required and zero check runs, the first-time-contributor gate. Nothing on a fork branch can trigger it, so it needs the workflow run approved on your side. I will not chase it further.

@zkasuran

zkasuran commented Aug 9, 2026

Copy link
Copy Markdown
Author

@Ray-56 nudge on this one. Both P2s from your review are fixed and pushed on 0034797; the full walkthrough is in my comment above.

  • Core override is validated before it arms setTimeout. usableTimeoutMs accepts a duration only when it is finite, above zero and no greater than MAX_TIMER_SECONDS (2147483). An unusable value falls back to the shared session ceiling the handshake already computed, not a hard-coded default. Tests cover the junk string, the negative, Infinity and one past the maximum, plus a wall-clock case proving the fallback arms the real timer instead of collapsing to 1ms.
  • Both docs/install/troubleshooting.md examples now carry the required --goal.

Re-ran the gates at 0034797 just now on Node 22.22.2 / pnpm 10.18.3: check:versions, check and pack:dry-run exit 0. pnpm test reports 137 tests, 136 pass, 1 skipped (the pre-existing Windows case).

Two things a fork can't do, so flagging them here: re-requesting your review and triggering the first-time-contributor CI (still action_required with zero jobs, it needs the workflow approved on your side). Thanks for the earlier pass.

@zkasuran

Copy link
Copy Markdown
Author

CALL-E 0.4.0 and core 0.3.0 landed the plan_call timeout and the recovery in e966ea7 and d5bf6c1, so this PR is redundant now. Closing it. Thanks for the review here.

One thing from your P2 carried over. The timeoutSeconds override that 0.3.0 added to callMcpTool reaches setTimeout without the MAX_TIMER_SECONDS cap, so a value above 2147483 seconds or a non-finite one still collapses to a 1ms abort. A config.timeoutSeconds from a direct core consumer hits the same arithmetic. I opened #83 with a small core-local clamp and tests for it.

@zkasuran zkasuran closed this Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Document how to recover from plan_call request timeouts calle call plan can time out under the 15-second default request timeout

2 participants