Skip to content

Fix/remote supervision hardening - #603

Open
edgarsskore wants to merge 3 commits into
fix/remote-local-mcp-supervisionfrom
fix/remote-supervision-hardening
Open

Fix/remote supervision hardening#603
edgarsskore wants to merge 3 commits into
fix/remote-local-mcp-supervisionfrom
fix/remote-supervision-hardening

Conversation

@edgarsskore

Copy link
Copy Markdown
Collaborator

What

Hardening on top of #598 (which correctly fixes both bugs — verified). Three things found while verifying it:

1. Regression: in-flight calls hung 60s on child death (882f0b0)
transport.onclose was assigned after client.connect(), replacing the SDK's handler that fails in-flight calls fast. Measured 60.0s → fixed to ~1s by moving the assignment above connect().

2. Restart policy (4488b2b)
Restarts were instant-forever (crash-looping child = ~0.35s cycles = ~5–6 mcp_devices PATCHes/sec, the 2026-07-18 incident pattern) or once-never (one failed restart = offline until manually restarted). Now: exponential backoff 2s → 5min ceiling, jittered, never gives up, resets after 60s stable uptime.

Since recovery windows are now minutes, added DeviceStatusArbiter: single status writer, online iff channel AND child healthy, transition-only writes — a channel resubscribe can no longer mark a dead-child device online.

edgarsskore and others added 3 commits July 21, 2026 15:33
…-flight calls fail fast

The SDK chains transport onclose/onerror handlers that exist BEFORE
client.connect() (its wrapper calls ours, then its own close handling,
which rejects in-flight requests with "Connection closed" immediately).
Assigned after connect() they REPLACED the SDK's handler, so a tool call
in flight when the local child died hung for the full 60s request
timeout instead of failing in ~0.1s as it did before supervision was
added. Measured: 60.0s "Request timed out" -> 1.0s "Connection closed".

Also guard callClientTool against the child dying between ensureReady()
resolving and callTool() dispatching: surface a descriptive Error
instead of a TypeError on a nulled client.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…from both health signals

handleLocalMcpLoss restarted the child instantly and gave up after one
failed attempt. A crash-looping child therefore cycled every ~0.35s,
PATCHing mcp_devices twice per cycle (~5-6 writes/sec per device,
indefinitely) — the same write pattern as the 2026-07-18 prod DB
incident — while a single transient restart failure (files mid-upgrade,
resource exhaustion) left the device offline until a human restarted
the process, since offline devices receive no calls to trigger the
lazy restart.

Recovery is now a serialized retry loop with exponential backoff:
2s base doubling to a 5min ceiling, 15% jitter so fleet-wide triggers
don't synchronize, never gives up, telemetry per failed attempt, and
the ladder resets after 60s of stable child uptime so a fresh one-off
crash still recovers in ~2-3s. Test knobs:
DC_LOCAL_RESTART_BACKOFF_BASE_MS / DC_LOCAL_RESTART_STABLE_UPTIME_MS.

Because backoff widens dead-child windows from ~0.35s to minutes, the
channel's unconditional 'online' write on every SUBSCRIBED became a
real hazard: a routine resubscribe during recovery would advertise a
device whose child is dead. New DeviceStatusArbiter is the single
mcp_devices status writer: online iff BOTH channel and child are
healthy, transition-only writes (a reconnect storm re-reporting the
same state produces zero extra PATCHes), sync() after registration.
RemoteChannel reports health through it, with a legacy direct-write
fallback when unwired.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five cases in test-remote-device-supervision.js, each verified failing
on the unfixed #598 head before the fixes landed (red -> green):
  - in-flight call fails fast on child death (was 60.0s, now ~1s)
  - restart backoff escalates (was constant ~330ms recovery cadence)
  - restart retries through transient failures (was give-up-after-one)
  - callClientTool surfaces a descriptive error, not a TypeError
  - status arbiter: channel-up + child-dead must not write 'online',
    transition-only writes
Real MCPDevice/DesktopCommanderIntegration with only setOnlineStatus
stubbed; optional case-name filter arg for single-case runs.

Also rework the "bogus executable" case in test-spawn-error-no-crash.js:
a falsy shell argument is coerced to the configured default shell, so
the case ran through a shell and could never produce a spawn 'error'
event — it now honestly asserts the exit-127 command-not-found path
(spawn-error coverage remains with the bogus-shell case).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69a24814-b8e0-471f-b8a8-4b31cc997211

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/remote-supervision-hardening

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.

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.

1 participant