machine-bridge-mcp 1.2.2 → 1.2.6

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.
@@ -1,23 +1,33 @@
1
1
  import { DurableObject } from "cloudflare:workers";
2
2
  import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
3
3
  import { PendingCallRegistrationError, PendingCallRegistry } from "./pending-calls.ts";
4
+ import {
5
+ DAEMON_HELLO_TIMEOUT_MS,
6
+ DAEMON_LIVENESS_TIMEOUT_MS,
7
+ DAEMON_READY_TIMEOUT_MS,
8
+ daemonLastSeenMs,
9
+ daemonLivenessDeadlineMs,
10
+ daemonReadyDeadlineMs,
11
+ isFreshDaemonCandidate,
12
+ } from "./daemon-liveness.ts";
13
+ import { DaemonSocketRegistry } from "./daemon-sockets.ts";
4
14
  import { mcpClientRequestKey, resolveMcpSession } from "./mcp-session.ts";
5
15
  import { daemonToolTimeoutMs } from "./tool-timeout.ts";
6
16
  import { WorkerObservability } from "./observability.ts";
7
17
  import { daemonToolError, publicWorkerToolError, WorkerToolError } from "./errors.ts";
8
- import { sanitizeDaemonPolicy, sanitizeDaemonTools, type DaemonPolicy } from "./policy.ts";
18
+ import { sanitizeDaemonPolicy, sanitizeDaemonTools } from "./policy.ts";
9
19
  import { accountRoleAllowsTool, accountRoleToolNames, type AccountRole } from "./access.ts";
10
20
  import { OAuthController, type AuthorizedToken, type OAuthControllerEnv } from "./oauth-controller.ts";
11
21
  import { accountAuthoritySnapshot, decorateProjectOverview, describeDaemonCeiling } from "./authority.ts";
12
22
  import { serverInfoTool, workspaceTools } from "./tool-catalog.ts";
13
23
  import { OFFLINE_ACCESS_SCOPE, randomToken, safeEqual } from "./oauth-state.ts";
14
24
  import {
15
- HttpError, applyCors, baseUrl, bearerToken, corsPreflight, json, methodNotAllowed, sanitizeMetadataText,
25
+ HttpError, applyCors, baseUrl, bearerToken, corsPreflight, json, methodNotAllowed,
16
26
  parseJsonRequest, workerErrorClass,
17
27
  } from "./http.ts";
18
28
 
19
29
  const SERVER_NAME = String(serverMetadata.name);
20
- const SERVER_VERSION = "1.2.2";
30
+ const SERVER_VERSION = "1.2.6";
21
31
  const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
22
32
  const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
23
33
  const JSONRPC_VERSION = "2.0";
@@ -25,7 +35,6 @@ const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
25
35
  const MAX_BODY_BYTES = 16 * 1024 * 1024;
26
36
  const MAX_PENDING_CALLS = 32;
27
37
  const MAX_DAEMON_MESSAGE_BYTES = 8 * 1024 * 1024;
28
- const DAEMON_HELLO_TIMEOUT_MS = 10_000;
29
38
 
