machine-bridge-mcp 3.0.0-beta.192 → 3.0.0-beta.195

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,12 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## 3.0.0-beta.192 - 2026-09-16
3
+ ## 3.0.0-beta.195 - 2026-09-18
4
4
 
5
- - Replace the shipped current-audit document's stale release chronology with version-agnostic security, continuity, privacy, and residual-limit invariants; repository hygiene now rejects numbered prerelease chronology and mutable acceptance/candidate authority claims.
6
- - Keep WebSocket daemon handshake diagnostics aligned with the resilient relay wrapper so Worker `server_info` preserves the same bounded per-outage HTTPS fallback takeover attribution as HTTPS descriptors and local diagnostics; private relay values remain excluded, and execution/replay semantics are unchanged.
7
- - Keep durable managed-job execution available across in-place Node package-manager upgrades by falling back from a removed daemon `process.execPath` only to the still-executable absolute Node launcher that originally started the daemon; diagnostics expose provenance/availability without local paths.
8
- - Make macOS sleep diagnosis retain actual timestamped `Sleep` records with a bounded 15-second power-log probe, avoiding the previous five-second timeout and broad filter that could hide evidence explaining relay suspension.
9
- - Add regressions for stale runtime launcher recovery and sleep-probe boundaries; relay authentication, replay, duplicate-side-effect prevention, and reconnect policy are unchanged.
5
+ - Update the fast-check development dependency from 4.9.0 to 4.10.0 and refresh its resolved lockfile entry.
6
+ - Preserve the beta.194 Wrangler/toolchain and relay changes while advancing release identity to beta.195 so this dependency update receives independent full verification, live candidate canary, and exact acceptance evidence.
10
7
 
11
8
  ## Historical releases
12
9
 
@@ -30,6 +30,6 @@
30
30
  "action": {
31
31
  "default_title": "Machine Bridge Browser"
32
32
  },
33
- "version_name": "3.0.0-beta.192",
33
+ "version_name": "3.0.0-beta.195",
34
34
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxryYkpZhq8+VAQLHcGS9BAHQcyKX8RHGIpIwvtIVRU/rcOcE0bNdnM0aZJ/h6xWQsGDHlhvjT2+1aJaAn/9k8473BRWajzVXld961CdHYVFVHoce2hHiSJ0xydWrHMMZhAm0mN0UzjEpgZ0tMw209efcZHIvSwuxhteZMRy4kyiVjwFlOf5oXFCxRuCJnPj3AK9CmCf4XgEBuPIJ0TZmjGHOOdBvJmbCNnAWXYEo5/mf7MfCGhV4IJ1hNuhpoNQfOFKMUcw9/v/IpT62XpfXdGYTfGYCmCjC+gntK1spbkr2P4/2+sYMQtLpse71mpSNGXfcf3abU55Vpn+gncSxRQIDAQAB"
35
35
  }
package/docs/AUDIT.md CHANGED
@@ -9,7 +9,7 @@ This file is a compact statement of current cross-cutting audit conclusions and
9
9
  - **State publication is atomic and identity-sensitive.** Security-sensitive state readers reject symlinks, unexpected hard links, malformed content, permission failures, and persistent identity changes. Bounded retries are reserved for explicitly classified atomic-publication races; durable state is complete before it becomes visible.
10
10
  - **Durable execution owns continuity explicitly.** Managed jobs persist recovery state independently of the hosted MCP response. Idempotency keys bind uncertain acceptance retries only while the retained record exists; cancellation, dependency state, cleanup, output budgets, and terminal persistence have explicit fail-closed boundaries. Callers recover the same accepted job instead of replaying its side effect.
11
11
  - **Daemon-owned Node children use a verified runtime launcher.** A live concrete runtime executable is preferred; if it disappears after a package-manager upgrade, only the original absolute Node launcher is eligible as fallback. Diagnostics expose provenance booleans rather than executable paths, and an unavailable launcher requires service restart instead of PATH guessing.
12
- - **Relay recovery separates transport from execution semantics.** WebSocket and signed HTTPS are transport paths, not permission or replay authorities. Ambiguous side effects are not transparently replayed. Same-daemon terminal-result retention is bounded, cancellation removes ownership, and repeated reconnects cannot accumulate execution time or delivery grace. Relay diagnostics project the same privacy-bounded fallback takeover history across WebSocket and HTTPS control paths.
12
+ - **Relay recovery separates transport from execution semantics.** WebSocket and signed HTTPS are transport paths, not permission or replay authorities. Their authenticated handshake/resume state is generation-scoped per transport, ended-generation control messages are discarded before reconciliation or interruption, and a protocol violation is routed only to its originating current transport. Ambiguous side effects are not transparently replayed. Same-daemon terminal-result retention is bounded, cancellation removes ownership, and repeated reconnects cannot accumulate execution time or delivery grace. Fatal protocol diagnostics retain only sanitized error/source/generation-class/stage/disposition fields, while fallback takeover history remains privacy-bounded across both control paths.
13
13
  - **Operating-system sleep is an observable suspension boundary, not a network guarantee.** On supported macOS hosts, diagnostics correlate bounded sleep history with runtime stalls and recovered relay outages. Machine Bridge cannot keep JavaScript or a socket executing while the machine is suspended, and sleep correlation does not identify unrelated upstream network causes.
14
14
  - **Browser automation remains existing-profile and snapshot-bound.** The extension controls an already installed Chromium profile; hosted calls bind explicit tab identity, and later Computer Use actions bind the exact observation snapshot. Mutation response loss, timeout, extension replacement, or ambiguous trusted-input settlement yields an unknown/non-retryable boundary that must be inspected before any retry.
15
15
  - **Desktop automation is permission- and evidence-bound.** Application actions use structured Accessibility operations or an explicitly enabled snapshot-bound visual backend. The runtime does not silently substitute arbitrary scripting or unverified pixel guessing when required authority/evidence is unavailable.
package/docs/LOGGING.md CHANGED
@@ -72,7 +72,7 @@ Brief network interruptions are expected on laptop network changes, Worker deplo
72
72
  - a separate periodic twenty-five-second application heartbeat refreshes Worker daemon activity and retains a seventy-five-second application-silence timeout; it begins only after end-to-end readiness, so authenticated probing cannot send a message type that the Worker probing state does not accept. Protocol-level Pong therefore cannot mask a Worker application path that has stopped replying. The Worker queues the heartbeat's JSON `pong` before Durable Object alarm inspection or mutation, then performs one explicit coalesced schedule, so storage latency is not allowed to sit ahead of application-liveness acknowledgement;
73
73
  - a late local transport-watchdog tick is classified as `runtime.event_loop.stall`, sends a fresh transport probe, and defers disconnect for a bounded recovery interval instead of being mislabeled as immediate remote failure; a macOS sleep/wake interval may legitimately produce this warning without a daemon fault. The heartbeat snapshot retains the lag of the last actual stall separately from the rolling maximum. Owner `diagnose_runtime` can compare that end time and duration with a bounded fixed `pmset` sleep-history projection and reports `matched_system_sleep` only when both dimensions agree within the fixed tolerance; an unmatched stall remains unclassified rather than being labeled synchronous JavaScript blockage by elimination. The same diagnostic also intersects the most recent completed relay disconnect interval with that bounded sleep history and reports only outage/overlap timing plus a fixed classification; `majority_system_sleep_overlap` means host suspension dominated the observed outage, so a retained `connection_reset` is aftermath evidence rather than sufficient independent-network evidence. An authorized, schema-valid remote tool call enters macOS activity protection only after policy/account/operation authorization and argument validation succeed; remote process sessions extend that ownership through child settlement. The default `activity` mode keeps the existing shared `/usr/bin/caffeinate -i -s -w <owner-pid>` assertion and fixed thirty-minute rolling inactivity grace. `ac-continuous` adds a daemon-lifetime `-s` assertion, while `continuous` holds `-i -s` for the daemon lifetime and does not arm inactivity grace. Unexpected assertion-child failure retains desired-state ownership and schedules bounded 1/5/30-second recovery; explicit release/shutdown cancels pending recovery. `runtime.idle_sleep_guard.unavailable` still records only a coarse `error_class`, never argv, paths, PID, tool name/content, session identity, or job identity; mode, generation, restart count, recovery-pending state, and bounded unprotected-duration telemetry are exposed through diagnostics rather than logs. Remote account managed-job runners own the same self-healing assertion primitive only after runner-claim confirmation and persisted account ownership validation; local managed jobs do not acquire this remote-continuity assertion. The remote runner fallback stderr diagnostic remains the fixed text `managed job idle-sleep assertion unavailable` plus a sanitized coarse `error_class`, without job name/id, workspace path, argv, environment, or captured output.
