machine-bridge-mcp 3.0.0-beta.21 → 3.0.0-beta.22
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 +14 -0
- package/browser-extension/manifest.json +1 -1
- package/docs/ARCHITECTURE.md +4 -4
- package/docs/AUDIT.md +12 -0
- package/docs/LOGGING.md +1 -1
- package/docs/OPERATIONS.md +8 -2
- package/docs/TESTING.md +1 -1
- package/package.json +1 -1
- package/src/local/autostart-log-maintenance.mjs +36 -0
- package/src/local/cli.mjs +10 -0
- package/src/local/relay-call-recovery.mjs +40 -21
- package/src/local/runtime.mjs +14 -1
- package/src/shared/relay-contract.json +1 -0
- package/src/worker/durable-stream-calls.ts +12 -24
- package/src/worker/index.ts +6 -8
- package/src/worker/tool-timeout.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.0-beta.22 - 2026-07-28
|
|
4
|
+
|
|
5
|
+
### ChatGPT call continuity and terminal delivery
|
|
6
|
+
|
|
7
|
+
- Add an explicit daemon-result acknowledgement. The local runtime retains every terminal result after WebSocket queueing, replays unacknowledged results after reconnect and on heartbeat, and removes them only after the Worker confirms that the generation-guarded terminal transaction committed. This closes the loss window between local `send()` acceptance and Durable Object persistence that could leave a completed local command as a ghost Worker call.
|
|
8
|
+
- Make durable settlement fail closed. A terminal-storage exception is observable and retryable instead of being reported as a completed call; stale connection generations remain unacknowledged, while duplicate results for an already terminal call are acknowledged idempotently so replay converges.
|
|
9
|
+
- Stop treating one tool deadline as proof that the complete daemon socket is dead. Tool timeout now cancels only that call; the independent 90-second daemon-liveness alarm remains the sole connection-invalidating authority.
|
|
10
|
+
- Bound remote foreground execution to 85 seconds plus five seconds of relay overhead, below the observed hosted-client request ceiling. The local process APIs retain their 600-second schema range, but work expected to exceed the interactive budget must use process sessions or managed jobs rather than one foreground ChatGPT call.
|
|
11
|
+
- Tail-trim background daemon logs every 15 minutes as well as before startup, reusing the existing owner-only, no-follow, single-link, schema-checked, UTF-8 line-safe maintenance path.
|
|
12
|
+
|
|
13
|
+
### Verification
|
|
14
|
+
|
|
15
|
+
- Add acknowledgement-loss/replay, persistent-terminal-write failure, stale generation, hosted-client deadline, runtime log-maintenance, and real Wrangler acknowledgement coverage. Type checking, lint, architecture, privacy, structured logging, security properties, SARIF, critical coverage, local self-test, Worker infrastructure, and Worker OAuth/MCP integration pass.
|
|
16
|
+
|
|
3
17
|
## 3.0.0-beta.21 - 2026-07-27
|
|
4
18
|
|
|
5
19
|
### Relay continuity and stable MCP catalog
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
"action": {
|
|
31
31
|
"default_title": "Machine Bridge Browser"
|
|
32
32
|
},
|
|
33
|
-
"version_name": "3.0.0-beta.
|
|
33
|
+
"version_name": "3.0.0-beta.22",
|
|
34
34
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxryYkpZhq8+VAQLHcGS9BAHQcyKX8RHGIpIwvtIVRU/rcOcE0bNdnM0aZJ/h6xWQsGDHlhvjT2+1aJaAn/9k8473BRWajzVXld961CdHYVFVHoce2hHiSJ0xydWrHMMZhAm0mN0UzjEpgZ0tMw209efcZHIvSwuxhteZMRy4kyiVjwFlOf5oXFCxRuCJnPj3AK9CmCf4XgEBuPIJ0TZmjGHOOdBvJmbCNnAWXYEo5/mf7MfCGhV4IJ1hNuhpoNQfOFKMUcw9/v/IpT62XpfXdGYTfGYCmCjC+gntK1spbkr2P4/2+sYMQtLpse71mpSNGXfcf3abU55Vpn+gncSxRQIDAQAB"
|
|
35
35
|
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -175,10 +175,10 @@ Remote OAuth binds each code, access token, and refresh token to a named Machine
|
|
|
175
175
|
8. A new daemon first authenticates as a bounded `probing` socket. The Worker sends a random `relay_probe`; the local runtime returns it through the normal session-bound result-delivery path; only the matching result produces `ready_ack`, promotion to the active daemon, and safe replacement of an incumbent connection.
|
|
176
176
|
9. `tools/list` is a stable package-and-account-role discovery catalog and declares `listChanged: false`; a brief relay interruption does not mutate it. `server_info.authorization.effective_tools` is the live daemon/account intersection and is the authority diagnostic.
|
|
177
177
|
10. `tools/call` receives a random relay call ID and is bound to the daemon process's ephemeral instance identifier, a random per-WebSocket connection generation, and the authenticated client request key. When the client accepts `text/event-stream`, `BridgeRoom` commits recovery state plus durable call ownership and deadlines before sending the daemon envelope, then returns a bounded descriptor immediately; the outer Worker owns the SSE priming frame, keepalives, and one internal terminal subscription. No unresolved terminal Promise, JavaScript timer, or Durable Object `waitUntil` owns the streamed dispatch. JSON-only clients retain the single terminal response.
|
|
178
|
-
11. The runtime validates policy and arguments, executes the tool, and
|
|
179
|
-
12. If the socket remains ready, the Durable Object accepts the result only from that connection generation. If it drops, the Worker durably detaches the streamed call for at most two minutes and accepts completion only after a replacement socket with the same daemon-process identifier has passed the end-to-end readiness probe and atomically acquired a new connection generation. A delayed result or close event from the old socket cannot settle or detach the rebound call. The local runtime preserves the operation and queues a completion over the same shared interval. The Worker pauses the record's remaining normal deadline while detached and resumes it after same-instance rebinding, so connected calls are not granted an unconditional recovery extension.
|
|
180
|
-
13. Only a matching session-scoped `notifications/cancelled` request removes the pending indexes and sends best-effort cancellation to a connected daemon. Local completion that races with explicit cancellation is discarded. On every readiness handover, the Worker first sends an authoritative bounded `resume_calls` set; the runtime cancels active calls and queued results absent from that set before accepting `ready_ack`. A request explicitly cancelled while disconnected therefore cannot be revived by a fast reconnect.
|
|
181
|
-
14. If same-instance readiness does not return before the grace deadline, the Worker rejects the detached request and the local runtime cancels ordinary calls, terminates their process trees, and discards queued results. A newly started daemon has a different instance identifier and cannot inherit prior calls.
|
|
178
|
+
11. The runtime validates policy and arguments, executes the tool, and produces a bounded result. It retains the terminal envelope after WebSocket queueing and replays it until the Worker returns `tool_result_ack`; queue acceptance is not treated as durable delivery. Closing or losing the HTTP response stream only makes that stream unwritable; it is not an MCP cancellation and does not remove the pending request.
|
|
179
|
+
12. If the socket remains ready, the Durable Object accepts the result only from that connection generation and acknowledges only after transient settlement, committed durable settlement, or an idempotent already-terminal duplicate. A persistence exception is not converted into success, and a stale generation is not acknowledged. If it drops, the Worker durably detaches the streamed call for at most two minutes and accepts completion only after a replacement socket with the same daemon-process identifier has passed the end-to-end readiness probe and atomically acquired a new connection generation. A delayed result or close event from the old socket cannot settle or detach the rebound call. The local runtime preserves the operation and queues a completion over the same shared interval. The Worker pauses the record's remaining normal deadline while detached and resumes it after same-instance rebinding, so connected calls are not granted an unconditional recovery extension.
|
|
180
|
+
13. The terminal store is exactly-once while daemon delivery is at-least-once. Lost acknowledgement causes heartbeat/reconnect replay; the generation guard and idempotent missing-record acknowledgement make replay converge without reviving a completed call. Only a matching session-scoped `notifications/cancelled` request removes the pending indexes and sends best-effort cancellation to a connected daemon. Local completion that races with explicit cancellation is discarded. On every readiness handover, the Worker first sends an authoritative bounded `resume_calls` set; the runtime cancels active calls and queued results absent from that set before accepting `ready_ack`. A request explicitly cancelled while disconnected therefore cannot be revived by a fast reconnect.
|
|
181
|
+
14. A tool deadline cancels only that operation and never infers daemon death from tool duration. The independent daemon-liveness alarm owns socket invalidation. If same-instance readiness does not return before the grace deadline, the Worker rejects the detached request and the local runtime cancels ordinary calls, terminates their process trees, and discards queued results. A newly started daemon has a different instance identifier and cannot inherit prior calls.
|
|
182
182
|
15. `start_job` is different: after durable acceptance, the detached runner is no longer bound to the relay call or socket. Later cancellation uses `cancel_job` or the local CLI.
|
|
183
183
|
|
|
184
184
|
Duplicate in-flight JSON-RPC IDs are rejected only within the same authenticated MCP session. The request key includes OAuth token identity, the HMAC-bound MCP session, JSON-RPC id type, and id value, so separate initialized clients may safely reuse the same numeric id.
|
package/docs/AUDIT.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Security and privacy audit notes
|
|
2
2
|
|
|
3
|
+
## 2026-07-28 version 3.0.0-beta.22 ChatGPT interruption and full repository audit
|
|
4
|
+
|
|
5
|
+
The visible symptom combined three different mechanisms that must not be collapsed into one diagnosis. First, historical 11–34 minute relay gaps aligned with macOS clamshell sleep and DarkWake intervals; those records are expected machine suspension, not evidence of an active-use Cloudflare or daemon failure. Second, a controlled foreground command requested 120 seconds but the hosted tool path abandoned it at approximately 100 seconds. Third, the live Worker retained durable `exec_command` records after the local runtime had already completed and removed the corresponding operation. The latter two mechanisms explain active-use ChatGPT interruption and ghost pending-call growth.
|
|
6
|
+
|
|
7
|
+
The terminal-delivery protocol had a concrete loss window. The local runtime deleted a completed result as soon as `WebSocket.send()` accepted the frame, although that call proves only local queueing. If the connection ended before the Worker committed its generation-guarded terminal storage transaction, the runtime had neither an active operation nor a retained result to replay, while the Worker still owned the durable call. Beta.22 adds `tool_result_ack`: the runtime retains and periodically replays terminal results until acknowledgement; the Worker acknowledges only a transient settlement, a committed durable settlement, or an idempotent already-terminal duplicate. A result from a stale WebSocket generation is never acknowledged. Durable persistence failure now propagates instead of logging and falsely incrementing completion.
|
|
8
|
+
|
|
9
|
+
The timeout path also crossed responsibility boundaries. A single call timeout used a 45-second socket-silence heuristic to invalidate the complete daemon connection even though daemon liveness already has an independent 90-second alarm and readiness state machine. A slow command could therefore interrupt unrelated reads and make ChatGPT lose every local surface. Beta.22 removes socket invalidation from both transient and durable call expiry; timeout sends best-effort per-call cancellation only. The liveness alarm remains the sole owner of connection invalidation. Because the hosted foreground tool path was observed to stop near 100 seconds, configurable remote operations are capped at 85 seconds plus five seconds for terminal delivery. Longer work belongs in `start_process`/`read_process` or managed jobs, subject to what the MCP host exposes.
|
|
10
|
+
|
|
11
|
+
The broader review found a separate operations defect not named in the report: active daemon logs were safely trimmed before startup but could grow without another bound during a very long service lifetime. Beta.22 reuses the existing secure in-place trim every 15 minutes. The maintenance path preserves regular-file, no-follow, single-link, owner-only mode, schema, UTF-8, and line-boundary checks; failures log only a coarse class. Static review found no new secret-bearing operational logs, raw command/result logging, unbounded Worker body, public control-plane path, or obsolete transient settlement branch. Intentional migration fixtures remain confined to tests and explicit state upgrade code.
|
|
12
|
+
|
|
13
|
+
Fault-directed tests cover connected and reconnect result retention, lost acknowledgement heartbeat replay, Worker acknowledgement identity, persistent terminal-write failure, stale-generation non-acknowledgement, host-safe timeout bounds, log-maintenance scheduling/failure containment, and the previous cancellation/reconnect matrix. The repository architecture line budget rejected an oversized first draft of the recovery module; the implementation was reduced below the existing boundary rather than weakening the gate.
|
|
14
|
+
|
|
3
15
|
## 2026-07-27 version 3.0.0-beta.21 relay-continuity and repository audit
|
|
4
16
|
|
|
5
17
|
The reported symptom was a temporary loss of every local command surface, including `pwd`, followed by automatic daemon reconnection. Process evidence rejects the premise that the local daemon crashed: launchd retained one daemon PID, one start time, and `runs=1` throughout the observed interval. The failing layer was the relay/Worker connection and result-delivery path. Historical service logs also contain abnormal WebSocket closure and heartbeat-outage intervals that later recovered without a daemon restart. The exact network origin of each `1006` remains unknowable from those logs alone; a system VPN/TUN, proxy route, intermediary, or edge connection can all produce the same transport-level symptom.
|
package/docs/LOGGING.md
CHANGED
|
@@ -135,7 +135,7 @@ logs/daemon.out.log
|
|
|
135
135
|
logs/daemon.err.log
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
Existing files are opened without following symbolic links where supported and tail-trimmed on UTF-8/line boundaries before startup. Background services use `warn`, so ordinary tool traffic and brief relay interruptions do not cause sustained growth.
|
|
138
|
+
Existing files are opened without following symbolic links where supported and tail-trimmed on UTF-8/line boundaries before startup. The active background daemon repeats the same secure trim every 15 minutes, so a long-lived process remains bounded even under repeated warning-level failures. Maintenance errors expose only a coarse error class. Background services use `warn`, so ordinary tool traffic and brief relay interruptions do not cause sustained growth.
|
|
139
139
|
|
|
140
140
|
The log format has an explicit schema marker. If the marker differs from the current format, the daemon clears the active files before startup and writes the current marker. Runtime code recognizes only `daemon.out.log` and `daemon.err.log`; it does not parse or archive other log formats.
|
|
141
141
|
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -65,6 +65,8 @@ A Worker-requested `daemon_transport_error` or `daemon_liveness_timeout` is a re
|
|
|
65
65
|
|
|
66
66
|
For Streamable HTTP clients such as ChatGPT that advertise `text/event-stream`, the outer Worker returns an immediate sequence-zero SSE event identifier and a keepalive comment every ten seconds until the terminal sequence-one JSON-RPC result. `BridgeRoom` never owns the long-lived public stream or an unresolved terminal Promise. Stream initiation commits recovery state and then durably attaches the opaque call ID, daemon instance, WebSocket generation, request key, deadlines, and bounded transform metadata before sending work and returning a descriptor. A later WebSocket result, explicit cancellation, operation timeout, send failure, or reconnect-grace expiry writes the terminal result through one generation-checked transaction. One internal hibernatable WebSocket subscription therefore coexists with concurrent `server_info`, recovery, and session-scoped `notifications/cancelled` requests while SSE remains open, without creating a request per poll interval. Caller-supplied internal stream headers are removed at the public boundary. If the client or an intermediary closes the stream, Machine Bridge keeps the bounded operation alive; only `notifications/cancelled` carries cancellation semantics. A compatible host resumes the original stream with authenticated `GET /mcp`, the original `MCP-Session-Id`, and `Last-Event-ID`; it must not repeat the POST. Recovery records are token/session-bound, retained for at most two minutes, limited to 64 streams, and persist at most 1.5 MiB of terminal JSON. Error `-32002` means the online result exceeded the replay budget. Error `-32003` is reserved for a pending stream record that has no durable call owner after restart; a valid persisted call remains pending and recoverable. Error `-32005` means stored replay data failed integrity validation.
|
|
67
67
|
|
|
68
|
+
The terminal-result protocol is at-least-once from daemon to Worker and exactly-once at the generation-guarded terminal store. The daemon retains a completed result until `tool_result_ack`, replays it after reconnect and on heartbeat, and reconciles it against the authoritative `resume_calls` set. A missing acknowledgement therefore causes bounded duplicate delivery, not a ghost call.
|
|
69
|
+
|
|
68
70
|
`server_info.worker.observability.calls.unmatched_results` counts results that reached the Worker after their pending record was already removed. A small increase can accompany cancellation or timeout races, especially during mixed-version upgrade convergence; sustained growth together with old pending calls indicates incompatible components or a lifecycle defect. The counter contains no tool arguments or result data.
|
|
69
71
|
|
|
70
72
|
### MCP host or connector internal-storage errors
|
|
@@ -81,6 +83,10 @@ A brief relay interruption is retried automatically and is visible only with `--
|
|
|
81
83
|
|
|
82
84
|
Use `--verbose` only when close codes, close reasons, heartbeat timeouts, and retry delays are needed for diagnosis. A close code of 1006 means the transport ended without a normal close handshake; it does not by itself identify the cause.
|
|
83
85
|
|
|
86
|
+
Compare outage intervals with operating-system sleep/wake records before classifying them as active network failures. A suspended laptop cannot maintain the relay heartbeat, and a long outage that exactly spans clamshell sleep or DarkWake is expected suspension evidence. It does not justify credential rotation, Worker redeployment, or daemon replacement.
|
|
87
|
+
|
|
88
|
+
A foreground MCP tool is not a durable job. Hosted clients may impose a request ceiling below the local tool schema; ChatGPT was observed abandoning a foreground call near 100 seconds. Machine Bridge therefore terminates remote foreground execution by 90 seconds including delivery overhead. Use `start_process` plus bounded `read_process`, or a managed job, for work that can exceed 85 seconds. If the host exposes only `exec_command`, split work into independently terminal steps rather than requesting a larger timeout.
|
|
89
|
+
|
|
84
90
|
The daemon honors `HTTPS_PROXY`/`HTTP_PROXY` and `NO_PROXY` through standard environment-proxy resolution for remote Worker health and relay traffic. `wss:` targets use HTTPS proxy selection and `ws:` targets use HTTP proxy selection. Only HTTP and HTTPS proxy URLs are accepted. Invalid URLs or unsupported protocols fail startup with corrective guidance instead of entering the reconnect loop. `server_info.runtime.relay.network_route` reports `system-network-stack`, `application-http-proxy`, or `invalid-application-proxy-configuration`. This field describes only Machine Bridge application-level proxy selection: an operating-system VPN/TUN may still intercept `system-network-stack` traffic. `network_route_scope`, outage timestamps/durations, close category/code, transport error class, and next retry timing make that distinction explicit; proxy endpoints and credentials are never returned or logged. The browser-broker CLI health probe is a separate loopback-only path: it accepts only canonical `127.0.0.1`, uses direct Node HTTP with no proxy agent, and does not depend on `NO_PROXY`.
|
|
85
91
|
|
|
86
92
|
## Browser extension setup and diagnosis
|
|
@@ -182,7 +188,7 @@ Windows Task Scheduler limits the `/TR` action text, so the platform adapter wri
|
|
|
182
188
|
|
|
183
189
|
The Windows trigger is current-user `ONLOGON` with `LIMITED` run level. After a reboot, signing in to that user is sufficient; no terminal command is required. Pre-login operation is intentionally not provided by the default design because it would require a different service-account/credential boundary. Remote autostart definitions prefer a stable PATH alias that resolves to the currently running Node executable and persist a sanitized absolute-only service `PATH` containing the current Node/package directories, the operator's inherited absolute PATH entries, and platform defaults. When installation runs through npm, all nested run-script prefixes through the final npm private `node-gyp-bin` marker are discarded; paths belonging to inactive candidate runtimes are also removed. This prevents prerelease activation from persisting source-repository shims or the prior runtime that activation subsequently prunes, while an ordinary user-supplied `node_modules/.bin` remains valid. A private allowlisted `service-environment.json` preserves `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, matching lowercase forms, optional `ALL_PROXY`, Node proxy selection, and custom-CA path variables. Existing saved values survive an environment-free reinstall, while explicitly supplied values replace case-insensitive prior variants. Values may include proxy credentials, so the file stays in owner-only state and is never returned or logged; status exposes key names only. Re-run `machine-mcp service install` after changing Node installation families, PATH layout, proxy, or CA configuration. Custom Windows state paths used for autostart must also remain within the Task Scheduler path limit and must not contain a literal `%`.
|
|
184
190
|
|
|
185
|
-
A service-style `--daemon-only` start that finds the same workspace daemon already running is an idempotent no-op: it exits successfully without repeating warnings or readiness output; explicit policy/secret/change requests still report that changes were not applied. Autostart logs are stored under the state root in `logs/daemon.out.log` and `logs/daemon.err.log`. Installed services pass `--log-level warn --log-format json`, so each active line is a bounded JSON event suitable for ingestion. Files are owner-only where supported and tail-trimmed before daemon startup. If the log schema marker does not match the current format, the active files are cleared before startup and the current marker is written. Runtime code reads and maintains only the active filenames.
|
|
191
|
+
A service-style `--daemon-only` start that finds the same workspace daemon already running is an idempotent no-op: it exits successfully without repeating warnings or readiness output; explicit policy/secret/change requests still report that changes were not applied. Autostart logs are stored under the state root in `logs/daemon.out.log` and `logs/daemon.err.log`. Installed services pass `--log-level warn --log-format json`, so each active line is a bounded JSON event suitable for ingestion. Files are owner-only where supported and tail-trimmed before daemon startup and every 15 minutes while the background daemon remains active. Runtime maintenance reuses the same no-follow, regular-file, single-link, `0600`, schema, UTF-8, and line-boundary checks. If the log schema marker does not match the current format, the active files are cleared before startup and the current marker is written. Runtime code reads and maintains only the active filenames.
|
|
186
192
|
|
|
187
193
|
Logging is level-based:
|
|
188
194
|
|
|
@@ -280,7 +286,7 @@ Defense-in-depth limits include:
|
|
|
280
286
|
- process stdin write: 64 KiB per call;
|
|
281
287
|
- local simultaneous tool calls: 16;
|
|
282
288
|
- Worker pending daemon calls: 32;
|
|
283
|
-
- command timeout: 1–600 seconds;
|
|
289
|
+
- local command timeout schema: 1–600 seconds; remote foreground relay execution is capped at 85 seconds plus five seconds of terminal-delivery overhead;
|
|
284
290
|
- process-session read wait: at most 30 seconds, measured with monotonic elapsed time;
|
|
285
291
|
- direct directory result: 10,000 entries and 4 MiB of path metadata;
|
|
286
292
|
- recursive walk: 200,000 visited entries;
|
package/docs/TESTING.md
CHANGED
|
@@ -161,6 +161,6 @@ The stdio integration test also sends an oversized line, verifies bounded reject
|
|
|
161
161
|
|
|
162
162
|
`npm run mcp-resumption:test` directly exercises stream cursor parsing, OAuth-token/MCP-session isolation, immediate pending/terminal polls, active and completed replay, orphaned-stream restart ambiguity, persisted-call restart recovery, strict call-record validation, request-key uniqueness, operation/reconnect deadlines, repeated detach/rebind retention extension, stale-generation rejection, prototype-safe aggregation, exactly-once completion, result-size fallback, SHA-256 tamper detection, transient persistence failure, expiry, capacity, completed-record eviction, the four-row plain-stream budget, and the fixed six-row durable-call lifecycle budget.
|
|
163
163
|
|
|
164
|
-
`npm run worker-runtime-infrastructure:test` verifies outer-Worker stream ownership, stripping of caller-supplied internal headers, bounded descriptor/subscribe adaptation, fixed request budgets, bounded transient subscription retries, outer metadata/404/invalid-method bypass, stateful burst limiting, structured and duplicate-suppressed gateway failures, coalesced alarm writes, sequence-zero/sequence-one framing, subscription-error closure, subscriber replacement, non-daemon socket isolation, and the shared two-minute/64-stream/1.5-MiB contract. It models the production durable-call boundary: initiation persists ownership without retaining a terminal Promise, later success or daemon rejection settles once, send failure cleans the record, capacity fails closed, and same-instance reconnect moves ownership to a new generation while rejecting the stale one. Deadline tests prove both JSON-only timer/sweep behavior and persisted-call alarm expiry without leaking request keys. A direct runtime-alarm coordinator test verifies the earliest transient-or-durable deadline, alarm removal when no deadline remains, event-entry expiry before rescheduling, and bounded reporting when Durable Object alarm storage fails. The same suite also proves that direct same-instance handover preserves the remaining timeout budget. `npm run worker:integration-test` performs the real Wrangler path: the role-filtered catalog remains stable before, during, and after daemon availability changes; execution still fails closed without a ready daemon; an open SSE stream coexists with concurrent `server_info` and explicit cancellation; verified same-instance replacement transfers an in-flight call before incumbent close; disconnect/recovery remains token/session isolated; and sequence-one acknowledgement is not delivered twice. Managed-job integration treats `__proto__`, `constructor`, `toString`, and `valueOf` environment/resource-map keys as ordinary own data while retaining duplicate-key rejection. Static architecture checks forbid a stream-initiation `dispatchJsonRpc` Promise, `resumption.attach`, Durable Object `waitUntil`, Promise-valued recovery state, or return of the obsolete transient `registerEvent` branch. The parser accumulates complete SSE events and does not assume network chunk boundaries. CORS coverage requires both `DPoP` and `Last-Event-ID`.
|
|
164
|
+
`npm run worker-runtime-infrastructure:test` verifies outer-Worker stream ownership, stripping of caller-supplied internal headers, bounded descriptor/subscribe adaptation, fixed request budgets, bounded transient subscription retries, outer metadata/404/invalid-method bypass, stateful burst limiting, structured and duplicate-suppressed gateway failures, coalesced alarm writes, sequence-zero/sequence-one framing, subscription-error closure, subscriber replacement, non-daemon socket isolation, and the shared two-minute/64-stream/1.5-MiB contract. It models the production durable-call boundary: initiation persists ownership without retaining a terminal Promise, later success or daemon rejection settles once, send failure cleans the record, capacity fails closed, and same-instance reconnect moves ownership to a new generation while rejecting the stale one. Deadline tests prove both JSON-only timer/sweep behavior and persisted-call alarm expiry without leaking request keys. A direct runtime-alarm coordinator test verifies the earliest transient-or-durable deadline, alarm removal when no deadline remains, event-entry expiry before rescheduling, and bounded reporting when Durable Object alarm storage fails. The same suite also proves that direct same-instance handover preserves the remaining timeout budget. It additionally proves terminal-result retention until Worker acknowledgement, heartbeat replay after acknowledgement loss, stale-generation non-acknowledgement, propagation of durable terminal-write failure without false completion, and the 85-second interactive execution plus five-second delivery budget. Local infrastructure coverage verifies periodic secure autostart-log trimming and contains maintenance failure without leaking exception text. `npm run worker:integration-test` performs the real Wrangler path: the role-filtered catalog remains stable before, during, and after daemon availability changes; execution still fails closed without a ready daemon; an open SSE stream coexists with concurrent `server_info` and explicit cancellation; verified same-instance replacement transfers an in-flight call before incumbent close; disconnect/recovery remains token/session isolated; and sequence-one acknowledgement is not delivered twice. Managed-job integration treats `__proto__`, `constructor`, `toString`, and `valueOf` environment/resource-map keys as ordinary own data while retaining duplicate-key rejection. Static architecture checks forbid a stream-initiation `dispatchJsonRpc` Promise, `resumption.attach`, Durable Object `waitUntil`, Promise-valued recovery state, or return of the obsolete transient `registerEvent` branch. The parser accumulates complete SSE events and does not assume network chunk boundaries. CORS coverage requires both `DPoP` and `Last-Event-ID`.
|
|
165
165
|
|
|
166
166
|
Beta.21 closeout requires a fresh pass of the complete 63-task fast plan and 91-task full plan after the final relay error-classification changes; an earlier green run cannot be reused as terminal evidence.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.22",
|
|
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",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { trimAutostartLogs } from "./service.mjs";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAINTENANCE_INTERVAL_MS = 15 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Keep long-lived background-service logs within the same bounds enforced at
|
|
9
|
+
* startup. The injected scheduler/trim hooks keep the lifecycle deterministic
|
|
10
|
+
* in tests without weakening the production file-safety checks.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} stateRoot
|
|
13
|
+
* @param {{
|
|
14
|
+
* intervalMs?: number,
|
|
15
|
+
* trim?: (stateRoot: string) => void,
|
|
16
|
+
* onError?: (error: unknown) => void,
|
|
17
|
+
* scheduler?: {setInterval: (callback: () => void, delay: number) => any},
|
|
18
|
+
* }} [options]
|
|
19
|
+
*/
|
|
20
|
+
export function startAutostartLogMaintenance(stateRoot, options = {}) {
|
|
21
|
+
const intervalMs = positiveInteger(options.intervalMs, DEFAULT_MAINTENANCE_INTERVAL_MS);
|
|
22
|
+
const trim = typeof options.trim === "function" ? options.trim : trimAutostartLogs;
|
|
23
|
+
const onError = typeof options.onError === "function" ? options.onError : () => {};
|
|
24
|
+
const scheduler = options.scheduler || { setInterval };
|
|
25
|
+
const maintain = () => {
|
|
26
|
+
try { trim(stateRoot); } catch (error) { onError(error); }
|
|
27
|
+
};
|
|
28
|
+
const timer = scheduler.setInterval(maintain, intervalMs);
|
|
29
|
+
timer?.unref?.();
|
|
30
|
+
return { intervalMs, maintain };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function positiveInteger(value, fallback) {
|
|
34
|
+
const number = Number(value);
|
|
35
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
36
|
+
}
|
package/src/local/cli.mjs
CHANGED
|
@@ -287,6 +287,16 @@ async function startRemoteRuntime({ args, workspace, state, daemonLock, logger }
|
|
|
287
287
|
runtime = createRemoteRuntime({ args, workspace, state, daemonLock, deviceSessionIdentity: readiness.deviceSessionIdentity });
|
|
288
288
|
await runtime.start();
|
|
289
289
|
reportRemoteReady(args, state, readiness, logger);
|
|
290
|
+
if (args.daemonOnly) {
|
|
291
|
+
const { startAutostartLogMaintenance } = await import("./autostart-log-maintenance.mjs");
|
|
292
|
+
startAutostartLogMaintenance(state.paths.stateRoot, {
|
|
293
|
+
onError(error) {
|
|
294
|
+
logger.event?.("warn", "service.log_maintenance.failed", {
|
|
295
|
+
error_class: classifyOperationalError(error),
|
|
296
|
+
}, "Background log maintenance failed");
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
290
300
|
keepProcessAlive({ daemon: runtime, lock: daemonLock, logger });
|
|
291
301
|
} catch (error) {
|
|
292
302
|
try { runtime?.stop?.(); } catch {}
|
|
@@ -43,28 +43,36 @@ export class RelayCallRecovery {
|
|
|
43
43
|
/** @param {RelayResult} response */
|
|
44
44
|
deliver(response) {
|
|
45
45
|
const callId = String(response?.id || "");
|
|
46
|
-
if (this.send(response)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.pendingResults.set(callId, response);
|
|
52
|
-
this.scheduleExpiry();
|
|
53
|
-
this.logger.event?.("debug", "relay.tool_result.queued", {
|
|
54
|
-
call_id: shortCallId(callId), queued_results: this.pendingResults.size,
|
|
55
|
-
}, "Queued a completed tool result while the relay reconnects");
|
|
46
|
+
if (!callId) return this.send(response);
|
|
47
|
+
if (!this.isRecoverable()) {
|
|
48
|
+
this.logger.event?.("debug", "relay.tool_result.discarded", {
|
|
49
|
+
call_id: shortCallId(callId), reason: "transport_unavailable",
|
|
50
|
+
}, "Discarded a tool result because the relay is no longer recoverable");
|
|
56
51
|
return false;
|
|
57
52
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
|
|
54
|
+
// Retain sent results until the Worker commits and acknowledges them.
|
|
55
|
+
this.pendingResults.set(callId, response);
|
|
56
|
+
const sent = this.send(response);
|
|
57
|
+
if (sent) {
|
|
58
|
+
this.logger.event?.("debug", "relay.tool_result.awaiting_ack", {
|
|
59
|
+
call_id: shortCallId(callId), unacknowledged_results: this.pendingResults.size,
|
|
60
|
+
}, "Delivered a tool result and retained it until Worker acknowledgement");
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.scheduleExpiry();
|
|
65
|
+
this.logger.event?.("debug", "relay.tool_result.queued", {
|
|
66
|
+
call_id: shortCallId(callId), queued_results: this.pendingResults.size,
|
|
67
|
+
}, "Queued a completed tool result while the relay reconnects");
|
|
61
68
|
return false;
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
/** @param {unknown} callId */
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
72
|
+
acknowledge(callId) { return this.pendingResults.delete(String(callId)); }
|
|
73
|
+
|
|
74
|
+
/** @param {unknown} callId */
|
|
75
|
+
discard(callId) { return this.pendingResults.delete(String(callId)); }
|
|
68
76
|
|
|
69
77
|
/** @param {Iterable<string>} resumedCallIds @param {(callId: string) => boolean} cancelCall */
|
|
70
78
|
reconcile(resumedCallIds, cancelCall) {
|
|
@@ -94,18 +102,29 @@ export class RelayCallRecovery {
|
|
|
94
102
|
|
|
95
103
|
ready() {
|
|
96
104
|
this.clearTimer();
|
|
105
|
+
this.retryUnacknowledged("reconnected");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pulse() {
|
|
109
|
+
this.retryUnacknowledged("heartbeat");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @param {string} reason */
|
|
113
|
+
retryUnacknowledged(reason) {
|
|
97
114
|
let delivered = 0;
|
|
98
|
-
for (const
|
|
115
|
+
for (const response of this.pendingResults.values()) {
|
|
99
116
|
if (!this.send(response)) {
|
|
100
117
|
this.scheduleExpiry();
|
|
101
118
|
break;
|
|
102
119
|
}
|
|
103
|
-
this.pendingResults.delete(callId);
|
|
104
120
|
delivered += 1;
|
|
105
121
|
}
|
|
106
122
|
if (delivered > 0) {
|
|
107
|
-
this.logger.event?.("info", "relay.tool_results.replayed", {
|
|
108
|
-
|
|
123
|
+
this.logger.event?.(reason === "reconnected" ? "info" : "debug", "relay.tool_results.replayed", {
|
|
124
|
+
delivered_results: delivered,
|
|
125
|
+
reason,
|
|
126
|
+
unacknowledged_results: this.pendingResults.size,
|
|
127
|
+
}, "Replayed completed tool results awaiting Worker acknowledgement");
|
|
109
128
|
}
|
|
110
129
|
}
|
|
111
130
|
|
|
@@ -122,7 +141,7 @@ export class RelayCallRecovery {
|
|
|
122
141
|
const cancelled = this.cancelOrigin("remote relay reconnect grace expired");
|
|
123
142
|
const discarded = this.pendingResults.size;
|
|
124
143
|
this.pendingResults.clear();
|
|
125
|
-
this.terminate();
|
|
144
|
+
if (cancelled > 0) this.terminate();
|
|
126
145
|
if (cancelled > 0 || discarded > 0) {
|
|
127
146
|
this.logger.warn?.(`remote relay did not recover within ${this.graceMs / 1000} seconds; cancelled ${cancelled} call(s) and discarded ${discarded} queued result(s)`);
|
|
128
147
|
}
|
package/src/local/runtime.mjs
CHANGED
|
@@ -334,7 +334,20 @@ export class LocalRuntime {
|
|
|
334
334
|
this.relayResumeSessionId = 0;
|
|
335
335
|
return true;
|
|
336
336
|
}
|
|
337
|
-
if (message.type === "pong")
|
|
337
|
+
if (message.type === "pong") {
|
|
338
|
+
this.relayCallRecovery.pulse();
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
if (message.type === "tool_result_ack") {
|
|
342
|
+
if (!isRelayReadyContext(relayContext, this.relay)
|
|
343
|
+
|| typeof message.id !== "string"
|
|
344
|
+
|| !/^call_[A-Za-z0-9_-]{8,240}$/.test(message.id)) {
|
|
345
|
+
this.handleRelayProtocolViolation("invalid_tool_result_ack");
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
this.relayCallRecovery.acknowledge(message.id);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
338
351
|
if (message.type === "error") {
|
|
339
352
|
this.relay?.handleServerError(message);
|
|
340
353
|
return true;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { daemonLastSeenMs } from "./daemon-liveness.ts";
|
|
2
1
|
import type { DaemonSocketRegistry } from "./daemon-sockets.ts";
|
|
3
2
|
import { publicWorkerToolError, WorkerToolError } from "./errors.ts";
|
|
4
3
|
import { workerErrorClass } from "./http.ts";
|
|
@@ -9,13 +8,6 @@ import { transformDurableStreamOutcome } from "./durable-stream-result.ts";
|
|
|
9
8
|
import type { WorkerObservability } from "./observability.ts";
|
|
10
9
|
import { sendWebSocketQuietly } from "./websocket-protocol.ts";
|
|
11
10
|
|
|
12
|
-
type InvalidateDaemonSocket = (
|
|
13
|
-
socket: WebSocket,
|
|
14
|
-
message: string,
|
|
15
|
-
closeReason: string,
|
|
16
|
-
errorCode?: string,
|
|
17
|
-
) => Promise<void>;
|
|
18
|
-
|
|
19
11
|
type TransientPendingSnapshot = {
|
|
20
12
|
active: number;
|
|
21
13
|
detached: number;
|
|
@@ -29,20 +21,17 @@ export class DurableStreamCallCoordinator {
|
|
|
29
21
|
private readonly daemonRegistry: DaemonSocketRegistry;
|
|
30
22
|
private readonly observability: WorkerObservability;
|
|
31
23
|
private readonly maximumPendingCalls: number;
|
|
32
|
-
private readonly invalidateDaemonSocket: InvalidateDaemonSocket;
|
|
33
24
|
|
|
34
25
|
constructor(
|
|
35
26
|
calls: McpPendingCallStore,
|
|
36
27
|
daemonRegistry: DaemonSocketRegistry,
|
|
37
28
|
observability: WorkerObservability,
|
|
38
29
|
maximumPendingCalls: number,
|
|
39
|
-
invalidateDaemonSocket: InvalidateDaemonSocket,
|
|
40
30
|
) {
|
|
41
31
|
this.calls = calls;
|
|
42
32
|
this.daemonRegistry = daemonRegistry;
|
|
43
33
|
this.observability = observability;
|
|
44
34
|
this.maximumPendingCalls = maximumPendingCalls;
|
|
45
|
-
this.invalidateDaemonSocket = invalidateDaemonSocket;
|
|
46
35
|
}
|
|
47
36
|
|
|
48
37
|
async snapshot(transient: TransientPendingSnapshot): Promise<Record<string, unknown>> {
|
|
@@ -66,9 +55,10 @@ export class DurableStreamCallCoordinator {
|
|
|
66
55
|
connectionId: string,
|
|
67
56
|
outcome: PendingCallOutcome,
|
|
68
57
|
knownCall?: PendingStreamCallView,
|
|
69
|
-
): Promise<
|
|
58
|
+
): Promise<"committed" | "missing" | "stale"> {
|
|
70
59
|
const call = knownCall ?? await this.calls.get(callId);
|
|
71
|
-
if (!call
|
|
60
|
+
if (!call) return "missing";
|
|
61
|
+
if (call.connection_id !== connectionId) return "stale";
|
|
72
62
|
const normalized = transformDurableStreamOutcome(call, outcome);
|
|
73
63
|
const code = normalized.ok ? "" : publicWorkerToolError(normalized.error).code;
|
|
74
64
|
try {
|
|
@@ -77,23 +67,21 @@ export class DurableStreamCallCoordinator {
|
|
|
77
67
|
connectionId,
|
|
78
68
|
streamTerminalMessage(call.requestId, normalized),
|
|
79
69
|
);
|
|
80
|
-
if (!completed)
|
|
70
|
+
if (!completed) {
|
|
71
|
+
const current = await this.calls.get(callId);
|
|
72
|
+
return current ? "stale" : "missing";
|
|
73
|
+
}
|
|
81
74
|
} catch (error) {
|
|
82
75
|
this.observability.event("error", "mcp.stream.persist.failed", { error_class: workerErrorClass(error) });
|
|
76
|
+
throw error;
|
|
83
77
|
}
|
|
84
78
|
this.observability.callFinished(call.tool, code);
|
|
85
|
-
return
|
|
79
|
+
return "committed";
|
|
86
80
|
}
|
|
87
81
|
|
|
88
82
|
async expire(call: PendingStreamCallView): Promise<void> {
|
|
89
83
|
const socket = call.state === "attached" ? this.daemonRegistry.socketForConnectionId(call.connection_id) : undefined;
|
|
90
|
-
if (socket) {
|
|
91
|
-
sendWebSocketQuietly(socket, { type: "cancel_call", id: call.call_id });
|
|
92
|
-
const silentForMs = Date.now() - daemonLastSeenMs(this.daemonRegistry.readyAttachment(socket));
|
|
93
|
-
if (!Number.isFinite(silentForMs) || silentForMs > 45_000) {
|
|
94
|
-
void this.invalidateDaemonSocket(socket, "daemon became unresponsive", "daemon liveness timeout");
|
|
95
|
-
}
|
|
96
|
-
}
|
|
84
|
+
if (socket) sendWebSocketQuietly(socket, { type: "cancel_call", id: call.call_id });
|
|
97
85
|
const error = call.state === "attached"
|
|
98
86
|
? new WorkerToolError("timeout", `daemon tool timed out: ${call.tool}`, true)
|
|
99
87
|
: new WorkerToolError("unavailable", "daemon disconnected; reconnect grace expired", true);
|
|
@@ -111,12 +99,12 @@ export class DurableStreamCallCoordinator {
|
|
|
111
99
|
if (!call) return false;
|
|
112
100
|
const socket = call.state === "attached" ? this.daemonRegistry.socketForConnectionId(call.connection_id) : undefined;
|
|
113
101
|
if (socket) sendWebSocketQuietly(socket, { type: "cancel_call", id: call.call_id });
|
|
114
|
-
return this.settle(
|
|
102
|
+
return (await this.settle(
|
|
115
103
|
call.call_id,
|
|
116
104
|
call.connection_id,
|
|
117
105
|
{ ok: false, error: new WorkerToolError("cancelled", "tool call cancelled by client") },
|
|
118
106
|
call,
|
|
119
|
-
);
|
|
107
|
+
)) === "committed";
|
|
120
108
|
}
|
|
121
109
|
|
|
122
110
|
detach(connectionId: string, graceMs: number): Promise<number> {
|
package/src/worker/index.ts
CHANGED
|
@@ -7,7 +7,6 @@ import type { PendingCallOutcome } from "./pending-call-contract.ts";
|
|
|
7
7
|
import {
|
|
8
8
|
DAEMON_LIVENESS_TIMEOUT_MS,
|
|
9
9
|
DAEMON_READY_TIMEOUT_MS,
|
|
10
|
-
daemonLastSeenMs,
|
|
11
10
|
daemonLivenessDeadlineMs,
|
|
12
11
|
isFreshDaemonCandidate,
|
|
13
12
|
} from "./daemon-liveness.ts";
|
|
@@ -50,7 +49,7 @@ import {
|
|
|
50
49
|
} from "./websocket-protocol.ts";
|
|
51
50
|
|
|
52
51
|
const SERVER_NAME = String(serverMetadata.name);
|
|
53
|
-
const SERVER_VERSION = "3.0.0-beta.
|
|
52
|
+
const SERVER_VERSION = "3.0.0-beta.22";
|
|
54
53
|
const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
|
|
55
54
|
const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
|
|
56
55
|
const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
@@ -99,7 +98,6 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
99
98
|
this.daemonRegistry,
|
|
100
99
|
this.observability,
|
|
101
100
|
MAX_PENDING_CALLS,
|
|
102
|
-
(socket, message, closeReason, errorCode) => this.invalidateDaemonSocket(socket, message, closeReason, errorCode),
|
|
103
101
|
);
|
|
104
102
|
}
|
|
105
103
|
|
|
@@ -343,9 +341,13 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
343
341
|
let matched = outcome.ok
|
|
344
342
|
? await this.pending.resolve(body.id, ws, outcome.value)
|
|
345
343
|
: await this.pending.reject(body.id, outcome.error, ws);
|
|
344
|
+
let acknowledge = Boolean(matched);
|
|
346
345
|
if (!matched && socketAttachment.connectionId) {
|
|
347
|
-
|
|
346
|
+
const settlement = await this.durableCalls.settle(body.id, socketAttachment.connectionId, outcome);
|
|
347
|
+
matched = settlement === "committed";
|
|
348
|
+
acknowledge = settlement !== "stale";
|
|
348
349
|
}
|
|
350
|
+
if (acknowledge) trySendWebSocket(ws, { type: "tool_result_ack", id: body.id });
|
|
349
351
|
if (!matched) this.observability.unmatchedResult();
|
|
350
352
|
else await this.scheduleRuntimeAlarm();
|
|
351
353
|
}
|
|
@@ -630,10 +632,6 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
630
632
|
}
|
|
631
633
|
private daemonCallTimeout(record: import("./pending-call-contract.ts").PendingCallRecord, name: string): Error {
|
|
632
634
|
if (record.socket) sendWebSocketQuietly(record.socket, { type: "cancel_call", id: record.id });
|
|
633
|
-
const silentForMs = record.socket ? Date.now() - daemonLastSeenMs(this.daemonRegistry.readyAttachment(record.socket)) : 0;
|
|
634
|
-
if (record.socket && (!Number.isFinite(silentForMs) || silentForMs > 45_000)) {
|
|
635
|
-
void this.invalidateDaemonSocket(record.socket, "daemon became unresponsive", "daemon liveness timeout");
|
|
636
|
-
}
|
|
637
635
|
return new WorkerToolError("timeout", `daemon tool timed out: ${name}`, true);
|
|
638
636
|
}
|
|
639
637
|
|
|
@@ -10,7 +10,11 @@ export function daemonToolTimeoutMs(name: string, args: Record<string, unknown>)
|
|
|
10
10
|
]);
|
|
11
11
|
if (!configurable.has(name)) return 60_000;
|
|
12
12
|
const seconds = clampNumber(args.timeout_seconds, name === "browser_fill_form" ? 60 : 120, 1, 600);
|
|
13
|
-
const executionMs = Math.min(
|
|
13
|
+
const executionMs = Math.min(
|
|
14
|
+
seconds * 1000,
|
|
15
|
+
relayContract.maximumExecutionTimeoutMs,
|
|
16
|
+
relayContract.maximumInteractiveExecutionTimeoutMs,
|
|
17
|
+
);
|
|
14
18
|
return Math.min(executionMs + relayContract.toolCallOverheadMs, relayContract.maximumRelayToolTimeoutMs);
|
|
15
19
|
}
|
|
16
20
|
|