30
39
  interface BridgeEnv extends OAuthControllerEnv {
31
40
  BRIDGE: DurableObjectNamespace<BridgeRoom>;
@@ -45,23 +54,18 @@ interface JsonRpcRequest {
45
54
  params?: unknown;
46
55
  }
47
56
 
48
- interface DaemonAttachment {
49
- role: "candidate" | "expired" | "daemon";
50
- connectedAt: string;
51
- policy?: DaemonPolicy;
52
- tools?: string[];
53
- }
54
-
55
57
  const MCP_INSTRUCTIONS = serverMetadata.instructions.map((value) => String(value)).join("\n");
56
58
 
57
59
  export class BridgeRoom extends DurableObject<BridgeEnv> {
58
60
  private readonly pending = new PendingCallRegistry(MAX_PENDING_CALLS);
59
61
  private readonly observability = new WorkerObservability();
60
62
  private readonly oauth: OAuthController;
63
+ private readonly daemonRegistry: DaemonSocketRegistry;
61
64
 
62
65
  constructor(ctx: DurableObjectState, env: BridgeEnv) {
63
66
  super(ctx, env);
64
67
  this.oauth = new OAuthController(ctx, env, SERVER_NAME);
68
+ this.daemonRegistry = new DaemonSocketRegistry(ctx);
65
69
  }
66
70
 
67
71
  async fetch(request: Request): Promise<Response> {
@@ -156,7 +160,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
156
160
  }
157
161
  const body = parsed;
158
162
 
159
- const socketAttachment = this.socketAttachment(ws);
163
+ const socketAttachment = this.daemonRegistry.attachment(ws);
160
164
  if (!socketAttachment) {
161
165
  closeWebSocketQuietly(ws, 1008, "missing daemon attachment");
162
166
  return;
@@ -168,58 +172,83 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
168
172
  }
169
173
 
170
174
  if (body.type === "hello") {
171
- if (socketAttachment.role === "daemon") {
175
+ if (socketAttachment.role !== "candidate") {
172
176
  rejectDaemonMessage(ws, "duplicate_hello", 1002, "duplicate daemon hello");
173
177
  return;
174
178
  }
175
- const previousDaemons = this.daemonSockets().filter((socket) => socket !== ws);
176
- if (socketAttachment.role === "candidate") {
177
- if (!isFreshDaemonCandidate(socketAttachment.connectedAt)) {
178
- closeWebSocketQuietly(ws, 1008, "stale daemon candidate");
179
- await this.scheduleCandidateAlarm();
180
- return;
181
- }
179
+ if (!isFreshDaemonCandidate(socketAttachment.connectedAt)) {
180
+ closeWebSocketQuietly(ws, 1008, "stale daemon candidate");
181
+ await this.scheduleSocketAlarms();
182
+ return;
182
183
  }
183
184
  const daemonPolicy = sanitizeDaemonPolicy(body.policy);
184
- ws.serializeAttachment({
185
- role: "daemon",
186
- connectedAt: new Date().toISOString(),
185
+ const authenticatedAt = new Date().toISOString();
186
+ const probeId = randomToken("probe");
187
+ this.daemonRegistry.beginProbe(ws, {
188
+ connectedAt: authenticatedAt,
189
+ probeId,
187
190
  policy: daemonPolicy,
188
191
  tools: sanitizeDaemonTools(body.tools, daemonPolicy),
189
- } satisfies DaemonAttachment);
192
+ });
190
193
  this.observability.socketAuthenticated();
191
- await this.scheduleCandidateAlarm();
192
194
  try {
193
195
  ws.send(JSON.stringify({ type: "hello_ack", server: SERVER_NAME, version: SERVER_VERSION }));
196
+ ws.send(JSON.stringify({ type: "relay_probe", id: probeId }));
194
197
  } catch {
195
- ws.serializeAttachment({
196
- role: "expired",
197
- connectedAt: socketAttachment.connectedAt,
198
- } satisfies DaemonAttachment);
199
- closeWebSocketQuietly(ws, 1011, "daemon hello acknowledgement failed");
198
+ this.daemonRegistry.expire(ws);
199
+ closeWebSocketQuietly(ws, 1011, "daemon readiness probe failed");
200
+ await this.scheduleSocketAlarms();
200
201
  return;
201
202
  }
202
- for (const socket of previousDaemons) {
203
- closeWebSocketQuietly(socket, 1012, "replaced by authenticated daemon");
204
- }
203
+ await this.scheduleSocketAlarms();
205
204
  return;
206
205
  }
207
206
 
208
- if (socketAttachment.role !== "daemon") {
207
+ if (socketAttachment.role === "candidate") {
209
208
  closeWebSocketQuietly(ws, 1008, "daemon hello required");
210
209
  return;
211
210
  }
212
211
 
213
212
  if (body.type === "heartbeat" || body.type === "ping") {
213
+ await this.touchDaemonSocket(ws);
214
214
  ws.send(JSON.stringify({ type: "pong", ts: body.ts ?? Date.now() }));
215
215
  return;
216
216
  }
217
217
 
218
+ if (socketAttachment.role === "probing") {
219
+ if (body.type !== "relay_probe_result" || typeof body.id !== "string" || body.id !== socketAttachment.probeId) {
220
+ rejectDaemonMessage(ws, "invalid_relay_probe_result", 1002, "invalid daemon readiness result");
221
+ return;
222
+ }
223
+ const readyAt = new Date().toISOString();
224
+ if (!this.daemonRegistry.promote(ws, readyAt)) {
225
+ rejectDaemonMessage(ws, "invalid_relay_readiness_state", 1002, "invalid daemon readiness state");
226
+ return;
227
+ }
228
+ try {
229
+ ws.send(JSON.stringify({ type: "ready_ack", server: SERVER_NAME, version: SERVER_VERSION }));
230
+ } catch {
231
+ this.invalidateDaemonSocket(ws, "daemon readiness acknowledgement failed", "daemon ready timeout", "daemon_ready_timeout");
232
+ return;
233
+ }
234
+ this.observability.socketReady();
235
+ for (const previous of this.daemonRegistry.readyRoleSockets().filter((socket) => socket !== ws)) {
236
+ closeWebSocketQuietly(previous, 1012, "replaced by verified daemon");
237
+ }
238
+ return;
239
+ }
240
+
241
+ if (socketAttachment.role !== "daemon") {
242
+ closeWebSocketQuietly(ws, 1008, "daemon readiness required");
243
+ return;
244
+ }
245
+
218
246
  if (body.type !== "tool_result" || typeof body.id !== "string") {
219
247
  rejectDaemonMessage(ws, "unknown_message_type", 1002, "unknown daemon message type");
220
248
  return;
221
249
  }
222
250
 
251
+ await this.touchDaemonSocket(ws);
223
252
  const matched = body.ok === false
224
253
  ? this.pending.reject(body.id, daemonToolError(body.error), ws)
225
254
  : this.pending.resolve(body.id, ws, body.result);
@@ -237,8 +266,8 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
237
266
 
238
267
  private async cleanupDaemonSocket(ws: WebSocket, message: string): Promise<void> {
239
268
  this.observability.socketDisconnected();
240
- await this.scheduleCandidateAlarm();
241
269
  this.pending.rejectSocket(ws, () => new WorkerToolError("unavailable", message, true));
270
+ await this.scheduleSocketAlarms();
242
271
  }
243
272
 
244
273
  private async handleMcp(request: Request, base: string): Promise<Response> {
@@ -360,7 +389,14 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
360
389
  daemon,
361
390
  worker: {
362
391
  pending_calls: this.pending.snapshot(),
363
- daemon_candidates: this.candidateSockets().length,
392
+ daemon_candidates: this.daemonRegistry.candidateSockets().length,
393
+ daemon_probes: this.daemonRegistry.probingSockets().length,
394
+ sockets_live: {
395
+ authenticated: this.daemonRegistry.readyRoleSockets().length + this.daemonRegistry.probingSockets().length,
396
+ ready: this.daemonRegistry.readySockets().length,
397
+ probing: this.daemonRegistry.probingSockets().length,
398
+ candidates: this.daemonRegistry.candidateSockets().length,
399
+ },
364
400
  observability: this.observability.snapshot(),
365
401
  },
366
402
  tools,
@@ -392,7 +428,8 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
392
428
  requestKey?: string,
393
429
  signal?: AbortSignal,
394
430
  ): Promise<unknown> {
395
- const socket = this.daemonSockets()[0];
431
+ this.reclaimStaleDaemonSockets();
432
+ const socket = this.daemonRegistry.readySockets()[0];
396
433
  if (!socket) throw new WorkerToolError("unavailable", "local daemon is not connected; keep the CLI start command running", true);
397
434
  const id = randomToken("call");
398
435
  const timeoutMs = daemonToolTimeoutMs(name, args);
@@ -406,6 +443,10 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
406
443
  timeoutMs,
407
444
  onTimeout: (record) => {
408
445
  sendWebSocketQuietly(record.socket, { type: "cancel_call", id: record.id });
446
+ const silentForMs = Date.now() - daemonLastSeenMs(this.daemonRegistry.readyAttachment(record.socket));
447
+ if (!Number.isFinite(silentForMs) || silentForMs > 45_000) {
448
+ this.invalidateDaemonSocket(record.socket, "daemon became unresponsive", "daemon liveness timeout");
449
+ }
409
450
  return new WorkerToolError("timeout", `daemon tool timed out: ${name}`, true);
410
451
  },
411
452
  signal,
@@ -429,6 +470,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
429
470
  }));
430
471
  } catch {
431
472
  this.pending.reject(id, new WorkerToolError("network_error", "failed to send daemon tool call", true), socket);
473
+ this.invalidateDaemonSocket(socket, "failed to send daemon tool call", "daemon send failed");
432
474
  }
433
475
  }
434
476
  try {
@@ -454,7 +496,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
454
496
  const supplied = request.headers.get("X-Bridge-Token") ?? "";
455
497
  if (!expected || !(await safeEqual(supplied, expected))) return new Response("Unauthorized daemon", { status: 401 });
456
498
 
457
- for (const socket of this.nonDaemonSockets()) {
499
+ for (const socket of this.daemonRegistry.nonReadySockets()) {
458
500
  closeWebSocketQuietly(socket, 1012, "replaced by newer daemon candidate");
459
501
  }
460
502
 
@@ -462,11 +504,8 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
462
504
  const [client, server] = Object.values(pair) as [WebSocket, WebSocket];
463
505
  this.ctx.acceptWebSocket(server);
464
506
  this.observability.socketCandidate();
465
- server.serializeAttachment({
466
- role: "candidate",
467
- connectedAt: new Date().toISOString(),
468
- } satisfies DaemonAttachment);
469
- await this.ctx.storage.setAlarm(Date.now() + DAEMON_HELLO_TIMEOUT_MS);
507
+ this.daemonRegistry.beginCandidate(server);
508
+ await this.scheduleSocketAlarms();
470
509
  server.send(JSON.stringify({ type: "welcome", server: SERVER_NAME, version: SERVER_VERSION }));
471
510
  return new Response(null, { status: 101, webSocket: client });
472
511
  }
@@ -487,79 +526,80 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
487
526
  return this.daemonAdvertisedTools().has(name);
488
527
  }
489
528
  private daemonAdvertisedTools(): Set<string> {
490
- const socket = this.daemonSockets()[0];
529
+ this.reclaimStaleDaemonSockets();
530
+ const socket = this.daemonRegistry.readySockets()[0];
491
531
  if (!socket) return new Set();
492
- const attachment = this.daemonAttachment(socket);
532
+ const attachment = this.daemonRegistry.readyAttachment(socket);
493
533
  if (!attachment?.tools) return new Set();
494
534
  return new Set(attachment.tools);
495
535
  }
496
536
 
497
- private daemonSockets(): WebSocket[] {
498
- return this.ctx.getWebSockets()
499
- .filter((socket) => this.daemonAttachment(socket) && socket.readyState === WebSocket.OPEN)
500
- .sort((left, right) => {
501
- const leftTime = Date.parse(this.daemonAttachment(left)?.connectedAt ?? "") || 0;
502
- const rightTime = Date.parse(this.daemonAttachment(right)?.connectedAt ?? "") || 0;
503
- return rightTime - leftTime;
504
- });
537
+ private async touchDaemonSocket(ws: WebSocket): Promise<void> {
538
+ if (!this.daemonRegistry.touch(ws)) return;
539
+ await this.scheduleSocketAlarms();
505
540
  }
506
541
 
507
- private candidateSockets(): WebSocket[] {
508
- return this.ctx.getWebSockets().filter((socket) => this.socketAttachment(socket)?.role === "candidate" && socket.readyState === WebSocket.OPEN);
509
- }
510
-
511
- private nonDaemonSockets(): WebSocket[] {
512
- return this.ctx.getWebSockets().filter((socket) => {
513
- const role = this.socketAttachment(socket)?.role;
514
- return role !== "daemon" && socket.readyState === WebSocket.OPEN;
515
- });
516
- }
517
-
518
- private socketAttachment(socket: WebSocket): DaemonAttachment | undefined {
519
- const raw = socket.deserializeAttachment();
520
- if (!raw || typeof raw !== "object") return undefined;
521
- const candidate = raw as Partial<DaemonAttachment>;
522
- if (candidate.role !== "candidate" && candidate.role !== "expired" && candidate.role !== "daemon") return undefined;
523
- const policy = sanitizeDaemonPolicy(candidate.policy);
524
- return {
525
- role: candidate.role,
526
- connectedAt: sanitizeMetadataText(candidate.connectedAt, 64) ?? "",
527
- policy,
528
- tools: sanitizeDaemonTools(candidate.tools, policy),
529
- };
542
+ private invalidateDaemonSocket(
543
+ ws: WebSocket,
544
+ message: string,
545
+ closeReason: string,
546
+ errorCode = "daemon_liveness_timeout",
547
+ ): void {
548
+ this.daemonRegistry.expire(ws);
549
+ this.pending.rejectSocket(ws, () => new WorkerToolError("unavailable", message, true));
550
+ sendWebSocketQuietly(ws, { type: "error", error: errorCode });
551
+ closeWebSocketQuietly(ws, 1008, closeReason);
530
552
  }
531
553
 
532
- private daemonAttachment(socket: WebSocket): DaemonAttachment | undefined {
533
- const attachment = this.socketAttachment(socket);
534
- return attachment?.role === "daemon" ? attachment : undefined;
554
+ private reclaimStaleDaemonSockets(now = Date.now()): void {
555
+ for (const socket of this.daemonRegistry.readyRoleSockets()) {
556
+ const deadline = daemonLivenessDeadlineMs(this.daemonRegistry.readyAttachment(socket));
557
+ if (Number.isFinite(deadline) && deadline > now) continue;
558
+ this.invalidateDaemonSocket(socket, "daemon became unresponsive", "daemon liveness timeout");
559
+ }
535
560
  }
536
561
 
537
562
  async alarm(): Promise<void> {
538
563
  const now = Date.now();
539
564
  let nextDeadline = Number.POSITIVE_INFINITY;
540
- for (const socket of this.candidateSockets()) {
541
- const attachment = this.socketAttachment(socket);
565
+ for (const socket of this.daemonRegistry.candidateSockets()) {
566
+ const attachment = this.daemonRegistry.attachment(socket);
542
567
  const connectedAt = Date.parse(attachment?.connectedAt ?? "");
543
568
  const deadline = connectedAt + DAEMON_HELLO_TIMEOUT_MS;
544
569
  if (!Number.isFinite(connectedAt) || deadline <= now) {
545
- socket.serializeAttachment({
546
- role: "expired",
547
- connectedAt: attachment?.connectedAt ?? new Date(0).toISOString(),
548
- } satisfies DaemonAttachment);
570
+ this.daemonRegistry.expire(socket);
549
571
  sendWebSocketQuietly(socket, { type: "error", error: "daemon_hello_timeout" });
550
572
  closeWebSocketQuietly(socket, 1008, "daemon hello timeout");
551
573
  continue;
552
574
  }
553
575
  nextDeadline = Math.min(nextDeadline, deadline);
554
576
  }
577
+ for (const socket of this.daemonRegistry.probingSockets()) {
578
+ const attachment = this.daemonRegistry.attachment(socket);
579
+ const readyDeadline = daemonReadyDeadlineMs(attachment);
580
+ const liveDeadline = daemonLivenessDeadlineMs(attachment);
581
+ if (!Number.isFinite(readyDeadline) || !Number.isFinite(liveDeadline) || Math.min(readyDeadline, liveDeadline) <= now) {
582
+ this.invalidateDaemonSocket(socket, "daemon did not complete end-to-end readiness verification", "daemon ready timeout", "daemon_ready_timeout");
583
+ continue;
584
+ }
585
+ nextDeadline = Math.min(nextDeadline, readyDeadline, liveDeadline);
586
+ }
587
+ for (const socket of this.daemonRegistry.readyRoleSockets()) {
588
+ const deadline = daemonLivenessDeadlineMs(this.daemonRegistry.readyAttachment(socket));
589
+ if (!Number.isFinite(deadline) || deadline <= now) {
590
+ this.invalidateDaemonSocket(socket, "daemon became unresponsive", "daemon liveness timeout");
591
+ continue;
592
+ }
593
+ nextDeadline = Math.min(nextDeadline, deadline);
594
+ }
555
595
  if (Number.isFinite(nextDeadline)) await this.ctx.storage.setAlarm(nextDeadline);
556
596
  else await this.ctx.storage.deleteAlarm();
557
597
  }
558
598
 
559
- private async scheduleCandidateAlarm(): Promise<void> {
599
+ private async scheduleSocketAlarms(): Promise<void> {
560
600
  let nextDeadline = Number.POSITIVE_INFINITY;
561
- for (const socket of this.candidateSockets()) {
562
- const attachment = this.socketAttachment(socket);
601
+ for (const socket of this.daemonRegistry.candidateSockets()) {
602
+ const attachment = this.daemonRegistry.attachment(socket);
563
603
  const connectedAt = Date.parse(attachment?.connectedAt ?? "");
564
604
  if (!Number.isFinite(connectedAt)) {
565
605
  closeWebSocketQuietly(socket, 1008, "invalid daemon candidate timestamp");
@@ -567,19 +607,42 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
567
607
  }
568
608
  nextDeadline = Math.min(nextDeadline, connectedAt + DAEMON_HELLO_TIMEOUT_MS);
569
609
  }
610
+ for (const socket of this.daemonRegistry.probingSockets()) {
611
+ const attachment = this.daemonRegistry.attachment(socket);
612
+ const readyDeadline = daemonReadyDeadlineMs(attachment);
613
+ const liveDeadline = daemonLivenessDeadlineMs(attachment);
614
+ if (!Number.isFinite(readyDeadline) || !Number.isFinite(liveDeadline)) {
615
+ this.invalidateDaemonSocket(socket, "daemon readiness state is invalid", "daemon ready timeout", "daemon_ready_timeout");
616
+ continue;
617
+ }
618
+ nextDeadline = Math.min(nextDeadline, readyDeadline, liveDeadline);
619
+ }
620
+ for (const socket of this.daemonRegistry.readyRoleSockets()) {
621
+ const deadline = daemonLivenessDeadlineMs(this.daemonRegistry.readyAttachment(socket));
622
+ if (!Number.isFinite(deadline)) {
623
+ this.invalidateDaemonSocket(socket, "daemon became unresponsive", "invalid daemon liveness timestamp");
624
+ continue;
625
+ }
626
+ nextDeadline = Math.min(nextDeadline, deadline);
627
+ }
570
628
  if (Number.isFinite(nextDeadline)) await this.ctx.storage.setAlarm(Math.max(Date.now(), nextDeadline));
571
629
  else await this.ctx.storage.deleteAlarm();
572
630
  }
573
631
 
574
632
  private daemonStatus(detail: boolean): Record<string, unknown> {
575
- const sockets = this.daemonSockets();
576
- const attachment = sockets[0] ? this.daemonAttachment(sockets[0]) : undefined;
633
+ this.reclaimStaleDaemonSockets();
634
+ const sockets = this.daemonRegistry.readySockets();
635
+ const attachment = sockets[0] ? this.daemonRegistry.readyAttachment(sockets[0]) : undefined;
577
636
  const tools = attachment?.tools ?? [];
578
637
  const base = {
579
638
  connected: sockets.length > 0,
580
639
  count: sockets.length,
581
640
  tool_count: tools.length,
582
641
  connected_at: attachment?.connectedAt ?? null,
642
+ last_seen_at: attachment?.lastSeenAt ?? attachment?.connectedAt ?? null,
643
+ readiness_verified: sockets.length > 0,
644
+ readiness_timeout_ms: DAEMON_READY_TIMEOUT_MS,
645
+ liveness_timeout_ms: DAEMON_LIVENESS_TIMEOUT_MS,
583
646
  };
584
647
  if (!detail) return base;
585
648
  return {
@@ -713,13 +776,6 @@ function textToolResult(value: unknown, isError = false): Record<string, unknown
713
776
  return result;
714
777
  }
715
778
 
716
- function isFreshDaemonCandidate(connectedAt: string): boolean {
717
- const timestamp = Date.parse(connectedAt);
718
- if (!Number.isFinite(timestamp)) return false;
719
- const age = Date.now() - timestamp;
720
- return age >= 0 && age <= DAEMON_HELLO_TIMEOUT_MS;
721
- }
722
-
723
779
  function asObject(value: unknown): Record<string, unknown> {
724
780
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
725
781
  return value as Record<string, unknown>;
@@ -6,7 +6,7 @@ export class WorkerObservability {
6
6
  private readonly startedAt = performance.now();
7
7
  private readonly requests = { total: 0, successful: 0, client_error: 0, server_error: 0 };
8
8
  private readonly calls = { started: 0, completed: 0, failed: 0, cancelled: 0, timed_out: 0, unmatched_results: 0 };
9
- private readonly sockets = { candidates: 0, authenticated: 0, disconnected: 0, protocol_errors: 0 };
9
+ private readonly sockets = { candidates: 0, authenticated: 0, ready: 0, disconnected: 0, protocol_errors: 0 };
10
10
  private readonly errors = new Map<string, number>();
11
11
  private readonly tools = new Map<string, { started: number; completed: number; failed: number; active: number }>();
12
12
 
@@ -43,6 +43,7 @@ export class WorkerObservability {
43
43
 
44
44
  socketCandidate(): void { this.sockets.candidates += 1; }
45
45
  socketAuthenticated(): void { this.sockets.authenticated += 1; }
46
+ socketReady(): void { this.sockets.ready += 1; }
46
47
  socketDisconnected(): void { this.sockets.disconnected += 1; }
47
48
  socketProtocolError(code: string): void {
48
49
  this.sockets.protocol_errors += 1;