74
74
 
75
- A WebSocket close code such as `1006` means the transport ended without a normal close handshake, but it does not identify who initiated termination: Machine Bridge's own liveness recovery calls `terminate()` when a transport/send timeout is confirmed, and that local hard close can surface as 1006. Diagnose the cause from `last_close_category`, transport-confirmation/send-timeout evidence, and retained network milestones rather than treating 1006 itself as proof of a remote/network-initiated close. If it recovers inside ten seconds, the warning-level service log is intentionally silent and the authenticated `daemon.relay_transport` snapshot is the post-event evidence surface. It is useful for debug diagnosis but not useful as the default user message. It is not evidence that the daemon process restarted. Worker `daemon_transport_error` / `daemon_liveness_timeout` messages and their 1012 close frames are likewise retryable connection conditions, not upgrade instructions. Only an unknown/incompatible Worker error, authentication failure, or identity/version mismatch may produce the fatal protocol/configuration log and daemon exit. Default logs therefore describe the affected layer, duration, classification, and recovery behavior rather than printing raw close envelopes.
75
+ A WebSocket close code such as `1006` means the transport ended without a normal close handshake, but it does not identify who initiated termination: Machine Bridge's own liveness recovery calls `terminate()` when a transport/send timeout is confirmed, and that local hard close can surface as 1006. Diagnose the cause from `last_close_category`, transport-confirmation/send-timeout evidence, and retained network milestones rather than treating 1006 itself as proof of a remote/network-initiated close. If it recovers inside ten seconds, the warning-level service log is intentionally silent and the authenticated `daemon.relay_transport` snapshot is the post-event evidence surface. It is useful for debug diagnosis but not useful as the default user message. It is not evidence that the daemon process restarted. Worker `daemon_transport_error` / `daemon_liveness_timeout` messages and their 1012 close frames are likewise retryable connection conditions, not upgrade instructions. Only an unknown/incompatible Worker error, authentication failure, or identity/version mismatch may produce the fatal protocol/configuration log and daemon exit. A fatal relay protocol log retains only a sanitized protocol error code, source transport (WebSocket or HTTPS), coarse connection-generation class, handshake stage, and fatal/retry disposition; it never records the numeric session generation, endpoint, credentials, account identity, call IDs, or message payload. Default logs therefore describe the affected layer, duration, classification, and recovery behavior rather than printing raw close envelopes.
76
76
 
77
77
  Streamed-call diagnostics are deliberately coarse. MCP `2026-07-28` request/response-stream ownership and the Worker pending-call registry are memory-only; there is no protocol-session, terminal-result, subscriber, or replay store. Worker event counters are scoped to the current isolate and say so in `metric_scope`, while `server_info` exposes only bounded current pending-call/capacity state. These diagnostics do not prove public SSE consumption or MCP-host receipt. Logs and `server_info` must not include tool arguments, terminal results, command text, request keys, account identifiers, raw call IDs, raw connection generations, mirrored parameter values, private paths, or subscriber payloads. A stale-generation result is counted as unmatched rather than logged with its envelope.
78
78
 
