nanocodex 0.6.5 → 0.6.7

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.
Files changed (121) hide show
  1. package/README.md +311 -6
  2. package/THIRD-PARTY-LICENSES +23 -0
  3. package/browser/Agent.d.mts +5 -4
  4. package/browser/Agent.mjs +4 -0
  5. package/browser/Claude.d.mts +4 -0
  6. package/browser/Claude.mjs +11 -0
  7. package/browser/InlineAgent.mjs +19 -1
  8. package/browser/Voice.mjs +11 -4
  9. package/browser/VoiceSession.mjs +20 -6
  10. package/browser/host.d.mts +54 -1
  11. package/browser/host.mjs +65 -16
  12. package/browser/hostManagedWebSocket.mjs +11 -2
  13. package/browser/index.d.mts +3 -10
  14. package/browser/index.mjs +2 -5
  15. package/browser/socket-observations.mjs +203 -0
  16. package/cloud/Decorator.d.mts +2 -2
  17. package/cloud/Decorator.mjs +1 -0
  18. package/cloud/Dialog.d.mts +13 -0
  19. package/cloud/Dialog.mjs +52 -6
  20. package/cloud/Transport.d.mts +0 -11
  21. package/cloud/Transport.mjs +1 -386
  22. package/cloud/actions/account.d.mts +12 -0
  23. package/cloud/actions/account.mjs +11 -0
  24. package/cloud/actions/agent.mjs +3 -0
  25. package/cloud/actions/connection.d.mts +3 -1
  26. package/cloud/actions/connection.mjs +19 -4
  27. package/cloud/index.d.mts +1 -1
  28. package/cloud/index.mjs +1 -1
  29. package/cloud/internal.mjs +19 -4
  30. package/cloud/mercator.mjs +1 -1
  31. package/cloud/mock.d.mts +17 -0
  32. package/cloud/mock.mjs +392 -0
  33. package/cloud/types.d.mts +4 -1
  34. package/cloudflare/Agent.d.mts +6 -0
  35. package/cloudflare/Agent.mjs +190 -10
  36. package/cloudflare/agent-settings.d.mts +1 -1
  37. package/cloudflare/agent-settings.mjs +12 -3
  38. package/cloudflare/egress.mjs +9 -6
  39. package/cloudflare/gateway-binding-responses.mjs +3 -3
  40. package/cloudflare/gateway-responses.d.mts +1 -0
  41. package/cloudflare/gateway-responses.mjs +7 -7
  42. package/cloudflare/managed-auth.d.mts +4 -4
  43. package/cloudflare/managed-auth.mjs +20 -2
  44. package/cloudflare/managed-live.d.mts +6 -0
  45. package/cloudflare/managed-live.mjs +56 -1
  46. package/cloudflare/tracing.d.mts +16 -0
  47. package/cloudflare/tracing.mjs +47 -0
  48. package/cloudflare/workers-ai-responses.d.mts +3 -2
  49. package/cloudflare/workers-ai-responses.mjs +83 -8
  50. package/host/Agent.d.mts +6 -2
  51. package/host/Claude.d.mts +3 -0
  52. package/host/Claude.mjs +2 -0
  53. package/host/index.d.mts +6 -10
  54. package/host/index.mjs +2 -5
  55. package/index.d.mts +11 -10
  56. package/index.mjs +0 -5
  57. package/internal.mjs +33 -3
  58. package/managed/Agent.d.mts +10 -3
  59. package/managed/Agent.mjs +60 -10
  60. package/managed/ManagedError.d.mts +3 -0
  61. package/managed/Voice.mjs +3 -1
  62. package/managed/internal.mjs +2 -1
  63. package/node/Agent.d.mts +6 -2
  64. package/node/Agent.mjs +14 -1
  65. package/node/Claude.d.mts +4 -0
  66. package/node/Claude.mjs +24 -0
  67. package/node/host.mjs +18 -9
  68. package/node/index.d.mts +6 -10
  69. package/node/index.mjs +2 -5
  70. package/package.json +45 -9
  71. package/pkg-node/nanocodex.d.ts +123 -2
  72. package/pkg-node/nanocodex.js +879 -89
  73. package/pkg-node/nanocodex_bg.wasm.d.ts +49 -3
  74. package/pkg-web/.nanocodex-bindgen-stamp +4 -4
  75. package/pkg-web/nanocodex-build.json +1 -1
  76. package/pkg-web/nanocodex.d.ts +172 -5
  77. package/pkg-web/nanocodex.js +872 -88
  78. package/pkg-web/nanocodex_bg.js +872 -88
  79. package/pkg-web/nanocodex_bg.wasm +0 -0
  80. package/pkg-web/nanocodex_bg.wasm.d.ts +49 -3
  81. package/pkg-web/nanocodex_worker.js +1 -1
  82. package/runtime/claude-host.mjs +308 -0
  83. package/runtime/claude-subscription.mjs +79 -0
  84. package/runtime/claude.d.mts +112 -0
  85. package/runtime/claude.mjs +221 -0
  86. package/runtime/code-effect-identity.mjs +123 -0
  87. package/runtime/harnesses.mjs +51 -0
  88. package/runtime/mcp-payment.mjs +8 -0
  89. package/runtime/mcp-runtime.d.mts +10 -0
  90. package/runtime/mcp-runtime.mjs +242 -55
  91. package/runtime/mercator.mjs +2 -0
  92. package/runtime/quickjs-evaluator.mjs +23 -11
  93. package/runtime/response-controls.mjs +16 -10
  94. package/runtime/responses-http.mjs +4 -2
  95. package/runtime/retry-after.d.mts +10 -0
  96. package/runtime/retry-after.mjs +86 -0
  97. package/runtime/steer-receipt.mjs +8 -1
  98. package/runtime/subagent-routing.d.mts +4 -2
  99. package/runtime/subagent-routing.mjs +9 -3
  100. package/runtime/subagents.d.mts +8 -3
  101. package/services/account.d.mts +76 -0
  102. package/services/account.mjs +151 -0
  103. package/services/hosted.mjs +72 -0
  104. package/services/index.d.mts +95 -0
  105. package/services/index.mjs +192 -0
  106. package/services/scope.mjs +36 -0
  107. package/{runtime/tempo-provider.d.mts → tempo/index.d.mts} +20 -1
  108. package/{runtime/tempo-provider.mjs → tempo/index.mjs} +39 -3
  109. package/tools/browser/browserShell.mjs +18 -8
  110. package/tools/browser/python.worker.mjs +17 -1
  111. package/tools/browser/threadGit.mjs +12 -2
  112. package/tools/environment.d.mts +17 -0
  113. package/tools/environment.mjs +24 -1
  114. package/tools/index.d.mts +6 -3
  115. package/types.d.mts +107 -6
  116. package/worker/Claude.d.mts +4 -0
  117. package/worker/Claude.mjs +11 -0
  118. package/worker/ClaudeSubscription.d.mts +24 -0
  119. package/worker/ClaudeSubscription.mjs +9 -0
  120. package/worker/index.d.mts +9 -0
  121. package/worker/index.mjs +4 -0
