Skip to content

fix(#611): make the send_to contract true, and stop the errors hiding the fix (S) - #614

Merged
EtanHey merged 2 commits into
mainfrom
fix/611-send-to-contract
Sep 9, 2026
Merged

fix(#611): make the send_to contract true, and stop the errors hiding the fix (S)#614
EtanHey merged 2 commits into
mainfrom
fix/611-send-to-contract

Conversation

@EtanHey

@EtanHey EtanHey commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Make the send_to contract true

size:S. Closes #611.

Etan sent a screenshot of three consecutive send_to calls failing, with the agent's own line under them:

"The coordination tool rejected its documented default mode; I'll retry with an explicit mode."

That sentence is the bug. The schema declared mode .optional() (server.ts:726) while the runtime threw when it was omitted (:17590, :17612). A correct reading of the published contract produced a failing call, so every agent rediscovered it by trial and error at 2–3 wasted turns each. It was never a regression — it was always wrong.

What changed

mode now has a real .default("agent") that the runtime honours. Agent mode is what essentially every caller means. Verified off the wire against the built server: mode default: "agent".

The remaining guards carry the copy-pasteable example. Previously a bogus mode got Example: send_to({ mode: "agent", agent_id: "...", text: "hello" }) from the enum errorMap, and an omitted mode got a bare string. That is backwards — omission is the case agents actually hit. The constant already existed at :723; the throws just did not use it.

read_screen accepts surface_id as an alias for surface — because our own output taught that name. spawn_agent's output schema (:860) and every list_agents row (:4339) emit surface_id, so the natural workflow (list agents, read the surface it named) hands the key straight back and got a validation error. The value was always right; only the name was, and the tool that taught the wrong name was ours. This is an alias for that reason — not backwards compatibility.

No message alias. git log -S'message: z.string()' -- src/server.ts returns nothing; that field never existed, so an alias would invent an API that was never real. The improved error names text instead.

Description bloat

Etan raised this twice. Measured, rather than guessed — the whole tool surface, off the wire:

before after
total 31,674 B (~7,919 tok) 30,993 B (~7,748 tok)
send_to 6,704 B 6,552 B
spawn_agent 10,942 B 10,329 B

−2.2%. That is a start, not a win, and I am not dressing it up. The measurement produced a more useful finding than the trim: spawn_agent (~2,736 tok) is 63% BIGGER than send_to (~1,676), and the bulk is in PARAMETER descriptions, not the tool description. Etan named send_to twice; the bigger cost is next door. That baseline is now recorded for the real lane.

Trimmed here: allow_busy, which spent 301 characters describing a deprecated no-op (twice — the same prose was duplicated on send_to_agent), and report_path.

The irony worth naming: the description is enormous and still failed to convey the one field a caller had to supply. Length is not clarity; here it bought neither.

Tests

New tests go through a real MCP client, because the defect was in the served contract rather than an internal helper.

Calibrated against origin/main: both new tests FAIL there and pass here, while the 8 surrounding tests pass either way — so the new arms are the only thing moving.

Four existing tests encoded the old contract and were updated, not deleted — including one literally named "requires mode", which asserted the refusal that was the bug.

One of them caught me over-trimming. The coordination-footer test pinned that report_path documents every coordination_footer_delivered: false outcome. My first cut (1023 → 412 chars) dropped branches callers need. I restored the substance (→ 838) and left that guard's regex untouched. It did exactly its job, on me.

Full suite: 159 files, 3799 passed, 1 skipped.

— cmuxlayerClaude-70bfff64 (lead) · claude/claude-opus-5[1m]


Note

Medium Risk
Changes default parsing and validation for widely used coordination tools (send_to, read_screen); behavior is intentional but affects every agent caller omitting mode or passing surface_id.

Overview
Fixes #611 by aligning the MCP tool contract with runtime behavior so agents stop failing on “correct” calls.

send_to: mode is now .default("agent") instead of optional while the handler still required it. Omitted mode no longer throws before parse; invalid modes and wrong payload keys still error with the shared SEND_TO_WORKING_EXAMPLE. allow_busy descriptions on send_to / send_to_agent are shortened but still note picker/permission safety gates.

read_screen: Accepts surface_id as an alias for surface (matching list_agents / spawn_agent output), resolves one ref, and errors clearly if neither is provided.

spawn_agent: Trims report_path parameter docs without dropping coordination-footer guidance.

Tests: Real MCP client coverage for omitted-mode delivery and published schema default; prior tests that encoded the bogus “mode required” refusal are inverted or relaxed.

Reviewed by Cursor Bugbot for commit aa58772. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Default send_to mode to agent in SendToArgsSchema and accept surface_id in read_screen

  • Changes SendToArgsSchema to set mode default to agent, fixing a mismatch between the published contract and runtime behavior; omitted mode now reaches the agent path instead of being rejected.
  • Updates read_screen to accept surface_id (emitted by list_agents and spawn_agent) as an alias for surface, resolving both into one reference and erroring when neither is provided.
  • Adds a copy-pasteable send_to usage example to errors caused by legacy payload parameter names, and shortens allow_busy descriptions to mark the option as a deprecated no-op while noting picker/menu/permission safety gates remain active.
  • Updates tests across enter-reliability.test.ts, server.test.ts, and thin-core-tools.test.ts to verify omitted-mode acceptance and updated documentation concepts.
  • Behavioral Change: callers that previously received a missing-mode error for omitted mode now get agent-mode handling; read_screen callers must provide either surface or surface_id or receive an explicit target error.
📊 Macroscope summarized aa58772. 1 file reviewed, 1 issue evaluated, 0 issues filtered, 1 comment posted

🗂️ Filtered Issues

EtanHey and others added 2 commits September 8, 2026 18:35
… the fix

The schema declared mode .optional() while the runtime threw when it was
omitted, so a correct reading of the published contract produced a
failing call. Agents rediscovered this by trial and error at 2-3 wasted
turns each; one said out loud that 'the coordination tool rejected its
documented default mode'. It was never a regression -- it was always
wrong.

- mode now has a real default of "agent" that the runtime honours, so
  the contract and the behaviour agree. agent mode is what essentially
  every caller means.
- the remaining guards carry the copy-pasteable example the enum
  errorMap already produced. Previously a BOGUS mode got an example and
  an OMITTED mode got a bare string -- backwards, since omission is the
  case agents actually hit.
- read_screen accepts surface_id as an alias for surface, because OUR
  OWN output taught that name: spawn_agent's output schema and every
  list_agents row emit surface_id, so the natural workflow hands it
  straight back. Not compatibility -- we taught it.
- no message alias: that field never existed.
- trimmed the two worst parameter descriptions (allow_busy described a
  deprecated no-op in 301 chars; report_path ran to 1023).

Tests go through a real MCP client because the defect was in the served
contract. Calibrated against origin/main: both new tests FAIL there and
pass here, while the 8 surrounding tests pass either way.

Tool surface: 31,674 -> 30,993 bytes (~7,919 -> ~7,748 tokens/turn).

Closes #611.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four tests failed, all of them asserting the behaviour the fix removes:

- 'returns the exact missing-mode error' asserted the bare refusal that
  IS the bug. Inverted: omission must now be accepted.
- 'requires mode ...' was named after the contract lie. Renamed and
  inverted.
- the allow_busy doc test pinned 300+ chars of prose per site, twice. It
  now guards the two things that matter -- still marked deprecated, and
  the safety gates it does NOT bypass are named -- not the wording.
- the coordination-footer test CAUGHT ME OVER-TRIMMING. My report_path
  cut dropped the documented coordination_footer_delivered=false
  branches, which callers need. Restored that substance (1023 -> 838,
  not 412) and left the guard's regex untouched.

Also trimmed the second allow_busy site, which was the same 200+ chars
of no-op prose duplicated on send_to_agent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@EtanHey EtanHey added the size:S Tight-loop PR size: 21-100 hand-written lines changed label Sep 9, 2026
@cursor

cursor Bot commented Sep 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_7b28ce96-738b-4a2d-818e-2b3ba2972bf2)

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 56 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 11e5bfb5-7eb9-4a38-8078-d96c7c4bc0d9

📥 Commits

Reviewing files that changed from the base of the PR and between a68510f and aa58772.

📒 Files selected for processing (4)
  • src/server.ts
  • tests/enter-reliability.test.ts
  • tests/server.test.ts
  • tests/thin-core-tools.test.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/server.ts
),
allow_busy: SendToArgsSchema.shape.allow_busy.describe(
"Deprecated no-op retained for compatibility: send_to always attempts immediate delivery. Input landing behind an active turn is reported as queued_behind_turn, not a nonterminal queued state. Picker/menu and permission-prompt safety gates still refuse text; use mode=key for deliberate menu driving.",
"Deprecated no-op. Safety gates still refuse text at a picker/menu or permission prompt; use mode=key to drive those deliberately.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High src/server.ts:17606

allow_busy: true still changes submission verification by selecting BUSY_AGENT_SUBMIT_VERIFY_TIMEOUT_MS, so callers are told the flag is a no-op even though it changes timeout behavior and can produce a SubmitVerificationError. Remove the flag from the deliverAgentInput call or update this description to document its actual effect.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/server.ts around line 17606:

`allow_busy: true` still changes submission verification by selecting `BUSY_AGENT_SUBMIT_VERIFY_TIMEOUT_MS`, so callers are told the flag is a no-op even though it changes timeout behavior and can produce a `SubmitVerificationError`. Remove the flag from the `deliverAgentInput` call or update this description to document its actual effect.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Daemon performance budget: GREEN

Replay: 8 clients x 12 rounds. Runner regression ratio: 1.25x. Sampled rows use max(2 x (p95 - p50), 3 sigma of p50 after five green main runs); single-shot or untrusted-history rows retain +300 ms. Every row keeps the baseline x 1.25 floor and its sanity cap.

Operation Transport Sampling Margin rule Metric Baseline Current Ceiling Status
list_surfaces socket sampled measured (16 runs) p50_ms 9.47 ms 8.81 ms 92.41 ms PASS
list_surfaces socket sampled measured (16 runs) p95_ms 50.94 ms 29.85 ms 133.88 ms PASS
read_screen socket sampled measured (16 runs) p50_ms 13.9 ms 12.49 ms 33.6 ms PASS
read_screen socket sampled measured (16 runs) p95_ms 23.75 ms 22.96 ms 43.45 ms PASS
send_to_surface_warm socket sampled measured (16 runs) p50_ms 65.02 ms 65.58 ms 81.27 ms PASS
send_to_surface_warm socket sampled measured (16 runs) p95_ms 71.2 ms 71.81 ms 89 ms PASS
send_to_agent_warm socket sampled measured (16 runs) p50_ms 93.26 ms 90.97 ms 151.92 ms PASS
send_to_agent_warm socket sampled measured (16 runs) p95_ms 122.59 ms 107.8 ms 181.25 ms PASS
list_agents socket sampled measured (16 runs) p50_ms 67.66 ms 62.89 ms 171.2 ms PASS
list_agents socket sampled measured (16 runs) p95_ms 119.43 ms 105.05 ms 222.97 ms PASS
control_health socket sampled measured (16 runs) p50_ms 132.77 ms 129.99 ms 236.13 ms PASS
control_health socket sampled measured (16 runs) p95_ms 184.45 ms 148.92 ms 287.81 ms PASS
spawn_close_during_sweep socket sampled measured (16 runs) p50_ms 46.04 ms 42.33 ms 240.52 ms PASS
spawn_close_during_sweep socket sampled measured (16 runs) p95_ms 143.28 ms 60.25 ms 337.76 ms PASS
first_send_after_spawn socket sampled measured (16 runs) p50_ms 74.06 ms 70.95 ms 98.52 ms PASS
first_send_after_spawn socket sampled measured (16 runs) p95_ms 86.29 ms 86.65 ms 110.75 ms PASS
send_to_surface_10_parallel socket sampled · stress measured (16 runs) p50_ms 145.68 ms 149.25 ms 592.26 ms PASS
send_to_surface_10_parallel socket sampled · stress measured (16 runs) p95_ms 368.97 ms 168.14 ms 815.55 ms PASS
read_screen_10_parallel socket sampled · stress measured (16 runs) p50_ms 31.4 ms 27.92 ms 86.66 ms PASS
read_screen_10_parallel socket sampled · stress measured (16 runs) p95_ms 59.03 ms 54.62 ms 114.29 ms PASS
send_to_surface_warm socket sampled measured (16 runs) lock_hold_ms 178 ms 84 ms 222.5 ms PASS
send_to_agent_warm socket sampled measured (16 runs) lock_hold_ms 200 ms 67 ms 258.66 ms PASS
list_agents socket sampled measured (16 runs) lock_hold_ms 141.66617600002792 ms 122.21979900001315 ms 245.21 ms PASS
first_send_after_spawn socket sampled measured (16 runs) lock_hold_ms 120 ms 72 ms 150 ms PASS
send_to_surface_10_parallel socket sampled · stress measured (16 runs) lock_hold_ms 353 ms 116 ms 799.58 ms PASS
send_to_surface_warm socket sampled measured (16 runs) cli_send_ms 65.02 ms 65.58 ms 81.27 ms PASS

15 rows unchanged.

Full table
Operation Transport Sampling Margin rule Metric Baseline Current Ceiling Status
list_surfaces socket sampled measured (16 runs) p50_ms 9.47 ms 8.81 ms 92.41 ms PASS
list_surfaces socket sampled measured (16 runs) p95_ms 50.94 ms 29.85 ms 133.88 ms PASS
read_screen socket sampled measured (16 runs) p50_ms 13.9 ms 12.49 ms 33.6 ms PASS
read_screen socket sampled measured (16 runs) p95_ms 23.75 ms 22.96 ms 43.45 ms PASS
send_to_surface_warm socket sampled measured (16 runs) p50_ms 65.02 ms 65.58 ms 81.27 ms PASS
send_to_surface_warm socket sampled measured (16 runs) p95_ms 71.2 ms 71.81 ms 89 ms PASS
send_to_agent_warm socket sampled measured (16 runs) p50_ms 93.26 ms 90.97 ms 151.92 ms PASS
send_to_agent_warm socket sampled measured (16 runs) p95_ms 122.59 ms 107.8 ms 181.25 ms PASS
list_agents socket sampled measured (16 runs) p50_ms 67.66 ms 62.89 ms 171.2 ms PASS
list_agents socket sampled measured (16 runs) p95_ms 119.43 ms 105.05 ms 222.97 ms PASS
control_health socket sampled measured (16 runs) p50_ms 132.77 ms 129.99 ms 236.13 ms PASS
control_health socket sampled measured (16 runs) p95_ms 184.45 ms 148.92 ms 287.81 ms PASS
spawn_close_during_sweep socket sampled measured (16 runs) p50_ms 46.04 ms 42.33 ms 240.52 ms PASS
spawn_close_during_sweep socket sampled measured (16 runs) p95_ms 143.28 ms 60.25 ms 337.76 ms PASS
first_send_after_spawn socket sampled measured (16 runs) p50_ms 74.06 ms 70.95 ms 98.52 ms PASS
first_send_after_spawn socket sampled measured (16 runs) p95_ms 86.29 ms 86.65 ms 110.75 ms PASS
send_to_surface_10_parallel socket sampled · stress measured (16 runs) p50_ms 145.68 ms 149.25 ms 592.26 ms PASS
send_to_surface_10_parallel socket sampled · stress measured (16 runs) p95_ms 368.97 ms 168.14 ms 815.55 ms PASS
read_screen_10_parallel socket sampled · stress measured (16 runs) p50_ms 31.4 ms 27.92 ms 86.66 ms PASS
read_screen_10_parallel socket sampled · stress measured (16 runs) p95_ms 59.03 ms 54.62 ms 114.29 ms PASS
list_surfaces socket sampled measured (16 runs) lock_hold_ms 0 ms 0 ms 82.94 ms PASS
read_screen socket sampled measured (16 runs) lock_hold_ms 0 ms 0 ms 19.7 ms PASS
send_to_surface_warm socket sampled measured (16 runs) lock_hold_ms 178 ms 84 ms 222.5 ms PASS
send_to_agent_warm socket sampled measured (16 runs) lock_hold_ms 200 ms 67 ms 258.66 ms PASS
list_agents socket sampled measured (16 runs) lock_hold_ms 141.66617600002792 ms 122.21979900001315 ms 245.21 ms PASS
control_health socket sampled measured (16 runs) lock_hold_ms 0 ms 0 ms 103.36 ms PASS
spawn_close_during_sweep socket sampled measured (16 runs) lock_hold_ms 0 ms 0 ms 194.48 ms PASS
first_send_after_spawn socket sampled measured (16 runs) lock_hold_ms 120 ms 72 ms 150 ms PASS
send_to_surface_10_parallel socket sampled · stress measured (16 runs) lock_hold_ms 353 ms 116 ms 799.58 ms PASS
read_screen_10_parallel socket sampled · stress measured (16 runs) lock_hold_ms 0 ms 0 ms 55.26 ms PASS
send_to_surface_warm socket sampled measured (16 runs) cli_send_ms 65.02 ms 65.58 ms 81.27 ms PASS
list_surfaces socket sampled measured (16 runs) request_bytes 142 bytes 142 bytes 142 bytes PASS
read_screen socket sampled measured (16 runs) request_bytes 161 bytes 161 bytes 161 bytes PASS
send_to_surface_warm socket sampled measured (16 runs) request_bytes 214 bytes 214 bytes 214 bytes PASS
send_to_agent_warm socket sampled measured (16 runs) request_bytes 232 bytes 232 bytes 232 bytes PASS
list_agents socket sampled measured (16 runs) request_bytes 112 bytes 112 bytes 112 bytes PASS
control_health socket sampled measured (16 runs) request_bytes 97 bytes 97 bytes 97 bytes PASS
spawn_close_during_sweep socket sampled measured (16 runs) request_bytes 161 bytes 161 bytes 161 bytes PASS
first_send_after_spawn socket sampled measured (16 runs) request_bytes 231 bytes 231 bytes 231 bytes PASS
send_to_surface_10_parallel socket sampled · stress measured (16 runs) request_bytes 2510 bytes 2510 bytes 2510 bytes PASS
read_screen_10_parallel socket sampled · stress measured (16 runs) request_bytes 1930 bytes 1930 bytes 1930 bytes PASS

@EtanHey
EtanHey merged commit 22cd39e into main Sep 9, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S Tight-loop PR size: 21-100 hand-written lines changed

Projects

None yet

1 participant