@@ -93,7 +93,7 @@ After the host path recovers, compare authenticated `server_info`, `machine-mcp
93
93
 
94
94
  ### Relay interruption messages
95
95
 
96
- A reconnect warning proves a transport interruption, not a daemon crash. Compare daemon PID and process start time with `connected_at`, `last_seen_at`, `daemon.relay_transport.last_close_category`, `last_close_code`, `outage_count`, `recent_outages`, `outage_attempts`, `previous_ready_inbound_silence_ms`, `last_connect_milestones_ms`, and the coarse network-route class. In each completed `recent_outages` entry, `disconnected_at` is the first transition into that outage, `last_disconnect_at` is the final failed reconnect/close transition, `ready_at` is verified recovery, and `duration_ms` measures first-disconnect-to-ready time. The bounded `probe_dispatch_pending_at_start`, `probe_dispatch_age_ms_at_start`, `probe_outstanding_at_start`, `probe_age_ms_at_start`, `transport_confirmation_pending_at_start`, and `application_inbound_silence_ms_at_start` fields freeze the liveness phase at that first transition. Use them to test hypotheses such as a failure coinciding with the first five-second protocol Ping, but do not treat that timing correlation as proof that Ping caused the reset. Also distinguish a planned restart from an accidental outage: a current-generation daemon sends `daemon_draining` before relay close, affected calls receive `reason=daemon_planned_drain`, and owner/full `server_info.worker.continuity_evidence` schema 2 durably retains the planned-drain count/time plus bounded socket-disconnect and client-cancellation observations across Worker isolate replacement. Treat `ready_socket_disconnects`, `unplanned_ready_socket_disconnects`, and `last_ready_socket_disconnect` as the qualified durable socket-outage evidence; the broader `socket_disconnects` counters include candidate/probing transport churn. `last_socket_disconnect.role`, `was_ready`, and `connected_at` identify whether the newest first-observed close/error had ever become the authoritative ready channel. Planned-drain intent is serialized into the WebSocket attachment before close so a Durable Object isolate replacement cannot turn a planned shutdown into an unplanned disconnect. Schema-1 disconnect counts are intentionally reset during the schema-2 transition because old isolates could count stale/expired close callbacks more than once. `worker.observability.continuity` remains isolate-local and may reset; use the qualified durable summary for post-incident correlation rather than relying on a later close-1006 inference. For hosted `read_job`, recover with the same returned `job_id` plus its `recovery_key`; do not resubmit the job's underlying mutation. If the job predates capability issuance or its capability was never delivered, use local CLI/stdio administration instead of attempting hosted global discovery. `last_connect_milestones_ms` contains only bounded relative timings for the most recent connection attempt phases such as DNS resolution, TCP connect, TLS establishment, HTTP rejection, and WebSocket open; `last_failed_connect_stage`, `last_failed_connect_duration_ms`, `last_failed_connect_milestones_ms`, and `last_failed_connect_http_status` retain the most recent failed attempt even after a later retry succeeds. `last_transport_error_ready` and `last_transport_error_authenticated` distinguish failure of an already-established channel from a pre-readiness connection failure. `last_transport_error_reason` is a strict privacy-safe allowlist (`connection_reset`, `connection_timeout`, `network_unreachable`, bounded DNS/TLS classes, or `unknown`) rather than the raw operating-system message. The signed HTTPS fallback retains its last error class/reason after a later successful poll while resetting the current `http_poll_failures` count, so post-recovery diagnosis can determine whether WSS and HTTPS failed through the same system-network episode. None of these fields contains a hostname, address, DNS answer, certificate, close reason, or proxy endpoint. While no daemon channel is ready, `server_info.daemon.previous_connection` retains only the last verified channel's transport, connected/last-seen/disconnected timestamps, and sanitized relay diagnostics; it excludes policy, tools, account identity, daemon instance/connection identity, call IDs, arguments, and results, and it never participates in routing or authorization. `outage_duration_ms` measures the close-to-ready recovery episode; `previous_ready_inbound_silence_ms` measures how long the preceding ready socket had stopped producing inbound transport proof before it actually closed. The second value is therefore the field that exposes a black-holed OPEN WebSocket whose visible reconnect later completes quickly. A VPN/TUN UI may remain “connected” while its upstream route is unusable. Local OS logs can be compared with the exact `last_disconnected_at` timestamp, but an interface-quality change or tunnel-process correlation is not by itself proof of which product, node, edge, or upstream failed. Correlated failure of an independent HTTPS client at the same timestamp—for example an external API `unexpected EOF` while the relay records WebSocket 1006/`connection_reset`—is stronger evidence of a shared system-network/VPN/TUN episode than of a Machine Bridge event-loop or resource-admission failure; it still does not identify the failing tunnel node or upstream provider. Machine Bridge reports only coarse route/proxy classes and never sends or logs interface names, addresses, DNS answers, proxy credentials, or Worker secrets.
96
+ A reconnect warning proves a transport interruption, not a daemon crash. When the daemon instead exits on a relay protocol error, inspect the same default service-log entry for the sanitized protocol error code, source transport, coarse generation class, handshake stage, and disposition; those fields distinguish a current-generation fatal WebSocket violation from a retryable HTTPS fallback violation without exposing raw connection identifiers. Compare daemon PID and process start time with `connected_at`, `last_seen_at`, `daemon.relay_transport.last_close_category`, `last_close_code`, `outage_count`, `recent_outages`, `outage_attempts`, `previous_ready_inbound_silence_ms`, `last_connect_milestones_ms`, and the coarse network-route class. In each completed `recent_outages` entry, `disconnected_at` is the first transition into that outage, `last_disconnect_at` is the final failed reconnect/close transition, `ready_at` is verified recovery, and `duration_ms` measures first-disconnect-to-ready time. The bounded `probe_dispatch_pending_at_start`, `probe_dispatch_age_ms_at_start`, `probe_outstanding_at_start`, `probe_age_ms_at_start`, `transport_confirmation_pending_at_start`, and `application_inbound_silence_ms_at_start` fields freeze the liveness phase at that first transition. Use them to test hypotheses such as a failure coinciding with the first five-second protocol Ping, but do not treat that timing correlation as proof that Ping caused the reset. Also distinguish a planned restart from an accidental outage: a current-generation daemon sends `daemon_draining` before relay close, affected calls receive `reason=daemon_planned_drain`, and owner/full `server_info.worker.continuity_evidence` schema 2 durably retains the planned-drain count/time plus bounded socket-disconnect and client-cancellation observations across Worker isolate replacement. Treat `ready_socket_disconnects`, `unplanned_ready_socket_disconnects`, and `last_ready_socket_disconnect` as the qualified durable socket-outage evidence; the broader `socket_disconnects` counters include candidate/probing transport churn. `last_socket_disconnect.role`, `was_ready`, and `connected_at` identify whether the newest first-observed close/error had ever become the authoritative ready channel. Planned-drain intent is serialized into the WebSocket attachment before close so a Durable Object isolate replacement cannot turn a planned shutdown into an unplanned disconnect. Schema-1 disconnect counts are intentionally reset during the schema-2 transition because old isolates could count stale/expired close callbacks more than once. `worker.observability.continuity` remains isolate-local and may reset; use the qualified durable summary for post-incident correlation rather than relying on a later close-1006 inference. For hosted `read_job`, recover with the same returned `job_id` plus its `recovery_key`; do not resubmit the job's underlying mutation. If the job predates capability issuance or its capability was never delivered, use local CLI/stdio administration instead of attempting hosted global discovery. `last_connect_milestones_ms` contains only bounded relative timings for the most recent connection attempt phases such as DNS resolution, TCP connect, TLS establishment, HTTP rejection, and WebSocket open; `last_failed_connect_stage`, `last_failed_connect_duration_ms`, `last_failed_connect_milestones_ms`, and `last_failed_connect_http_status` retain the most recent failed attempt even after a later retry succeeds. `last_transport_error_ready` and `last_transport_error_authenticated` distinguish failure of an already-established channel from a pre-readiness connection failure. `last_transport_error_reason` is a strict privacy-safe allowlist (`connection_reset`, `connection_timeout`, `network_unreachable`, bounded DNS/TLS classes, or `unknown`) rather than the raw operating-system message. The signed HTTPS fallback retains its last error class/reason after a later successful poll while resetting the current `http_poll_failures` count, so post-recovery diagnosis can determine whether WSS and HTTPS failed through the same system-network episode. None of these fields contains a hostname, address, DNS answer, certificate, close reason, or proxy endpoint. While no daemon channel is ready, `server_info.daemon.previous_connection` retains only the last verified channel's transport, connected/last-seen/disconnected timestamps, and sanitized relay diagnostics; it excludes policy, tools, account identity, daemon instance/connection identity, call IDs, arguments, and results, and it never participates in routing or authorization. `outage_duration_ms` measures the close-to-ready recovery episode; `previous_ready_inbound_silence_ms` measures how long the preceding ready socket had stopped producing inbound transport proof before it actually closed. The second value is therefore the field that exposes a black-holed OPEN WebSocket whose visible reconnect later completes quickly. A VPN/TUN UI may remain “connected” while its upstream route is unusable. Local OS logs can be compared with the exact `last_disconnected_at` timestamp, but an interface-quality change or tunnel-process correlation is not by itself proof of which product, node, edge, or upstream failed. Correlated failure of an independent HTTPS client at the same timestamp—for example an external API `unexpected EOF` while the relay records WebSocket 1006/`connection_reset`—is stronger evidence of a shared system-network/VPN/TUN episode than of a Machine Bridge event-loop or resource-admission failure; it still does not identify the failing tunnel node or upstream provider. Machine Bridge reports only coarse route/proxy classes and never sends or logs interface names, addresses, DNS answers, proxy credentials, or Worker secrets.
97
97
 
98
98
  Brief retryable outages recover automatically. On a verified current daemon channel, `server_info.daemon.relay_transport.outage_active=false`; retained fields describe the immediately preceding transport episode rather than claiming a current outage. WebSocket remains preferred and requests a protocol-level probe after five seconds. Calling `ws.ping()` only queues the control frame; it is not treated as remote-probe dispatch until the WebSocket sender's write callback confirms that the Ping actually left the local send queue. The local sender has a separate thirty-second bounded dispatch window, while a confirmed Ping retains its full ten-second Pong deadline. This deliberately prevents compression/backpressure or a slow local socket queue from spending the remote-response budget before any probe was transmitted. A protocol Pong that arrives while a Ping callback is still pending records bidirectional proof for that dispatch round; if the local write callback then completes inside the thirty-second dispatch budget, it does not arm a stale future Pong deadline. Unrelated application inbound is receive-side evidence only and cannot prove the daemon-to-Worker direction. A local queue whose Ping write callback still has not completed after thirty seconds is classified as `relay_transport_send_timeout` even if unrelated inbound traffic continues. One dispatched Ping that reaches the ten-second response deadline does not hard-kill an otherwise ready WSS: the relay runtime enters a fifteen-second `transport_confirmation_pending` window, sends the existing JSON application heartbeat as an independent path check, and prewarms signed HTTPS in standby without taking ownership away from the still-ready WSS. A protocol Pong or the explicit JSON application `pong` clears suspicion and stops standby prewarm; ordinary tool/control traffic does not, because it proves only the Worker-to-daemon receive direction. Only a second-stage confirmation window that receives no application `pong` becomes `relay_transport_timeout` and terminates the WSS. This keeps a true black hole bounded while no longer amplifying a roughly ten-to-fifteen-second persistent-flow stall into an immediate reconnect storm. `heartbeat.probe_dispatch_*`, `heartbeat.transport_confirmation_*`, and bounded sender-backlog fields distinguish local send delay, first-stage response loss, successful second-stage recovery, and confirmed two-stage failure. The separate periodic JSON application heartbeat remains twenty-five seconds with a seventy-five-second application-silence timeout, begins only after verified relay readiness, and the Worker keeps a wider ninety-second WebSocket liveness fallback. This is a detection/recovery bound, not a guarantee that a degraded network can complete another WebSocket handshake inside the same interval. WebSocket connect attempts have a thirty-second outer budget so a degraded but still valid DNS/TCP/TLS/WebSocket upgrade is not misclassified by an unrealistically narrow connection cutoff. The daemon also explicitly disables client `permessage-deflate`: the relay carries bounded control/JSON traffic, while `ws` enables compression by default on clients and compression adds sender-state/CPU overhead that can queue later frames; the stability path does not need that optional negotiation. The fallback still begins independently rather than waiting thirty seconds for WSS. On first-stage WSS liveness suspicion, the same root-certified ephemeral daemon identity prewarms signed HTTPS in standby; if WSS proves live during the second-stage confirmation, that standby poller stops. If the WSS actually disconnects, fallback switches to exact-generation takeover immediately; an in-flight standby request is aborted and replaced rather than being allowed to consume up to its own request deadline before takeover can start. That in-memory session certificate intentionally has a 24-hour maximum lifetime. For the default portable JWK root, the runtime derives a fresh root-certified ephemeral session ten minutes before expiry, swaps the shared WSS/HTTPS authentication provider, and reconnects the same daemon instance so existing reconnect reconciliation can resume in-flight relay calls without `runtime.stop()`. If the machine was suspended across the renewal timer, the next authentication boundary synchronously renews before signing instead of first attempting an expired certificate. `server_info.runtime.trust.daemon_session` exposes only coarse renewal state such as `automatic_renewal`, `session_generation`, `expires_at`, `renewal_due_at`, and failure class/count; it never exposes session keys or certificate material. A Secure Enclave root does not receive unattended renewal because signing may require user presence. If unattended renewal is unavailable or repeatedly fails until the session is truly expired, the existing fail-closed path still terminates with `relay_device_session_expired` rather than retrying forever with unusable credentials; installed launchd/systemd/Windows supervision may then restart the daemon and obtain a new root-signed session, while a manually run daemon requires operator restart. Ordinary standby/ready fallback requests retain the seven-second deadline; exact-generation takeover requests use a three-second deadline until verified readiness. The ordinary one-second ready poll cadence, five-second standby-prewarm cadence, bounded one/two/four/five-second retry backoff, 750 ms hard minimum request-start interval, and twelve-second liveness window are unchanged. A new daemon-backed call waits at most fifteen seconds for some verified daemon channel, and the measured wait is deducted from that call's original execution budget; the takeover budget is deliberately small enough for one fully timed-out stale request, the first retry backoff, and two further takeover exchanges to fit inside that ceiling. After an established WSS disappears, the daemon explicitly marks its signed HTTP request as a takeover of the Worker-issued `connection_id` for that exact disconnected WebSocket generation. Once candidate preconditions pass, HTTPS may retire only that targeted same-instance zombie WSS that the Worker has not yet observed closing. If a newer same-instance WSS is already ready before the HTTP request arrives, the old generation no longer matches and the stale takeover remains standby instead of retiring the recovered socket. A takeover request without the exact Worker-issued WebSocket connection ID is invalid rather than being treated as an instance-only legacy takeover. Malformed, stale, wrongly targeted, or different-instance requests cannot preempt a healthy incumbent. During replacement, the daemon reconciles `resume_calls`, processes `ready_ack`, proves local readiness, and only then returns `resume_calls_ack.missing_ids`. A missing ID therefore proves both that the same daemon has no active/unacknowledged-result ownership for that call and that the replacement channel is ready. If the initiating MCP response is still open and at least one second remains in the original execution budget, the Worker may transparently retransmit exactly that same call ID, arguments, authority, and a reduced timeout. `read_job` is stricter: redelivery requires the full ten-second reconciliation headroom to remain, otherwise the Worker declines redelivery and returns retryable recovery failure rather than rewriting the call into an under-budget immediate read. If safe redelivery cannot be accepted, the call falls back to retryable `unavailable` with `side_effects_started=false`. Calls that may have executed, retained terminal results, different-daemon calls, and ambiguous mutations are never automatically replayed. Completed relay results that are still waiting for Worker acknowledgement remain bounded in daemon memory and consume the same recovery-ownership capacity as active calls: 16 total with two control-plane slots reserved for `diagnose_runtime`/`list_roots`. When ordinary recovery ownership reaches 14, another ordinary relay call is rejected before execution with retryable `limit_exceeded` and `side_effects_started=false`; the two reserved diagnostic/recovery calls remain available until total capacity reaches 16. The retained-result implementation also keeps one non-admission emergency ownership slot solely for a violated internal capacity invariant: if an already-executed result reaches retention after the normal 16-entry ceiling is unexpectedly full, that one result remains retained for acknowledgement/reconnect ownership instead of being sent unowned and later misclassified as safe to redeliver. Use of that slot emits an error-level capacity event and may make diagnostics temporarily report ownership above the normal maximum; a second such overflow is not sent. This slot is not usable admission capacity and must never be counted to raise the 16-call execution ceiling. An acknowledgement that is permanently lost cannot pin a result forever: first retention is monotonic and the result expires after the 315-second maximum Worker settlement lifetime on the next live relay heartbeat; the disconnected path still uses the shorter reconnect-grace cleanup. `diagnose_runtime.runtime.relay_result_recovery` exposes only aggregate `active_calls`, `retained_results`, active ownership, and capacity counts—never call IDs, tool arguments, or results. Compare outage intervals with sleep/wake records and `diagnose_runtime.runtime.relay.heartbeat` before classifying them as active network faults; local stdio `server_info.runtime.relay.heartbeat` exposes the same state. On macOS, authorized relay activity is protected only after policy/account/operation authorization and argument validation succeed, and relay heartbeats do not count as user activity. The persisted idle-sleep mode is `activity` by default, `ac-continuous`, or `continuous`: `activity` retains the existing shared `/usr/bin/caffeinate -i -s -w <daemon-pid>` assertion plus fixed thirty-minute inactivity grace; `ac-continuous` adds a daemon-lifetime `-s -w <daemon-pid>` assertion while keeping that activity lease; `continuous` holds `-i -s -w <daemon-pid>` for the daemon lifetime and does not arm inactivity grace. A remote `start_process` extends activity ownership only after resource admission and through child settlement. Remote account managed-job runners independently hold the same `-i -s -w <runner-pid>` primitive after runner-claim/account-ownership validation and through admission, steps, cleanup, and terminal persistence. Every assertion retains desired-state recovery after unexpected child failure with fixed 1/5/30-second backoff; explicit release/shutdown disables recovery before terminating the child. `diagnose_runtime.runtime.idle_sleep_guard` reports only coarse mode/activity/recovery ownership and bounded unprotected-duration telemetry; it does not enumerate process-session or job identities. Runtime shutdown terminates process sessions before releasing daemon assertions. None of these modes claims to prevent explicit sleep, lid-close sleep, power loss, or operating-system behavior outside `caffeinate` contracts. A nonzero `event_loop_stall_count` with a large `max_event_loop_lag_ms` means the local daemon was not scheduled promptly; a short stall enters recovery grace, sends a fresh transport probe, and deliberately postpones disconnect. A large stall that aligns with `pmset` Sleep/Wake is suspension evidence. Independently, `relay_outage_analysis` can show either that a close-to-ready interval was itself dominated by system sleep or that the socket failure was first observable at the wake boundary while the same sleep matches the runtime pause; a genuinely awake outage without either evidence remains real transport evidence without identifying which network/host layer caused it. A large `previous_ready_inbound_silence_ms` without a matching local stall remains useful pre-close half-open evidence. Use `--verbose` only when close codes, liveness deadlines, and retry delays are required.
99
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "3.0.0-beta.192",
3
+ "version": "3.0.0-beta.195",
4
4
  "description": "Cross-client MCP bridge for local agent context, structured browser and application automation, files, Git, processes, resources, and durable jobs over stdio or OAuth relay.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -272,10 +272,10 @@
272
272
  "devDependencies": {
273
273
  "@types/node": "26.4.0",
274
274
  "eslint": "10.9.1",
275
- "fast-check": "4.9.0",
275
+ "fast-check": "4.10.0",
276
276
  "globals": "17.11.0",
277
277
  "typescript": "7.0.2",
278
- "wrangler": "4.127.1"
278
+ "wrangler": "4.131.2"
279
279
  },
280
280
  "keywords": [
281
281
  "mcp",
@@ -307,7 +307,7 @@
307
307
  "esbuild@0.28.1": true,
308
308
  "fsevents": false,
309
309
  "sharp@0.35.4": true,
310
- "workerd@1.20260828.1": true
310
+ "workerd@1.20260911.1": true
311
311
  },
312
312
  "packageManager": "npm@12.0.2",
313
313
  "devEngines": {
@@ -335,7 +335,7 @@ export class RelayConnection {
335
335
  terminateSocket(socket);
336
336
  return true;
337
337
  }
338
- this.failPermanently("relay_protocol_error");
338
+ this.failPermanently("relay_protocol_error", { diagnostics: message?.diagnostics || null });
339
339
  return true;
340
340
  }
341
341
 
@@ -521,7 +521,7 @@ export class RelayConnection {
521
521
  });
522
522
  }
523
523
 
524
- failPermanently(category, { socketAlreadyClosed = false, wasReady = this.ready } = {}) {
524
+ failPermanently(category, { socketAlreadyClosed = false, wasReady = this.ready, diagnostics = null } = {}) {
525
525
  if (this.closed) return;
526
526
  const socket = this.socket;
527
527
  this.closed = true;
@@ -550,11 +550,12 @@ export class RelayConnection {
550
550
  this.connectedOnceResolve = null;
551
551
  this.connectedOnceReject = null;
552
552
  this.resetOutage();
553
+ if (diagnostics) this.logger.error?.(message, diagnostics);
553
554
  if (!this.hasConnected && reject) {
554
555
  reject(error);
555
556
  return;
556
557
  }
557
- this.logger.error?.(message);
558
+ if (!diagnostics) this.logger.error?.(message);
558
559
  this.logger.debug?.("remote relay fatal details", { category, cause: relayCloseUserCause(category) });
559
560
  queueMicrotask(async () => {
560
561
  try {
@@ -1,6 +1,7 @@
1
1
  import relayContract from "../shared/relay-contract.json" with { type: "json" };
2
2
  import { RelayConnection } from "./relay-connection.mjs";
3
3
  import { DaemonHttpRelayConnection } from "./daemon-http-relay-connection.mjs";
4
+ import { relayServerErrorReconnectCategory, sanitizeProtocolErrorCode } from "./relay-connection-classification.mjs";
4
5
 
5
6
  export class ResilientRelayConnection {
6
7
  constructor(options = {}) {
@@ -107,6 +108,14 @@ export class ResilientRelayConnection {
107
108
  return this.activeTransport === "https" ? this.http.currentSessionId() : this.websocket.currentSessionId();
108
109
  }
109
110
 
111
+ isCurrentSession(relayContext = {}) {
112
+ const sessionId = Number(relayContext?.sessionId) || 0;
113
+ if (!sessionId) return false;
114
+ if (relayContext?.transport === "https") return sessionId === this.http.currentSessionId();
115
+ if (relayContext?.transport === "websocket") return sessionId === this.websocket.currentSessionId();
116
+ return false;
117
+ }
118
+
110
119
  send(value) {
111
120
  if (this.activeTransport === "websocket") return this.websocket.send(value);
112
121
  if (this.activeTransport === "https") return this.http.send(value);
@@ -124,6 +133,13 @@ export class ResilientRelayConnection {
124
133
  return this.websocket.interrupt(category);
125
134
  }
126
135
 
136
+ interruptForContext(category, relayContext = {}) {
137
+ if (!this.isCurrentSession(relayContext)) return false;
138
+ return relayContext?.transport === "https"
139
+ ? this.http.interrupt(category)
140
+ : this.websocket.interrupt(category);
141
+ }
142
+
127
143
  refreshAuthentication() {
128
144
  const websocketInterrupted = this.websocket.interrupt("relay_session_rotated");
129
145
  const httpInterrupted = this.http.interrupt("relay_session_rotated");
@@ -139,14 +155,40 @@ export class ResilientRelayConnection {
139
155
  return this.websocket.acknowledge(message);
140
156
  }
141
157
  confirmReady(message, relayContext = {}) {
158
+ if (!this.isCurrentSession(relayContext)) return false;
142
159
  return relayContext?.transport === "https" ? this.http.confirmReady(message) : this.websocket.confirmReady(message);
143
160
  }
144
161
  observeApplicationPong(relayContext = {}) {
145
162
  return relayContext?.transport === "https" ? false : this.websocket.observeApplicationPong(relayContext);
146
163
  }
147
164
  handleServerError(message, relayContext = {}) {
148
- if (relayContext?.transport === "https") return this.http.interrupt(message?.error);
149
- return this.websocket.handleServerError(message);
165
+ const sessionId = Number(relayContext?.sessionId) || 0;
166
+ if (sessionId && !this.isCurrentSession(relayContext)) {
167
+ this.logger.debug?.("discarded relay error from an ended transport generation", {
168
+ source_transport: relayTransport(relayContext), connection_generation: "stale",
169
+ handshake_stage: relayHandshakeStage(relayContext),
170
+ });
171
+ return false;
172
+ }
173
+ const errorCode = sanitizeProtocolErrorCode(message?.error);
174
+ const reconnectCategory = relayServerErrorReconnectCategory(errorCode, {
175
+ authenticated: relayContext?.authenticated === true, ready: relayContext?.ready === true,
176
+ });
177
+ const sourceTransport = relayTransport(relayContext);
178
+ const diagnostics = {
179
+ error_code: errorCode, source_transport: sourceTransport,
180
+ connection_generation: sessionId ? "current" : "unbound",
181
+ handshake_stage: relayHandshakeStage(relayContext),
182
+ };
183
+ if (sourceTransport === "https") {
184
+ if (!reconnectCategory) this.logger.warn?.(
185
+ "remote HTTPS relay reported a protocol error; restarting the fallback transport",
186
+ { ...diagnostics, disposition: "retry_transport" },
187
+ );
188
+ return this.http.interrupt(errorCode);
189
+ }
190
+ return this.websocket.handleServerError(reconnectCategory
191
+ ? message : { ...message, error: errorCode, diagnostics: { ...diagnostics, disposition: "fatal_runtime" } });
150
192
  }
151
193
 
152
194
  handleReady(transport, event) {
@@ -278,3 +320,14 @@ function positiveInteger(value, fallback) {
278
320
  const number = Number(value);
279
321
  return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
280
322
  }
323
+
324
+ function relayTransport(relayContext = {}) {
325
+ const transport = String(relayContext?.transport || "");
326
+ return transport === "websocket" || transport === "https" ? transport : "unknown";
327
+ }
328
+
329
+ function relayHandshakeStage(relayContext = {}) {
330
+ if (relayContext?.ready === true) return "post_ready";
331
+ if (relayContext?.authenticated === true) return "authenticated_pre_ready";
332
+ return "pre_authentication";
333
+ }
@@ -40,7 +40,7 @@ export function runtimeRelayConnectionOptions(runtime, input) {
40
40
  ownedCallIds: () => runtime.relayOwnedCallIds(),
41
41
  onMessage,
42
42
  },
43
- onDisconnect: () => runtime.handleRelayDisconnect(),
44
- onReady: () => runtime.handleRelayReady(),
43
+ onDisconnect: (event) => runtime.handleRelayDisconnect(event),
44
+ onReady: (event) => runtime.handleRelayReady(event),
45
45
  };
46
46
  }
@@ -3,30 +3,69 @@ import { handleRuntimeRelayAcknowledgement } from "./runtime-relay-acknowledgeme
3
3
  import { normalizeRelayResumeCalls } from "./runtime-relay.mjs";
4
4
  import { normalizeAuthorityRevocation } from "../shared/authority-revocation.mjs";
5
5
 
6
+ export function handleRuntimeRelayProtocolViolation(runtime, errorCode, relayContext = {}) {
7
+ const sessionId = Number(relayContext?.sessionId) || 0;
8
+ if (staleRelayGeneration(runtime, relayContext)) {
9
+ runtime.logger?.event?.("debug", "relay.protocol_violation.stale_generation_discarded", {
10
+ source_transport: relayTransport(relayContext) || "unknown",
11
+ connection_generation: "stale",
12
+ handshake_stage: relayHandshakeStage(relayContext),
13
+ }, "Discarded a protocol violation from an ended relay generation");
14
+ return;
15
+ }
16
+ if (runtime.relay) {
17
+ runtime.relay.handleServerError({ type: "error", error: errorCode }, relayContext);
18
+ return;
19
+ }
20
+ runtime.logger?.error?.("remote relay protocol error; upgrade and redeploy both components, then restart the daemon", {
21
+ error_code: safeMessageType(errorCode),
22
+ source_transport: relayTransport(relayContext) || "unknown",
23
+ connection_generation: sessionId ? "unknown" : "unbound",
24
+ handshake_stage: relayHandshakeStage(relayContext),
25
+ disposition: "fatal_runtime",
26
+ });
27
+ }
28
+
29
+ export function handleRuntimeRelayDisconnect(runtime, relayContext = {}) {
30
+ const transport = relayTransport(relayContext);
31
+ const states = relayResumeState(runtime);
32
+ if (!transport) states.clear();
33
+ else clearRelayResumeState(runtime, transport);
34
+ runtime.relayCallRecovery.disconnected();
35
+ }
36
+
6
37
  export async function handleRuntimeRelayControlMessage(runtime, message, relayContext = {}) {
7
38
  if (message.type === "welcome") {
8
39
  runtime.relay?.observeWelcome(message, relayContext);
9
40
  return true;
10
41
  }
11
42
  if (message.type === "hello_ack") {
12
- runtime.relayResumeSessionId = 0;
13
- runtime.relayResumeMissingIds = [];
43
+ clearRelayResumeState(runtime, relayTransport(relayContext) || "websocket");
14
44
  runtime.relay?.acknowledge(message, relayContext);
15
45
  return true;
16
46
  }
47
+ if (staleRelayGeneration(runtime, relayContext)) {
48
+ runtime.logger?.event?.("debug", "relay.control.stale_generation_discarded", {
49
+ source_transport: relayTransport(relayContext) || "unknown",
50
+ connection_generation: "stale",
51
+ handshake_stage: relayHandshakeStage(relayContext),
52
+ message_type: safeMessageType(message?.type),
53
+ }, "Discarded a control message from an ended relay generation");
54
+ return true;
55
+ }
17
56
  if (message.type === "resume_calls") return handleResumeCalls(runtime, message, relayContext);
18
57
  if (message.type === "authority_revoke") {
19
58
  const sessionId = Number(relayContext.sessionId) || 0;
20
59
  const revocationId = String(message.revocation_id || "");
21
60
  const revocation = normalizeAuthorityRevocation(message);
22
61
  if (!sessionId || relayContext.authenticated !== true || !/^revoke_[A-Za-z0-9_-]{43}$/.test(revocationId) || !revocation) {
23
- runtime.handleRelayProtocolViolation("invalid_authority_revoke");
62
+ runtime.handleRelayProtocolViolation("invalid_authority_revoke", relayContext);
24
63
  return true;
25
64
  }
26
65
  try {
27
66
  await runtime.applyAuthorityRevocation(revocation);
28
67
  } catch (error) {
29
- runtime.relay?.interrupt?.("local_authority_revocation_retry");
68
+ interruptRelayContext(runtime, "local_authority_revocation_retry", relayContext);
30
69
  throw error;
31
70
  }
32
71
  runtime.relay?.sendForSession?.({ type: "authority_revoke_ack", revocation_id: revocationId }, sessionId);
@@ -34,29 +73,30 @@ export async function handleRuntimeRelayControlMessage(runtime, message, relayCo
34
73
  }
35
74
  if (message.type === "ready_ack") {
36
75
  const sessionId = Number(relayContext.sessionId) || 0;
37
- if (!sessionId || sessionId !== runtime.relayResumeSessionId) {
38
- runtime.handleRelayProtocolViolation("resume_calls_required");
76
+ const key = relayResumeKey(relayContext);
77
+ const resume = key ? relayResumeState(runtime).get(key) : null;
78
+ if (!sessionId || !resume) {
79
+ runtime.handleRelayProtocolViolation("resume_calls_required", relayContext);
39
80
  return true;
40
81
  }
41
82
  if (!runtime.relay?.confirmReady(message, relayContext)) {
42
- runtime.relay?.interrupt?.("relay_transport_error");
83
+ interruptRelayContext(runtime, "relay_transport_error", relayContext);
43
84
  return true;
44
85
  }
45
86
  const acknowledgement = runtime.relay?.sendForSession?.({
46
87
  type: "resume_calls_ack",
47
- missing_ids: Array.isArray(runtime.relayResumeMissingIds) ? runtime.relayResumeMissingIds : [],
88
+ missing_ids: Array.isArray(resume.missingIds) ? resume.missingIds : [],
48
89
  }, sessionId);
49
90
  if (!acknowledgement?.ok) {
50
- runtime.relay?.interrupt?.("relay_transport_error");
91
+ interruptRelayContext(runtime, "relay_transport_error", relayContext);
51
92
  return true;
52
93
  }
53
- runtime.relayResumeSessionId = 0;
54
- runtime.relayResumeMissingIds = [];
94
+ relayResumeState(runtime).delete(key);
55
95
  return true;
56
96
  }
57
97
  if (message.type === "pong") return handlePong(runtime, relayContext);
58
98
  if (message.type === "tool_result_ack" || message.type === "daemon_draining_ack") {
59
- return handleRuntimeRelayAcknowledgement(runtime, message, relayContext);
99
+ return handleRuntimeRelayAcknowledgement(runtimeForRelayContext(runtime, relayContext), message, relayContext);
60
100
  }
61
101
  if (message.type === "error") {
62
102
  runtime.relay?.handleServerError(message, relayContext);
@@ -66,7 +106,7 @@ export async function handleRuntimeRelayControlMessage(runtime, message, relayCo
66
106
  if (!isRelayReadyContext(relayContext, runtime.relay)
67
107
  || typeof message.id !== "string"
68
108
  || !/^call_[A-Za-z0-9_-]{8,240}$/.test(message.id)) {
69
- runtime.handleRelayProtocolViolation("invalid_cancel_call");
109
+ runtime.handleRelayProtocolViolation("invalid_cancel_call", relayContext);
70
110
  return true;
71
111
  }
72
112
  runtime.cancelRelayCall(message.id, "caller_cancelled");
@@ -78,13 +118,19 @@ export async function handleRuntimeRelayControlMessage(runtime, message, relayCo
78
118
  function handleResumeCalls(runtime, message, relayContext) {
79
119
  const sessionId = Number(relayContext.sessionId) || 0;
80
120
  const resume = normalizeRelayResumeCalls(message);
81
- if (!resume.ok || !sessionId || relayContext.authenticated !== true || relayContext.ready === true) {
82
- runtime.handleRelayProtocolViolation("invalid_resume_calls");
121
+ const key = relayResumeKey(relayContext);
122
+ if (!resume.ok || !sessionId || !key || relayContext.authenticated !== true || relayContext.ready === true) {
123
+ runtime.handleRelayProtocolViolation("invalid_resume_calls", relayContext);
83
124
  return true;
84
125
  }
126
+ const states = relayResumeState(runtime);
127
+ if (states.has(key)) {
128
+ runtime.handleRelayProtocolViolation("invalid_resume_calls", relayContext);
129
+ return true;
130
+ }
131
+ clearRelayResumeState(runtime, relayTransport(relayContext));
85
132
  const missingIds = runtime.reconcileRelayCalls(resume.ids);
86
- runtime.relayResumeSessionId = sessionId;
87
- runtime.relayResumeMissingIds = Array.isArray(missingIds) ? missingIds : [];
133
+ states.set(key, { missingIds: Array.isArray(missingIds) ? missingIds : [] });
88
134
  return true;
89
135
  }
90
136
 
@@ -93,3 +139,65 @@ function handlePong(runtime, relayContext) {
93
139
  runtime.relayCallRecovery.pulse();
94
140
  return true;
95
141
  }
142
+
143
+ function relayResumeState(runtime) {
144
+ if (!(runtime.relayResumeStates instanceof Map)) runtime.relayResumeStates = new Map();
145
+ return runtime.relayResumeStates;
146
+ }
147
+
148
+ function relayTransport(relayContext = {}) {
149
+ const value = String(relayContext?.transport || "");
150
+ return value === "websocket" || value === "https" ? value : "";
151
+ }
152
+
153
+ function relayResumeKey(relayContext = {}) {
154
+ const transport = relayTransport(relayContext);
155
+ const sessionId = Number(relayContext?.sessionId) || 0;
156
+ return transport && sessionId ? transport + ":" + sessionId : "";
157
+ }
158
+
159
+ function clearRelayResumeState(runtime, transport = "") {
160
+ const states = relayResumeState(runtime);
161
+ if (!transport) {
162
+ states.clear();
163
+ return;
164
+ }
165
+ const prefix = transport + ":";
166
+ for (const key of states.keys()) {
167
+ if (key.startsWith(prefix)) states.delete(key);
168
+ }
169
+ }
170
+
171
+ function staleRelayGeneration(runtime, relayContext = {}) {
172
+ const sessionId = Number(relayContext?.sessionId) || 0;
173
+ if (!sessionId || !relayTransport(relayContext) || typeof runtime.relay?.isCurrentSession !== "function") return false;
174
+ return runtime.relay.isCurrentSession(relayContext) !== true;
175
+ }
176
+
177
+ function interruptRelayContext(runtime, category, relayContext) {
178
+ if (typeof runtime.relay?.interruptForContext === "function") {
179
+ return runtime.relay.interruptForContext(category, relayContext);
180
+ }
181
+ return runtime.relay?.interrupt?.(category);
182
+ }
183
+
184
+ function runtimeForRelayContext(runtime, relayContext) {
185
+ return {
186
+ relay: runtime.relay,
187
+ relayCallRecovery: runtime.relayCallRecovery,
188
+ relayShutdownDrain: runtime.relayShutdownDrain,
189
+ handleRelayProtocolViolation(errorCode) {
190
+ return runtime.handleRelayProtocolViolation(errorCode, relayContext);
191
+ },
192
+ };
193
+ }
194
+
195
+ function relayHandshakeStage(relayContext = {}) {
196
+ if (relayContext?.ready === true) return "post_ready";
197
+ if (relayContext?.authenticated === true) return "authenticated_pre_ready";
198
+ return "pre_authentication";
199
+ }
200
+
201
+ function safeMessageType(value) {
202
+ return String(value || "unknown").replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64) || "unknown";
203
+ }
@@ -41,7 +41,7 @@ import { policyForContext } from "./authority-context.mjs";
41
41
  import { createRuntimeRelayConnection, normalizeRelayToolCall } from "./runtime-relay.mjs"; import { relayRecoveryCapacityRejection } from "./relay-recovery-admission.mjs";
42
42
  import { runtimeControlPlaneSnapshot } from "./runtime-diagnostic-state.mjs";
43
43
  import { shortCallId } from "./short-identifiers.mjs";
44
- import { handleRuntimeRelayControlMessage } from "./runtime-relay-control.mjs";
44
+ import { handleRuntimeRelayControlMessage, handleRuntimeRelayDisconnect, handleRuntimeRelayProtocolViolation } from "./runtime-relay-control.mjs";
45
45
  import { RelayCallRecovery } from "./relay-call-recovery.mjs";
46
46
  import { RuntimeRelayShutdownDrain } from "./runtime-relay-shutdown-drain.mjs";
47
47
  import { RuntimeResourceService } from "./runtime-resource-service.mjs";
@@ -83,7 +83,7 @@ export class LocalRuntime {
83
83
  this.relayInstanceId = `daemon_${randomBytes(18).toString("base64url")}`;
84
84
  this.activeRelayCalls = new Map();
85
85
  this.suppressedRelayResults = new Map();
86
- this.relayResumeSessionId = 0; this.relayResumeMissingIds = [];
86
+ this.relayResumeStates = new Map();
87
87
  this.remoteActivityIdleSleepGuard = new RemoteActivityIdleSleepGuard({ logger: this.logger, mode: idleSleepMode });
88
88
  this.callRegistry = new CallRegistry({
89
89
  maximum: MAX_CONCURRENT_TOOL_CALLS,
@@ -359,11 +359,7 @@ export class LocalRuntime {
359
359
  handleRelayControlMessage(message, relayContext = {}) { return handleRuntimeRelayControlMessage(this, message, relayContext); }
360
360
 
361
361
  handleRelayProtocolViolation(errorCode, relayContext = {}) {
362
- if (this.relay) {
363
- this.relay.handleServerError({ type: "error", error: errorCode }, relayContext);
364
- return;
365
- }
366
- this.logger.error?.("remote relay protocol error; upgrade and redeploy both components, then restart the daemon");
362
+ return handleRuntimeRelayProtocolViolation(this, errorCode, relayContext);
367
363
  }
368
364
 
369
365
  handleRelayProbe(message, relayContext = {}) {
@@ -452,11 +448,7 @@ export class LocalRuntime {
452
448
 
453
449
  relayOwnedCallIds() { return this.relayCallRecovery?.ownedCallIds?.() ?? [...this.activeRelayCalls.keys()]; }
454
450
 
455
- handleRelayDisconnect() {
456
- this.relayResumeSessionId = 0;
457
- this.relayResumeMissingIds = [];
458
- this.relayCallRecovery.disconnected();
459
- }
451
+ handleRelayDisconnect(relayContext = {}) { return handleRuntimeRelayDisconnect(this, relayContext); }
460
452
 
461
453
  handleRelayReady() {
462
454
  this.relayCallRecovery.ready();
@@ -8,7 +8,7 @@
8
8
  "name": "machine-bridge-mcp-wrangler-toolchain",
9
9
  "version": "1.0.0",
10
10
  "dependencies": {
11
- "wrangler": "4.127.1"
11
+ "wrangler": "4.131.2"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=26.0.0",
@@ -40,9 +40,9 @@
40
40
  }
41
41
  },
42
42
  "node_modules/@cloudflare/workerd-darwin-64": {
43
- "version": "1.20260828.1",
44
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260828.1.tgz",
45
- "integrity": "sha512-CVd+xPhqUESg8Xhq09TZx0wl4FSirfJGOzvbPz2yHhBIvmNHFFQkSN3rkd7wEwnhQQk37Xi0/aD6ykPLJbmGiQ==",
43
+ "version": "1.20260911.1",
44
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260911.1.tgz",
45
+ "integrity": "sha512-785eaY1bkR1cm4Z/PCUeteZYmTMe6lre2zz63/GdGGimsoMsKxgl4brFPRukim8iv28EyD1XoCB/VPYF20BERA==",
46
46
  "cpu": [
47
47
  "x64"
48
48
  ],
@@ -56,9 +56,9 @@
56
56
  }
57
57
  },
58
58
  "node_modules/@cloudflare/workerd-darwin-arm64": {
59
- "version": "1.20260828.1",
60
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260828.1.tgz",
61
- "integrity": "sha512-5HDPXRM152vU5JveByGFk34X57TVyIsfp4cabepAf45DC0MKvm52ucJqAjW1h8bvW4X+zRw9GU35OHF9FEC9Ww==",
59
+ "version": "1.20260911.1",
60
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260911.1.tgz",
61
+ "integrity": "sha512-WU4bFqEN0H7ndGWxoedegv95DmNVBtv0ncXcHG9nYFTUI78sxEb0qoT3U6Ga4hyBkzsJFBX/zvVBIGX3qKldGA==",
62
62
  "cpu": [
63
63
  "arm64"
64
64
  ],
@@ -72,9 +72,9 @@
72
72
  }
73
73
  },
74
74
  "node_modules/@cloudflare/workerd-linux-64": {
75
- "version": "1.20260828.1",
76
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260828.1.tgz",
77
- "integrity": "sha512-MQ1Ll9P7F72HHUKizbb7BlDfbY8fRoNMpbIpZoU6uKsSkneFICWSKv6UlgU9EQZ+w0i7TMa12iUgJ8l29eRI9A==",
75
+ "version": "1.20260911.1",
76
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260911.1.tgz",
77
+ "integrity": "sha512-0Y2gy62oxQxWa38qinSPE6zNL5+JmumJtDY9AWW1HB8KHuATxN71o5MGzmVFfB8PwZsiHfUd2Sv7O22krCOrhw==",
78
78
  "cpu": [
79
79
  "x64"
80
80
  ],
@@ -88,9 +88,9 @@
88
88
  }
89
89
  },
90
90
  "node_modules/@cloudflare/workerd-linux-arm64": {
91
- "version": "1.20260828.1",
92
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260828.1.tgz",
93
- "integrity": "sha512-FBTaUQ1xcU9jcp4OyBPcH8x0QiFvc1iuZL2GkD8zp2q1WyTVHYOptRDQUU+cuHjt0rQ2EIKVPBjahPxfa0joBw==",
91
+ "version": "1.20260911.1",
92
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260911.1.tgz",
93
+ "integrity": "sha512-kttNPnx1r2lCqFUoMH62z7CqGV+j4QBbw5fdtaz4pzOrzBv0AWkNATt7onFUe+SwP8zhcepMtbm2F4kKzTf6VA==",
94
94
  "cpu": [
95
95
  "arm64"
96
96
  ],
@@ -104,9 +104,9 @@
104
104
  }
105
105
  },
106
106
  "node_modules/@cloudflare/workerd-windows-64": {
107
- "version": "1.20260828.1",
108
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260828.1.tgz",
109
- "integrity": "sha512-yvr77hC7dUbvK5K+SCg062kkPq3sx+drV1PcgHslzHDYcJBtT0V3X80qLE49LW1vq2svaeNmsVQS+vHsqWu8cQ==",
107
+ "version": "1.20260911.1",
108
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260911.1.tgz",
109
+ "integrity": "sha512-5iO/YfoBDOgO3CrHdkiiVP8SL3O2jC+c6Ux3d378TSPKLhU5+CgHjtE/ZSodWQrzr4FzFRqdW8S7n5nbyD1MHQ==",
110
110
  "cpu": [
111
111
  "x64"
112
112
  ],
@@ -1276,15 +1276,15 @@
1276
1276
  }
1277
1277
  },
1278
1278
  "node_modules/miniflare": {
1279
- "version": "5.20260828.0-alpha",
1280
- "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260828.0-alpha.tgz",
1281
- "integrity": "sha512-6nbxhZEcz/UET3Y1OnYPsrAUjUmuFoib3ynUqteRdn1YnDxsLg8cwgZJZCk9QmtOmGzXwzXzgE/d/C0dJAPtVw==",
1279
+ "version": "5.20260911.1-alpha",
1280
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260911.1-alpha.tgz",
1281
+ "integrity": "sha512-7IDj9monoYcCPrS8HfcTt90T3pDwKGvNEAR1Y061KbJhBd5JkStOwOpHMUDFBo9PEbjWVDxPICAnXGNNV2LNfQ==",
1282
1282
  "license": "MIT",
1283
1283
  "dependencies": {
1284
1284
  "@cspotcode/source-map-support": "0.8.1",
1285
- "sharp": "0.35.2",
1285
+ "sharp": "0.35.4",
1286
1286
  "undici": "7.29.0",
1287
- "workerd": "1.20260828.1",
1287
+ "workerd": "1.20260911.1",
1288
1288
  "ws": "8.21.0",
1289
1289
  "youch": "4.1.0-beta.10"
1290
1290
  },
@@ -1403,9 +1403,9 @@
1403
1403
  }
1404
1404
  },
1405
1405
  "node_modules/workerd": {
1406
- "version": "1.20260828.1",
1407
- "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260828.1.tgz",
1408
- "integrity": "sha512-pB9yvt0kkwZDAGZHmpY59r0o3hM0DzdW6BJERqwZOhunZ3ssOyDSgQxOQer2cSZW4YCFeOTIQYN1qwhK5wv/Cw==",
1406
+ "version": "1.20260911.1",
1407
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260911.1.tgz",
1408
+ "integrity": "sha512-vRr8QdBxueQOZJO1hRCI73EZlix87IAyBAcSyI3rA1VB+6oxjw3oaqzYnIV8C4IOPtUgihbdMAgzkb5GM4V7DQ==",
1409
1409
  "hasInstallScript": true,
1410
1410
  "license": "Apache-2.0",
1411
1411
  "bin": {
@@ -1415,27 +1415,27 @@
1415
1415
  "node": ">=16"
1416
1416
  },
1417
1417
  "optionalDependencies": {
1418
- "@cloudflare/workerd-darwin-64": "1.20260828.1",
1419
- "@cloudflare/workerd-darwin-arm64": "1.20260828.1",
1420
- "@cloudflare/workerd-linux-64": "1.20260828.1",
1421
- "@cloudflare/workerd-linux-arm64": "1.20260828.1",
1422
- "@cloudflare/workerd-windows-64": "1.20260828.1"
1418
+ "@cloudflare/workerd-darwin-64": "1.20260911.1",
1419
+ "@cloudflare/workerd-darwin-arm64": "1.20260911.1",
1420
+ "@cloudflare/workerd-linux-64": "1.20260911.1",
1421
+ "@cloudflare/workerd-linux-arm64": "1.20260911.1",
1422
+ "@cloudflare/workerd-windows-64": "1.20260911.1"
1423
1423
  }
1424
1424
  },
1425
1425
  "node_modules/wrangler": {
1426
- "version": "4.127.1",
1427
- "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.127.1.tgz",
1428
- "integrity": "sha512-OzsiNgaI8i681L/+KnAKc+uEZ5D57xK5JuNvCOpRKICF4/5Q3Cu1oTGuUiT/f3GDUqQb3gzXNT0tfOHGMEtknw==",
1426
+ "version": "4.131.2",
1427
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.131.2.tgz",
1428
+ "integrity": "sha512-jmkGE7monbPKyYQr1FPQN+SARVhddqw2fhXOmTKCw4lroqlFGSS6rit/RTvPi/qzNLKrXxkS8DhWXasJnStplg==",
1429
1429
  "license": "MIT OR Apache-2.0",
1430
1430
  "dependencies": {
1431
1431
  "@cloudflare/kv-asset-handler": "0.5.0",
1432
1432
  "@cloudflare/unenv-preset": "2.16.1",
1433
1433
  "blake3-wasm": "2.1.5",
1434
1434
  "esbuild": "0.28.1",
1435
- "miniflare": "5.20260828.0-alpha",
1435
+ "miniflare": "5.20260911.1-alpha",
1436
1436
  "path-to-regexp": "6.3.0",
1437
1437
  "unenv": "2.0.0-rc.24",
1438
- "workerd": "1.20260828.1"
1438
+ "workerd": "1.20260911.1"
1439
1439
  },
1440
1440
  "bin": {
1441
1441
  "cf-wrangler": "bin/cf-wrangler.js",
@@ -1449,7 +1449,7 @@
1449
1449
  "fsevents": "2.3.3"
1450
1450
  },
1451
1451
  "peerDependencies": {
1452
- "@cloudflare/workers-types": "^5.20260828.1"
1452
+ "@cloudflare/workers-types": "^5.20260911.1"
1453
1453
  },
1454
1454
  "peerDependenciesMeta": {
1455
1455
  "@cloudflare/workers-types": {
@@ -8,7 +8,7 @@
8
8
  "npm": ">=12.0.0"
9
9
  },
10
10
  "dependencies": {
11
- "wrangler": "4.127.1"
11
+ "wrangler": "4.131.2"
12
12
  },
13
13
  "overrides": {
14
14
  "sharp": "0.35.4",
@@ -18,6 +18,6 @@
18
18
  "esbuild@0.28.1": true,
19
19
  "fsevents": false,
20
20
  "sharp@0.35.4": true,
21
- "workerd@1.20260828.1": true
21
+ "workerd@1.20260911.1": true
22
22
  }
23
23
  }
@@ -152,13 +152,13 @@ async function runNpm(npmCli, args, cwd, runCommand, options, timeoutMs, allowFa
152
152
  }
153
153
 
154
154
  function validateTemplate(manifest, lock) {
155
- if (manifest.private !== true || manifest.dependencies?.wrangler !== "4.127.1") {
155
+ if (manifest.private !== true || manifest.dependencies?.wrangler !== "4.131.2") {
156
156
  throw new Error("Wrangler toolchain manifest lost its exact private Wrangler dependency");
157
157
  }
158
158
  if (manifest.overrides?.undici !== "7.29.0" || manifest.overrides?.sharp !== "0.35.4") {
159
159
  throw new Error("Wrangler toolchain manifest lost its security overrides");
160
160
  }
161
- const expectedScripts = { "esbuild@0.28.1": true, fsevents: false, "sharp@0.35.4": true, "workerd@1.20260828.1": true };
161
+ const expectedScripts = { "esbuild@0.28.1": true, fsevents: false, "sharp@0.35.4": true, "workerd@1.20260911.1": true };
162
162
  if (JSON.stringify(manifest.allowScripts) !== JSON.stringify(expectedScripts)) {
163
163
  throw new Error("Wrangler toolchain manifest lost its exact install-script policy");
164
164
  }
@@ -57,7 +57,7 @@ import { hostedManagedJobDaemonArguments, projectHostedManagedJobResult } from "
57
57
  import { cancelManagedJobMonitorClaimsIfAvailable, claimManagedJobMonitor, hasManagedJobMonitorClaimIfAvailable, ManagedJobMonitorClaimStore } from "./mcp-job-monitor-claims.ts";
58
58
  import { JOB_MONITOR_CLAIM_TOOL, JOB_MONITOR_READ_TOOL, JOB_MONITOR_RENDER_TOOL, managedJobMonitorReadDaemonArguments, projectManagedJobMonitorStatus, renderManagedJobMonitor } from "./mcp-job-monitor-tools.ts";
59
59
  import { closeWebSocketQuietly, daemonErrorCloseCode, isObjectRecord, rejectDaemonMessage, sendWebSocketQuietly, trySendWebSocket } from "./websocket-protocol.ts";
60
- const SERVER_VERSION = "3.0.0-beta.192";
60
+ const SERVER_VERSION = "3.0.0-beta.195";
61
61
  const MCP_SERVER_INFO = mcpServerInfo(SERVER_VERSION);
62
62
  const MAX_DAEMON_MESSAGE_BYTES = 8 * 1024 * 1024;
63
63
  const DAEMON_RECONNECT_GRACE_MS = relayContract.reconnectGraceMs; const NEW_CALL_RECONNECT_GRACE_MS = relayContract.newCallReconnectGraceMs;