@@ -90,6 +90,7 @@ export class BrowserVoiceSession {
90
90
  #elevenLabs;
91
91
  #muted = false;
92
92
  #inputGeneration = 0;
93
+ #playbackRevision = 0;
93
94
  #meterTimer;
94
95
  #backendReady;
95
96
  #resolveBackendReady;
@@ -267,7 +268,7 @@ export class BrowserVoiceSession {
267
268
  const channel = this.#channel;
268
269
  channel.addEventListener("message", (event) => {
269
270
  if (this.#directControl && !this.#closed && this.#channel === channel) {
270
- this.#receiveControl(event.data, () => this.#channel === channel);
271
+ this.#receiveControl(event.data, () => this.#channel === channel && channel.readyState === "open");
271
272
  }
272
273
  });
273
274
  channel.addEventListener("close", () => {
@@ -311,6 +312,7 @@ export class BrowserVoiceSession {
311
312
  }
312
313
 
313
314
  noteTypedInput() {
315
+ this.#playbackRevision++;
314
316
  this.#playbackEnabled = false;
315
317
  this.#elevenLabs?.interrupt();
316
318
  this.#speaker?.setEnabled(false);
@@ -429,14 +431,19 @@ export class BrowserVoiceSession {
429
431
  for (const text of effects.undelivered_answers ?? []) this.#options.onUndeliveredAnswer?.(text);
430
432
  if (effects.input_generation !== undefined) {
431
433
  if (effects.input_generation < this.#inputGeneration) return;
432
- if (effects.input_generation > this.#inputGeneration) this.#elevenLabs?.interrupt();
434
+ if (effects.input_generation > this.#inputGeneration) {
435
+ this.#playbackRevision++;
436
+ this.#elevenLabs?.interrupt();
437
+ }
433
438
  this.#inputGeneration = effects.input_generation;
434
439
  }
435
440
  if (effects.playback_enabled === false) {
441
+ this.#playbackRevision++;
436
442
  this.#playbackEnabled = false;
437
443
  this.#elevenLabs?.interrupt();
438
444
  this.#speaker?.setEnabled(false);
439
445
  }
446
+ const playbackRevision = this.#playbackRevision;
440
447
  let sent = 0;
441
448
  for (const frame of effects.frames ?? []) {
442
449
  if (this.#directControl && this.#channel?.readyState === "open") {
@@ -448,12 +455,15 @@ export class BrowserVoiceSession {
448
455
  }
449
456
  }
450
457
  if (effects.acknowledge_frames && sent > 0) await this.#core?.framesSent(sent);
451
- if (!this.#closed && effects.playback_enabled === true && sent === (effects.frames?.length ?? 0)) {
458
+ // A live interruption can overtake asynchronous frame acknowledgement.
459
+ // Keep its speaker gate authoritative while retaining accepted captions.
460
+ const currentPlayback = playbackRevision === this.#playbackRevision;
461
+ if (currentPlayback && !this.#closed && effects.playback_enabled === true && sent === (effects.frames?.length ?? 0)) {
452
462
  this.#playbackEnabled = true;
453
463
  this.#speaker?.setEnabled(!this.#elevenLabs);
454
464
  }
455
465
  for (const entry of effects.transcripts ?? []) {
456
- if (this.#playbackEnabled && !this.#closed) this.#elevenLabs?.transcript(entry);
466
+ if (currentPlayback && this.#playbackEnabled && !this.#closed) this.#elevenLabs?.transcript(entry);
457
467
  this.#options.onTranscript(entry.speaker, entry.text, entry);
458
468
  }
459
469
  if (effects.status) this.#status(effects.status);
@@ -490,7 +500,7 @@ export class BrowserVoiceSession {
490
500
  let opened = false;
491
501
  sideband.addEventListener("message", (event) => {
492
502
  if (!this.#closed && generation === this.#sidebandGeneration) {
493
- this.#receiveControl(event.data, () => generation === this.#sidebandGeneration);
503
+ this.#receiveControl(event.data, () => generation === this.#sidebandGeneration && sideband.readyState === WebSocket.OPEN);
494
504
  }
495
505
  });
496
506
  sideband.addEventListener("close", () => {
@@ -516,7 +526,11 @@ export class BrowserVoiceSession {
516
526
  this.#applyLive(async () => {
517
527
  if (!this.#directControl) await this.#admission;
518
528
  if (this.#closed || !isCurrent()) return;
519
- if (await this.#core.requiresAgentAdmission(payload)) {
529
+ const requiresAdmission = await this.#core.requiresAgentAdmission(payload);
530
+ // Classification can yield while this transport closes or is replaced.
531
+ // Only requests accepted on the current connection may enter the queue.
532
+ if (this.#closed || !isCurrent()) return;
533
+ if (requiresAdmission) {
520
534
  // Retain accepted requests on close, but keep captions and interruptions
521
535
  // independent of the durable route and its ordered task queue.
522
536
  void this.#enqueue(async () => {
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  CodeEvaluator,
3
+ CodeEffectJournal,
3
4
  McpServers,
4
5
  MppSession,
5
6
  SubagentToolContext,
@@ -51,12 +52,60 @@ export type BrowserWebSocketConnection = {
51
52
  socket: WebSocket;
52
53
  status?: number | undefined;
53
54
  requestId?: string | undefined;
55
+ /** @internal Correlation with the separately deployed credential egress. */
56
+ egressRequestId?: string | undefined;
54
57
  serverModel?: string | undefined;
55
58
  reasoningIncluded?: boolean | undefined;
56
59
  turnState?: string | undefined;
57
60
  };
58
61
 
62
+ /** @internal Passive lifecycle metadata; never provider frames or request content. */
63
+ export type BrowserSocketObservation = {
64
+ event: "socket.connecting" | "socket.connect_waiting" | "socket.opened" | "socket.closed" | "socket.error"
65
+ | "request.send_started" | "request.send_waiting" | "request.sent" | "request.waiting" | "request.first_message"
66
+ | "request.first_output" | "request.first_reasoning_delta" | "request.first_answer_delta" | "request.first_tool_delta" | "request.finished" | "provider.timing";
67
+ socket_id: string;
68
+ request_id: string;
69
+ egress_request_id?: string;
70
+ provider_request_id?: string;
71
+ response_id?: string;
72
+ socket_request_index?: number;
73
+ model_call_index?: number;
74
+ phase?: "generation" | "compaction" | "warmup";
75
+ outcome?: "completed" | "failed" | "send_failed" | "superseded";
76
+ elapsed_ms?: number;
77
+ send_wait_ms?: number;
78
+ first_message_ms?: number;
79
+ first_output_ms?: number;
80
+ first_reasoning_delta_ms?: number;
81
+ first_answer_delta_ms?: number;
82
+ first_tool_delta_ms?: number;
83
+ /** Fixed protocol name, or unclassified for oversized/unknown envelopes. */
84
+ provider_event_type?: string;
85
+ output_kind?: "item" | "reasoning" | "answer" | "tool";
86
+ last_message_age_ms?: number;
87
+ received_message_count?: number;
88
+ queued_message_count?: number;
89
+ socket_delivered_message_count?: number;
90
+ socket_queue_residence_max_ms?: number;
91
+ buffered_send_bytes?: number;
92
+ close_code?: number;
93
+ close_clean?: boolean;
94
+ intentional?: boolean;
95
+ pre_inference_ms?: number;
96
+ engine_queue_max_ms?: number;
97
+ engine_service_ttft_total_ms?: number;
98
+ };
99
+
59
100
  export function createBrowserHost(options?: {
101
+ /** @internal Trusted runtime configuration carried by nanocodex.browser.internalRuntime. */
102
+ [key: symbol]: {
103
+ traceTool?: <T>(
104
+ name: string,
105
+ context: { sessionId: string; callId: string; parentCallId?: string; turnId?: string },
106
+ run: () => Promise<T>,
107
+ ) => Promise<T>;
108
+ } | undefined;
60
109
  WebSocketImpl?: typeof WebSocket;
61
110
  hostAuth?: boolean;
62
111
  hostManagedProtocol?: boolean;
@@ -74,7 +123,9 @@ export function createBrowserHost(options?: {
74
123
  /** Remote MCP servers exposed through native and Code Mode tool_search plus deferred tools. */
75
124
  mcp?: McpServers;
76
125
  codeEvaluator?: CodeEvaluator;
77
- toolMode?: "code" | "direct";
126
+ /** @internal Trusted durable effect receipts, not available inside guest code. */
127
+ codeEffectJournal?: CodeEffectJournal;
128
+ toolMode?: "code" | "code-only" | "direct";
78
129
  /** @internal Live host lifecycle for ephemeral Rust-owned subagents. */
79
130
  subagentRouting?: Pick<import('../runtime/subagent-routing.mjs').SubagentRouting, 'resolve' | 'bind'>;
80
131
  subagentSessions?: {
@@ -97,6 +148,8 @@ export function createBrowserHost(options?: {
97
148
  engine_service_ttft_total_ms?: number;
98
149
  }>;
99
150
  }) => void;
151
+ /** @internal Live observations while an owned request waits; does not cancel it. */
152
+ onSocketEvent?: (event: BrowserSocketObservation) => void;
100
153
  maxQueuedMessages?: number;
101
154
  maxQueuedBytes?: number;
102
155
  maxBufferedSendBytes?: number;
package/browser/host.mjs CHANGED
@@ -1,6 +1,7 @@
1
+ import { createCodeEffectIdentity } from "../runtime/code-effect-identity.mjs";
1
2
  import { createBeforeCompaction } from "../runtime/before-compaction.mjs";
2
3
  import { createResponsesHttp, responsesHttpHeaders } from "../runtime/responses-http.mjs";
3
- import { createCodeRuntime, toolResult } from "../runtime/code-runtime.mjs";
4
+ import { createCodeRuntime, toolResult, traceToolInvocation } from "../runtime/code-runtime.mjs";
4
5
  import {
5
6
  toolRouterBrand,
6
7
  toolRouterRuntime,
@@ -9,6 +10,7 @@ import {
9
10
  import { utf8ByteLength } from "../runtime/utf8.mjs";
10
11
  import { createWorkerEvaluator } from "../runtime/worker-evaluator.mjs";
11
12
  import { openHostManagedWebSocket } from "./hostManagedWebSocket.mjs";
13
+ import { createSocketObservations } from "./socket-observations.mjs";
12
14
 
13
15
  const DEFAULT_MAX_QUEUED_MESSAGES = 4_096;
14
16
  const DEFAULT_MAX_QUEUED_BYTES = 32 * 1024 * 1024;
@@ -19,14 +21,24 @@ const MPP_CLIENT_PROTOCOL_ERROR_CLOSE_CODE = 3008;
19
21
  const WEBSOCKET_OPEN = 1;
20
22
 
21
23
  export function createBrowserHost(options = {}) {
24
+ const internalRuntime = options[Symbol.for("nanocodex.browser.internalRuntime")];
25
+ if (internalRuntime !== undefined
26
+ && (!internalRuntime || typeof internalRuntime !== "object" || Array.isArray(internalRuntime))) {
27
+ throw new TypeError("browser host internal runtime options must be an object");
28
+ }
29
+ const traceTool = internalRuntime?.traceTool;
30
+ if (traceTool !== undefined && typeof traceTool !== "function") {
31
+ throw new TypeError("browser host tool tracing hook must be a function");
32
+ }
22
33
  const onSocketTiming = options.onSocketTiming;
23
34
  if (onSocketTiming !== undefined && typeof onSocketTiming !== "function") {
24
35
  throw new TypeError("host socket timing hook must be a function");
25
36
  }
37
+ const socketObservations = createSocketObservations(options.onSocketEvent);
26
38
  const preservation = createBeforeCompaction(options.beforeCompaction);
27
39
  const toolMode = options.toolMode ?? "code";
28
- if (toolMode !== "code" && toolMode !== "direct") {
29
- throw new TypeError("toolMode must be code or direct");
40
+ if (toolMode !== "code" && toolMode !== "code-only" && toolMode !== "direct") {
41
+ throw new TypeError("toolMode must be code, code-only or direct");
30
42
  }
31
43
  const toolsRouter = options.tools?.[toolRouterBrand]
32
44
  ? options.tools[toolRouterRuntime]
@@ -38,7 +50,7 @@ export function createBrowserHost(options = {}) {
38
50
  if (toolsMcp && options.mcp) {
39
51
  throw new TypeError("MCP is already configured in Tools");
40
52
  }
41
- if ((toolsMcp || options.mcp) && toolMode !== "code") {
53
+ if ((toolsMcp || options.mcp) && toolMode === "direct") {
42
54
  throw new TypeError("remote MCP requires Code Mode");
43
55
  }
44
56
  const toolsLifecycle = options.tools?.[toolRuntimeLifecycle];
@@ -76,8 +88,12 @@ export function createBrowserHost(options = {}) {
76
88
  : () => Promise.reject(new Error(
77
89
  "browser Code Mode requires a child Worker or an explicit codeEvaluator",
78
90
  )));
91
+ const effectIdentity = createCodeEffectIdentity(options.codeEffectJournal);
79
92
  const code = createCodeRuntime(options.tools, {
93
+ traceTool,
80
94
  evaluate: codeEvaluator,
95
+ effectJournal: options.codeEffectJournal,
96
+ effectIdentity: options.codeEffectJournal ? effectIdentity.resolve : undefined,
81
97
  subagentSessions: options.subagentSessions,
82
98
  });
83
99
  const toolProviders = options.toolProviders ?? [];
@@ -192,12 +208,22 @@ export function createBrowserHost(options = {}) {
192
208
  if (preconnected !== undefined) await closePreconnected();
193
209
  ownership = openOwned(() => createWebSocket(endpoint, sessionId, request));
194
210
  }
195
- const opened = await ownership.promise;
196
- const { socket, ...handshake } = normalizeWebSocketConnection(opened);
211
+ let connection;
212
+ const observation = socketObservations?.connect(sessionId, () => ({
213
+ queued_message_count: connection?.queue.length ?? 0,
214
+ socket_delivered_message_count: connection?.timing?.metrics.delivered_message_count ?? 0,
215
+ socket_queue_residence_max_ms: connection?.timing?.metrics.queue_residence_max_ms ?? 0,
216
+ buffered_send_bytes: connection?.socket.bufferedAmount ?? 0,
217
+ }));
218
+ let normalized;
219
+ try { normalized = normalizeWebSocketConnection(await ownership.promise); }
220
+ catch (error) { observation?.error(); observation?.close(undefined, false); throw error; }
221
+ const { socket, ...handshake } = normalized;
197
222
  return new Promise((resolve, reject) => {
198
223
  let settled = false;
199
- const connection = {
224
+ connection = {
200
225
  socket,
226
+ observation,
201
227
  timing: socketTiming(),
202
228
  queue: [],
203
229
  queuedBytes: 0,
@@ -209,6 +235,7 @@ export function createBrowserHost(options = {}) {
209
235
  if (settled) return;
210
236
  settled = true;
211
237
  connectingConnections.delete(connection);
238
+ observation?.close(undefined, false);
212
239
  finishSocketTiming(connection);
213
240
  reject(error);
214
241
  };
@@ -228,6 +255,7 @@ export function createBrowserHost(options = {}) {
228
255
  delete connection.reject;
229
256
  const handle = nextHandle++;
230
257
  connections.set(handle, connection);
258
+ observation?.connected(handshake);
231
259
  resolve(JSON.stringify({
232
260
  handle,
233
261
  status: handshake.status ?? 101,
@@ -244,6 +272,7 @@ export function createBrowserHost(options = {}) {
244
272
  : { kind: "binary" });
245
273
  });
246
274
  socket.addEventListener("close", (event) => {
275
+ observation?.close(event, connection.intentionallyClosed);
247
276
  connection.wakeSend?.();
248
277
  if (!settled) {
249
278
  rejectConnection(new Error(`WebSocket closed during connection with code ${event.code}`));
@@ -252,6 +281,7 @@ export function createBrowserHost(options = {}) {
252
281
  }
253
282
  });
254
283
  socket.addEventListener("error", () => {
284
+ observation?.error();
255
285
  if (!settled) {
256
286
  rejectConnection(new Error("WebSocket connection failed"));
257
287
  } else {
@@ -329,6 +359,8 @@ export function createBrowserHost(options = {}) {
329
359
  error: "concurrent WebSocket sends are unsupported" });
330
360
  }
331
361
  connection.sending = true;
362
+ let sent = false;
363
+ connection.observation?.sendStarted();
332
364
  try {
333
365
  if (connection.managed) {
334
366
  connection.socket.send(JSON.stringify({ mpp: "message", data: message }));
@@ -363,6 +395,8 @@ export function createBrowserHost(options = {}) {
363
395
  }
364
396
  if (closed()) return closedResult();
365
397
  connection.socket.send(message);
398
+ sent = true;
399
+ connection.observation?.sent();
366
400
  return JSON.stringify({ ok: true });
367
401
  } catch (error) {
368
402
  return JSON.stringify({
@@ -371,6 +405,8 @@ export function createBrowserHost(options = {}) {
371
405
  error: error instanceof Error ? error.message : String(error),
372
406
  });
373
407
  } finally {
408
+ // A send that did not reach socket.send must not leave a waiting timer.
409
+ if (!sent) connection.observation?.sendFailed();
374
410
  connection.sending = false;
375
411
  }
376
412
  }
@@ -406,6 +442,7 @@ export function createBrowserHost(options = {}) {
406
442
  if (!connection) return;
407
443
  connections.delete(handle);
408
444
  connection.intentionallyClosed = true;
445
+ connection.observation?.close(undefined, true);
409
446
  connection.wakeSend?.();
410
447
  connection.waiter?.({ kind: "closed", detail: "by the WASM runtime" });
411
448
  finishSocketTiming(connection);
@@ -422,11 +459,13 @@ export function createBrowserHost(options = {}) {
422
459
  if (timing && dataMessage) {
423
460
  timing.metrics.message_count += 1;
424
461
  // Only the exact metadata event can produce diagnostics; deltas stay opaque.
425
- if (message.kind === "text" && timing.provider.size < 32) {
462
+ connection.observation?.message(message.kind === "text" ? message.text : undefined);
463
+ if (message.kind === "text" && (timing.provider.size < 32 || connection.observation)) {
426
464
  const metadata = providerSocketTiming(message.text);
427
465
  if (metadata) {
466
+ connection.observation?.provider(metadata);
428
467
  const key = metadata.response_id ?? timing.provider.size;
429
- if (!timing.provider.has(key)) timing.provider.set(key, metadata);
468
+ if (timing.provider.size < 32 && !timing.provider.has(key)) timing.provider.set(key, metadata);
430
469
  }
431
470
  }
432
471
  }
@@ -461,7 +500,7 @@ export function createBrowserHost(options = {}) {
461
500
 
462
501
  // No counters, timestamps, metadata parsing or observations without the internal hook.
463
502
  function socketTiming() {
464
- return onSocketTiming === undefined ? undefined : {
503
+ return onSocketTiming === undefined && socketObservations === undefined ? undefined : {
465
504
  metrics: {
466
505
  message_count: 0,
467
506
  delivered_message_count: 0,
@@ -479,6 +518,7 @@ export function createBrowserHost(options = {}) {
479
518
  // Finalize owned consumption, not the remote close event: buffered frames may
480
519
  // still be drained after that event. Clear first for reentrant close/dispose.
481
520
  connection.timing = undefined;
521
+ if (onSocketTiming === undefined) return;
482
522
  const metrics = timing.metrics;
483
523
  try {
484
524
  const result = onSocketTiming({
@@ -513,7 +553,7 @@ export function createBrowserHost(options = {}) {
513
553
  }
514
554
  for (const handle of [...connections.keys()]) cleanup(() => close(handle));
515
555
  cleanup(() => closePreconnected(disposalError));
516
- cleanup(() => code.reset());
556
+ cleanup(() => { effectIdentity.reset(); return code.reset(); });
517
557
  cleanup(() => mcpInstalled?.then(() => {}));
518
558
  cleanup(() => toolsLifecycle?.close());
519
559
  cleanup(() => options.onDispose?.());
@@ -612,10 +652,19 @@ export function createBrowserHost(options = {}) {
612
652
  next,
613
653
  close,
614
654
  sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
615
- executeCode: code.executeCodeObserved,
616
- waitCode: code.waitCodeObserved,
655
+ executeCode: traceTool === undefined ? code.executeCodeObserved
656
+ : (source, sessionId = "default", callId = "exec", model = "unknown", turnId, localDefinitions, executeLocalTool) =>
657
+ traceToolInvocation(traceTool, "exec", {
658
+ sessionId, callId, ...(turnId == null ? {} : { turnId }),
659
+ }, () => code.executeCodeObserved(source, sessionId, callId, model, turnId, localDefinitions, executeLocalTool)),
660
+ waitCode: traceTool === undefined ? code.waitCodeObserved
661
+ : (input, sessionId = "default", callId = "wait") =>
662
+ traceToolInvocation(traceTool, "wait", { sessionId, callId },
663
+ () => code.waitCodeObserved(input, sessionId, callId)),
617
664
  beginCodeTurn: code.beginTurn,
618
665
  cancelCodeTurn: code.cancelTurn,
666
+ preemptCode: code.preempt,
667
+ preemptCodeTurn: code.preemptTurn,
619
668
  nextCodeUpdate: code.nextCodeUpdate,
620
669
  executeTool: code.executeTool,
621
670
  routeSubagent: (request) => {
@@ -653,9 +702,9 @@ export function createBrowserHost(options = {}) {
653
702
  },
654
703
  toolMode: () => toolMode,
655
704
  toolDefinitions: code.toolDefinitions,
656
- releaseSession: code.releaseSession,
657
- emitEvent: onEvent,
658
- reset: code.reset,
705
+ releaseSession: (sessionId) => { effectIdentity.release(sessionId); socketObservations?.release(sessionId); return code.releaseSession(sessionId); },
706
+ emitEvent: (event, ...args) => { code.observeEvent(event); effectIdentity.observe(event); socketObservations?.runtime(event); return onEvent(event, ...args); },
707
+ reset: () => { effectIdentity.reset(); return code.reset(); },
659
708
  dispose,
660
709
  });
661
710
  }
@@ -1,3 +1,5 @@
1
+ import { retryAfterAdvice } from "../runtime/retry-after.mjs";
2
+
1
3
  const DEFAULT_TIMEOUT_MS = 10_000;
2
4
 
3
5
  /** Open Nanocodex's same-origin Responses proxy and consume its setup frame. */
@@ -74,13 +76,20 @@ function waitForProxyHandshake(socket, timeoutMs) {
74
76
  && message.status <= 599) {
75
77
  const status = message.status;
76
78
  const body = typeof message.error === "string" ? message.error : `HTTP ${status}`;
77
- const retryAfter = Number(message.retryAfter);
79
+ const advice = retryAfterAdvice(
80
+ typeof message.retryAfter === "number" ? String(message.retryAfter) : message.retryAfter,
81
+ );
82
+ // The proxy may already have captured advice before draining its rejection body.
83
+ if (Number.isSafeInteger(message.retry_after_deadline_ms) && message.retry_after_deadline_ms >= 0) {
84
+ advice.retry_after_deadline_ms = message.retry_after_deadline_ms;
85
+ }
78
86
  fail(Object.assign(
79
87
  new Error(`Agent connection rejected with HTTP ${status}: ${body}`),
80
88
  {
81
89
  status,
82
90
  body,
83
- ...(Number.isFinite(retryAfter) && retryAfter >= 0 ? { retryAfter } : {}),
91
+ ...advice,
92
+ ...(advice.retry_after !== undefined ? { retryAfter: advice.retry_after } : {}),
84
93
  },
85
94
  ));
86
95
  return;
@@ -4,16 +4,6 @@ export {
4
4
  subscriptionRevision,
5
5
  } from "../runtime/subscription-store.mjs";
6
6
  export { createQuickJsEvaluator } from "../runtime/quickjs-evaluator.mjs";
7
- export {
8
- createTempoProvider,
9
- createTempoProviderFromAccounts,
10
- DEFAULT_MERCATOR_MCP_URL,
11
- } from "../runtime/tempo-provider.mjs";
12
- export type {
13
- AccountsTempoProviderOptions,
14
- AccountsWallet,
15
- TempoProvider,
16
- } from "../runtime/tempo-provider.mjs";
17
7
  export type {
18
8
  AgentEvent,
19
9
  AgentLifecycle,
@@ -46,6 +36,7 @@ export type {
46
36
  TurnResult,
47
37
  TurnUsage,
48
38
  McpPayment,
39
+ PaidMcpPayment,
49
40
  McpServer,
50
41
  McpServers,
51
42
  MemoryChatGptSubscriptionStore,
@@ -79,3 +70,5 @@ export type {
79
70
  BrowserWebSocketConnection,
80
71
  BrowserWebSocketRequest,
81
72
  } from "./host.mjs";
73
+
74
+ export * as Claude from "./Claude.mjs";
package/browser/index.mjs CHANGED
@@ -4,11 +4,6 @@ export {
4
4
  subscriptionRevision,
5
5
  } from "../runtime/subscription-store.mjs";
6
6
  export { createQuickJsEvaluator } from "../runtime/quickjs-evaluator.mjs";
7
- export {
8
- createTempoProvider,
9
- createTempoProviderFromAccounts,
10
- DEFAULT_MERCATOR_MCP_URL,
11
- } from "../runtime/tempo-provider.mjs";
12
7
  export * as Agent from "./Agent.mjs";
13
8
  export * as ChatGptSubscription from "./ChatGptSubscription.mjs";
14
9
  export * as Subagents from "../runtime/subagents.mjs";
@@ -21,3 +16,5 @@ export {
21
16
  defaultHostManagedWebSocketUrl,
22
17
  openHostManagedWebSocket,
23
18
  } from "./hostManagedWebSocket.mjs";
19
+
20
+ export * as Claude from "./Claude.mjs";