@standardagents/builder 0.27.13 → 0.28.1

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/dist/runtime.js CHANGED
@@ -28,6 +28,55 @@ var __copyProps = (to, from, except, desc) => {
28
28
  };
29
29
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
30
 
31
+ // src/agents/client-forwarding.ts
32
+ function asForwarding(state) {
33
+ return state;
34
+ }
35
+ function supportsClientForwarding(state) {
36
+ const s = asForwarding(state);
37
+ return typeof s.callClient === "function" && typeof s.callClientStream === "function";
38
+ }
39
+ function callClient(state, request) {
40
+ const s = asForwarding(state);
41
+ if (typeof s.callClient !== "function") {
42
+ return Promise.resolve({
43
+ ok: false,
44
+ error: "This thread runtime cannot forward tools to a client. Client forwarding requires the DurableThread bridge."
45
+ });
46
+ }
47
+ return s.callClient(request);
48
+ }
49
+ function callClientStream(state, request) {
50
+ const s = asForwarding(state);
51
+ if (typeof s.callClientStream !== "function") {
52
+ return Promise.resolve({
53
+ ok: false,
54
+ error: "This thread runtime cannot open client streams. Client streaming requires the DurableThread bridge."
55
+ });
56
+ }
57
+ return s.callClientStream(request);
58
+ }
59
+ function encodeStreamChunk(bytes) {
60
+ let binary = "";
61
+ const step = 32768;
62
+ for (let i = 0; i < bytes.length; i += step) {
63
+ binary += String.fromCharCode(...bytes.subarray(i, i + step));
64
+ }
65
+ return btoa(binary);
66
+ }
67
+ function decodeStreamChunk(data) {
68
+ const binary = atob(data);
69
+ const bytes = new Uint8Array(binary.length);
70
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
71
+ return bytes;
72
+ }
73
+ var CLIENT_STREAM_CHUNK_BYTES;
74
+ var init_client_forwarding = __esm({
75
+ "src/agents/client-forwarding.ts"() {
76
+ CLIENT_STREAM_CHUNK_BYTES = 256 * 1024;
77
+ }
78
+ });
79
+
31
80
  // src/agents/types.ts
32
81
  var SUBAGENT_INITIAL_ARGUMENTS_THREAD_PROPERTY, MAX_STEPS, MAX_RETRIES_PER_MODEL, TIMESTAMP_MULTIPLIER, STREAM_COOLDOWN, BACKOFF_BASE;
33
82
  var init_types = __esm({
@@ -920,6 +969,20 @@ var init_ProviderRegistry = __esm({
920
969
  const threadEnv = state.thread.metadata?.env ?? state.thread.metadata?.tenvs ?? null;
921
970
  const agentBuilderId = state.env.AGENT_BUILDER.idFromName("singleton");
922
971
  const agentBuilder = state.env.AGENT_BUILDER.get(agentBuilderId);
972
+ let instanceEnv = null;
973
+ if (typeof agentBuilder.getInstanceEnvEntries === "function") {
974
+ try {
975
+ const entries = await agentBuilder.getInstanceEnvEntries();
976
+ if (Array.isArray(entries) && entries.length > 0) {
977
+ instanceEnv = {};
978
+ for (const entry of entries) {
979
+ if (entry?.name) instanceEnv[entry.name] = entry.value;
980
+ }
981
+ }
982
+ } catch (error) {
983
+ console.warn("buildThreadScopedEnv: instance env lookup failed:", error);
984
+ }
985
+ }
923
986
  let userId = state.thread.metadata?.user_id ?? null;
924
987
  let parent = state.thread.metadata?.parent ?? null;
925
988
  let hops = 0;
@@ -956,11 +1019,12 @@ var init_ProviderRegistry = __esm({
956
1019
  console.warn("buildThreadScopedEnv:", diagnostic);
957
1020
  }
958
1021
  }
959
- if (!userEnv && !threadEnv) {
1022
+ if (!instanceEnv && !userEnv && !threadEnv) {
960
1023
  this.scopeDiagnostics.set(state.env, diagnostic);
961
1024
  return state.env;
962
1025
  }
963
- const merged = { ...state.env, ...userEnv ?? {}, ...threadEnv ?? {} };
1026
+ if (instanceEnv) diagnostic = `${diagnostic ? diagnostic + "; " : ""}instance env: ${Object.keys(instanceEnv).length} entries`;
1027
+ const merged = { ...state.env, ...instanceEnv ?? {}, ...userEnv ?? {}, ...threadEnv ?? {} };
964
1028
  this.scopeDiagnostics.set(merged, diagnostic);
965
1029
  return merged;
966
1030
  }
@@ -974,7 +1038,7 @@ var init_ProviderRegistry = __esm({
974
1038
  * @param thread - Thread instance for loading model from virtual modules
975
1039
  * @returns Provider and model definition
976
1040
  */
977
- async getProvider(modelName, env, thread) {
1041
+ async getProvider(modelName, env, thread, getState) {
978
1042
  if (!thread) {
979
1043
  throw new Error("Thread instance required for model lookup");
980
1044
  }
@@ -1007,6 +1071,18 @@ var init_ProviderRegistry = __esm({
1007
1071
  );
1008
1072
  }
1009
1073
  const config = resolved.providerConfig;
1074
+ if (typeof providerDefinition.createFetch === "function") {
1075
+ const state = getState ? await getState() : void 0;
1076
+ const configWithFetch = {
1077
+ ...config,
1078
+ fetch: providerDefinition.createFetch({
1079
+ providerName,
1080
+ config,
1081
+ state
1082
+ })
1083
+ };
1084
+ return this.buildResult(providerFactory(configWithFetch), modelDef, false);
1085
+ }
1010
1086
  const fingerprint = this.getProviderFingerprint(providerName, modelDef, config);
1011
1087
  const cached = this.providerCache.get(modelName);
1012
1088
  if (cached?.fingerprint === fingerprint) {
@@ -1879,7 +1955,16 @@ var init_LLMRequest = __esm({
1879
1955
  static async callModel(modelId, context, state, logId) {
1880
1956
  const { ProviderRegistry: ProviderRegistry2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
1881
1957
  const providerEnv = await ProviderRegistry2.buildThreadScopedEnv(state);
1882
- const { provider, modelName, modelDef, viaPlatform, platformProxyOrigin } = await ProviderRegistry2.getProvider(modelId, providerEnv, state.thread.instance);
1958
+ const getThreadState = async () => {
1959
+ const { ThreadStateImpl: ThreadStateImpl2 } = await Promise.resolve().then(() => (init_ThreadStateImpl(), ThreadStateImpl_exports));
1960
+ return ThreadStateImpl2.fromFlowState(state);
1961
+ };
1962
+ const { provider, modelName, modelDef, viaPlatform, platformProxyOrigin } = await ProviderRegistry2.getProvider(
1963
+ modelId,
1964
+ providerEnv,
1965
+ state.thread.instance,
1966
+ getThreadState
1967
+ );
1883
1968
  const streamAbort = new AbortController();
1884
1969
  const parentSignal = state.abortController?.signal;
1885
1970
  let onParentAbort;
@@ -6433,12 +6518,14 @@ var init_FlowEngine = __esm({
6433
6518
  } catch (error) {
6434
6519
  console.error("Fatal error in executeAsync:", error);
6435
6520
  await this.logError(state, error, "fatal_execution_error", "FlowEngine.executeAsync");
6521
+ await this.settleTerminalFailure(state, error);
6436
6522
  throw error;
6437
6523
  }
6438
6524
  } else {
6439
6525
  this.executeAsync(state, state.threadId).catch(async (error) => {
6440
6526
  console.error("Fatal error in executeAsync:", error);
6441
6527
  await this.logError(state, error, "fatal_execution_error", "FlowEngine.executeAsync");
6528
+ await this.settleTerminalFailure(state, error);
6442
6529
  });
6443
6530
  }
6444
6531
  return {
@@ -6492,10 +6579,27 @@ var init_FlowEngine = __esm({
6492
6579
  console.warn(
6493
6580
  `[FlowEngine] Finalized ${rows.length} orphaned incomplete log(s) for thread ${state.threadId}`
6494
6581
  );
6582
+ await state.thread.instance.failPendingMessagesAndBroadcast?.();
6495
6583
  } catch (err) {
6496
6584
  console.error("[FlowEngine] Failed to finalize orphaned logs (non-fatal):", err);
6497
6585
  }
6498
6586
  }
6587
+ /**
6588
+ * A terminal execution failure ended the run without a normal assistant
6589
+ * reply. Persist a durable failed assistant message + settle pending
6590
+ * messages so every connected head lands on a visible end state instead of
6591
+ * a stuck "Working" spinner. Best-effort — never masks the original error.
6592
+ */
6593
+ static async settleTerminalFailure(state, error) {
6594
+ try {
6595
+ const detail = error instanceof Error ? error.message : String(error);
6596
+ await state.thread.instance.settleTerminalFailure?.(
6597
+ `The agent run failed unexpectedly: ${detail}`
6598
+ );
6599
+ } catch (settleError) {
6600
+ console.error("[FlowEngine] Failed to settle terminal failure (non-fatal):", settleError);
6601
+ }
6602
+ }
6499
6603
  static async executeAsync(state, threadId) {
6500
6604
  const effectiveMaxSteps = Math.max(
6501
6605
  MAX_STEPS,
@@ -12770,6 +12874,7 @@ function runCodeViaThreadRpc(threadInstance, threadId, source, options) {
12770
12874
  var ThreadStateImpl, ExecutionStateImpl;
12771
12875
  var init_ThreadStateImpl = __esm({
12772
12876
  "src/agents/ThreadStateImpl.ts"() {
12877
+ init_client_forwarding();
12773
12878
  init_types();
12774
12879
  init_utilities();
12775
12880
  init_code_execution();
@@ -13354,9 +13459,10 @@ var init_ThreadStateImpl = __esm({
13354
13459
  }
13355
13460
  const instance = this._threadInstance;
13356
13461
  const toolCallId = this._flowState?.currentToolCall?.id;
13357
- const mustPark = request.requestPermission != null && await this.forwardedCallNeedsApproval(request);
13358
- if (mustPark && toolCallId && this._executionState && typeof instance.dispatchClientTool === "function" && typeof instance.hasBridge === "function" && await instance.hasBridge()) {
13359
- const side = this._flowState?.currentSide === "b" ? "b" : "a";
13462
+ const canPark = !!(toolCallId && this._executionState && typeof instance.dispatchClientTool === "function");
13463
+ const side = this._flowState?.currentSide === "b" ? "b" : "a";
13464
+ const mustParkForApproval = request.requestPermission != null && await this.forwardedCallNeedsApproval(request);
13465
+ if (mustParkForApproval && canPark && toolCallId && typeof instance.hasBridge === "function" && await instance.hasBridge()) {
13360
13466
  await instance.dispatchClientTool(request, toolCallId, side);
13361
13467
  this._executionState.stop();
13362
13468
  return {
@@ -13368,6 +13474,55 @@ var init_ThreadStateImpl = __esm({
13368
13474
  const local = await this._threadInstance.callClientTool(request);
13369
13475
  if (local.ok || !this.isNoClient(local)) return local;
13370
13476
  const env = this._threadInstance.env;
13477
+ if (env?.AGENT_BUILDER_THREAD) {
13478
+ const seen = /* @__PURE__ */ new Set([this._metadata.id]);
13479
+ let parentId = await this.resolveParentThreadId(this._metadata.id);
13480
+ while (parentId && !seen.has(parentId)) {
13481
+ seen.add(parentId);
13482
+ try {
13483
+ const stub = env.AGENT_BUILDER_THREAD.get(env.AGENT_BUILDER_THREAD.idFromName(parentId));
13484
+ const res = await stub.callClientTool(request);
13485
+ if (res.ok || !this.isNoClient(res)) return res;
13486
+ } catch {
13487
+ }
13488
+ parentId = await this.resolveParentThreadId(parentId);
13489
+ }
13490
+ }
13491
+ if (canPark && toolCallId) {
13492
+ await instance.dispatchClientTool(request, toolCallId, side);
13493
+ if (typeof instance.notifyClientWaiting === "function") {
13494
+ try {
13495
+ await instance.notifyClientWaiting(toolCallId, this._metadata.id, this._metadata.user_id ?? null);
13496
+ } catch {
13497
+ }
13498
+ }
13499
+ this._executionState.stop();
13500
+ return {
13501
+ ok: true,
13502
+ result: `\u23F3 Waiting for your machine to reconnect to run ${request.tool}\u2026`,
13503
+ data: { deferred: true, parked: true, waitingForClient: true, toolCallId }
13504
+ };
13505
+ }
13506
+ return local;
13507
+ }
13508
+ /**
13509
+ * Open a bidirectional byte stream to the connected client (e.g. a raw TCP
13510
+ * tunnel for Worker-side TLS termination). Streams require a LIVE bridge on
13511
+ * this thread and never park — a stream is only meaningful while the client
13512
+ * is attached. Subagents must not call this directly; they proxy whole
13513
+ * requests up to the root thread instead (request-up / stream-down).
13514
+ */
13515
+ async callClientStream(request) {
13516
+ const instance = this._threadInstance;
13517
+ if (typeof instance.openClientStream !== "function") {
13518
+ return {
13519
+ ok: false,
13520
+ error: "This thread runtime cannot open client streams. Client streaming requires the DurableThread bridge."
13521
+ };
13522
+ }
13523
+ const local = await instance.openClientStream(request);
13524
+ if (local.ok || !this.isNoClient(local)) return local;
13525
+ const env = this._threadInstance.env;
13371
13526
  if (!env?.AGENT_BUILDER_THREAD) return local;
13372
13527
  const seen = /* @__PURE__ */ new Set([this._metadata.id]);
13373
13528
  let parentId = await this.resolveParentThreadId(this._metadata.id);
@@ -13375,14 +13530,86 @@ var init_ThreadStateImpl = __esm({
13375
13530
  seen.add(parentId);
13376
13531
  try {
13377
13532
  const stub = env.AGENT_BUILDER_THREAD.get(env.AGENT_BUILDER_THREAD.idFromName(parentId));
13378
- const res = await stub.callClientTool(request);
13379
- if (res.ok || !this.isNoClient(res)) return res;
13533
+ if (typeof stub.proxyStreamOpen === "function") {
13534
+ const opened = await stub.proxyStreamOpen(request);
13535
+ if (opened.ok && opened.id) {
13536
+ return { ok: true, duplex: this.buildProxyDuplex(stub, opened.id) };
13537
+ }
13538
+ if (opened.error && !opened.noClient) {
13539
+ return { ok: false, error: opened.error };
13540
+ }
13541
+ }
13380
13542
  } catch {
13381
13543
  }
13382
13544
  parentId = await this.resolveParentThreadId(parentId);
13383
13545
  }
13384
13546
  return local;
13385
13547
  }
13548
+ /**
13549
+ * Build a client duplex that bridges to a stream opened on an ancestor DO
13550
+ * (the bridge holder) via chunk-pull RPCs. `readable` polls `proxyStreamPull`;
13551
+ * `writable` forwards each chunk with `proxyStreamWrite`; teardown calls
13552
+ * `proxyStreamAbort`.
13553
+ */
13554
+ buildProxyDuplex(stub, id) {
13555
+ let settleDone;
13556
+ const done = new Promise((r) => settleDone = r);
13557
+ let closed = false;
13558
+ const readable = new ReadableStream({
13559
+ async pull(controller) {
13560
+ while (!closed) {
13561
+ let res;
13562
+ try {
13563
+ res = await stub.proxyStreamPull(id);
13564
+ } catch (error) {
13565
+ controller.error(error instanceof Error ? error : new Error(String(error)));
13566
+ settleDone({ ok: false, error: "proxy stream pull failed" });
13567
+ return;
13568
+ }
13569
+ for (const chunk of res.chunks) {
13570
+ controller.enqueue(decodeStreamChunk(chunk));
13571
+ }
13572
+ if (res.done) {
13573
+ controller.close();
13574
+ settleDone(res.error ? { ok: false, error: res.error } : { ok: true });
13575
+ return;
13576
+ }
13577
+ if (res.chunks.length > 0) return;
13578
+ await new Promise((r) => setTimeout(r, 25));
13579
+ }
13580
+ },
13581
+ cancel() {
13582
+ closed = true;
13583
+ void stub.proxyStreamAbort(id).catch(() => {
13584
+ });
13585
+ }
13586
+ });
13587
+ const writable = new WritableStream({
13588
+ async write(chunk) {
13589
+ await stub.proxyStreamWrite(id, encodeStreamChunk(chunk));
13590
+ },
13591
+ close() {
13592
+ void stub.proxyStreamEnd?.(id).catch?.(() => {
13593
+ });
13594
+ },
13595
+ abort() {
13596
+ closed = true;
13597
+ void stub.proxyStreamAbort(id).catch(() => {
13598
+ });
13599
+ }
13600
+ });
13601
+ return {
13602
+ readable,
13603
+ writable,
13604
+ close: (error) => {
13605
+ closed = true;
13606
+ void stub.proxyStreamAbort(id).catch(() => {
13607
+ });
13608
+ settleDone(error ? { ok: false, error } : { ok: true });
13609
+ },
13610
+ done
13611
+ };
13612
+ }
13386
13613
  isNoClient(res) {
13387
13614
  return res.ok === false && !!res.data && typeof res.data === "object" && res.data.noClient === true;
13388
13615
  }
@@ -14256,6 +14483,9 @@ async function requireAdmin(request, env) {
14256
14483
  return result;
14257
14484
  }
14258
14485
 
14486
+ // src/runtime.ts
14487
+ init_client_forwarding();
14488
+
14259
14489
  // src/durable-objects/migrations/001_initial.ts
14260
14490
  var migration = {
14261
14491
  version: 1,
@@ -15931,6 +16161,7 @@ function isQualifiedName(name) {
15931
16161
  }
15932
16162
 
15933
16163
  // src/durable-objects/DurableThread.ts
16164
+ init_client_forwarding();
15934
16165
  init_sessionTools();
15935
16166
 
15936
16167
  // src/durable-objects/bridge-owner.ts
@@ -16161,6 +16392,11 @@ var DurableThread = class _DurableThread extends DurableObject {
16161
16392
  // setValue() so external KV writes (release/reassign) invalidate it too.
16162
16393
  executionOwnerCache = void 0;
16163
16394
  pendingClientCalls = /* @__PURE__ */ new Map();
16395
+ // Live bidirectional client byte streams (e.g. TCP tunnels for Worker-side
16396
+ // TLS). Keyed by stream id; each stream is bound to the bridge socket it was
16397
+ // opened on and dies with it. Streams are in-memory only — they exist solely
16398
+ // while the client is attached and never park durably.
16399
+ clientStreams = /* @__PURE__ */ new Map();
16164
16400
  alarmQueue;
16165
16401
  alarmQueueRearmed = false;
16166
16402
  currentAbortController = null;
@@ -16220,6 +16456,9 @@ var DurableThread = class _DurableThread extends DurableObject {
16220
16456
  case "watchdogCheck":
16221
16457
  await this.watchdogCheck(args.threadId);
16222
16458
  break;
16459
+ case "clientParkTimeout":
16460
+ await this.failParkedClientCall(args.toolCallId, args.threadId);
16461
+ break;
16223
16462
  default:
16224
16463
  throw new Error(`Unknown method: ${method}`);
16225
16464
  }
@@ -18203,6 +18442,69 @@ ${resultContent}${attachmentSummary}`;
18203
18442
  * Stop the currently executing thread (RPC method)
18204
18443
  * Simple "off" switch - stops turns, only cleared by user messages
18205
18444
  */
18445
+ /**
18446
+ * Flip every still-`pending` message to `failed` and broadcast the change to
18447
+ * all connected heads. This is THE settle path for any terminal condition
18448
+ * that leaves a turn half-open — user stop, lost execution, terminal
18449
+ * execution failure, and expired parked forwarded calls — so no head is
18450
+ * left rendering a stuck "Working" state. Returns the failure timestamp.
18451
+ */
18452
+ async failPendingMessagesAndBroadcast() {
18453
+ const now = Date.now() * TIMESTAMP_MULTIPLIER;
18454
+ await this.ctx.storage.sql.exec(
18455
+ `UPDATE messages SET status = 'failed', response_completed_at = ? WHERE status = 'pending'`,
18456
+ now
18457
+ );
18458
+ const cursor = await this.ctx.storage.sql.exec(
18459
+ `SELECT id, role, content, tool_calls, tool_call_id, tool_status, created_at, log_id, status, request_sent_at, response_completed_at FROM messages WHERE response_completed_at = ?`,
18460
+ now
18461
+ );
18462
+ for (const row of cursor) {
18463
+ this.broadcastMessage({
18464
+ id: row.id,
18465
+ role: row.role,
18466
+ content: row.content,
18467
+ tool_calls: row.tool_calls,
18468
+ tool_call_id: row.tool_call_id,
18469
+ tool_status: row.tool_status,
18470
+ created_at: row.created_at,
18471
+ log_id: row.log_id,
18472
+ status: "failed",
18473
+ request_sent_at: row.request_sent_at,
18474
+ response_completed_at: row.response_completed_at
18475
+ });
18476
+ }
18477
+ return now;
18478
+ }
18479
+ /**
18480
+ * A terminal failure ended a turn without a normal assistant reply: settle
18481
+ * every pending message and persist a durable `failed` assistant message so
18482
+ * all connected heads land on a real, visible end state — never a stuck
18483
+ * "Working" spinner. Used by the FlowEngine's terminal catch and the parked
18484
+ * forwarded-call TTL.
18485
+ */
18486
+ async settleTerminalFailure(errorMessage) {
18487
+ await this.ensureMigrated();
18488
+ await this.failPendingMessagesAndBroadcast();
18489
+ const messageId = crypto.randomUUID();
18490
+ const timestamp = Date.now() * TIMESTAMP_MULTIPLIER;
18491
+ await this.ctx.storage.sql.exec(
18492
+ `INSERT INTO messages (id, role, content, created_at, status, prompt) VALUES (?, ?, ?, ?, ?, ?)`,
18493
+ messageId,
18494
+ "assistant",
18495
+ errorMessage,
18496
+ timestamp,
18497
+ "failed",
18498
+ null
18499
+ );
18500
+ this.broadcastMessage({
18501
+ id: messageId,
18502
+ role: "assistant",
18503
+ content: errorMessage,
18504
+ created_at: timestamp,
18505
+ status: "failed"
18506
+ });
18507
+ }
18206
18508
  async stop() {
18207
18509
  await this.ensureMigrated();
18208
18510
  try {
@@ -18212,30 +18514,7 @@ ${resultContent}${attachmentSummary}`;
18212
18514
  if (this.currentAbortController) {
18213
18515
  this.currentAbortController.abort();
18214
18516
  }
18215
- const now = Date.now() * TIMESTAMP_MULTIPLIER;
18216
- const updateCursor = await this.ctx.storage.sql.exec(
18217
- `UPDATE messages SET status = 'failed', response_completed_at = ? WHERE status = 'pending'`,
18218
- now
18219
- );
18220
- const cursor = await this.ctx.storage.sql.exec(
18221
- `SELECT id, role, content, tool_calls, tool_call_id, tool_status, created_at, log_id, status, request_sent_at, response_completed_at FROM messages WHERE response_completed_at = ?`,
18222
- now
18223
- );
18224
- for (const row of cursor) {
18225
- this.broadcastMessage({
18226
- id: row.id,
18227
- role: row.role,
18228
- content: row.content,
18229
- tool_calls: row.tool_calls,
18230
- tool_call_id: row.tool_call_id,
18231
- tool_status: row.tool_status,
18232
- created_at: row.created_at,
18233
- log_id: row.log_id,
18234
- status: "failed",
18235
- request_sent_at: row.request_sent_at,
18236
- response_completed_at: row.response_completed_at
18237
- });
18238
- }
18517
+ const now = await this.failPendingMessagesAndBroadcast();
18239
18518
  const messagesWithToolsCursor = await this.ctx.storage.sql.exec(
18240
18519
  `SELECT id, tool_calls, depth FROM messages WHERE tool_calls IS NOT NULL AND tool_calls != 'null' AND (depth IS NULL OR depth = 0)`
18241
18520
  );
@@ -19712,6 +19991,7 @@ ${resultContent}${attachmentSummary}`;
19712
19991
  socket.close();
19713
19992
  } catch {
19714
19993
  }
19994
+ this.failClientStreamsForSocket(socket);
19715
19995
  this.failPendingClientCallsIfNoBridge();
19716
19996
  }
19717
19997
  /**
@@ -19967,6 +20247,352 @@ ${resultContent}${attachmentSummary}`;
19967
20247
  }
19968
20248
  return { side };
19969
20249
  }
20250
+ // ─── Client byte streams (Worker-side TLS tunnels) ─────────────────────────
20251
+ // A stream is a bidirectional byte pipe over the bridge WebSocket: the
20252
+ // client opens a raw TCP socket per the stream's `meta` and shuttles bytes
20253
+ // both ways; the Worker terminates TLS over those bytes (node:tls over the
20254
+ // duplex), so the client only ever relays ciphertext. Streams are bound to a
20255
+ // live socket and never park.
20256
+ /** Default time to wait for the client to accept a stream open. */
20257
+ static CLIENT_STREAM_OPEN_TIMEOUT_MS = 3e4;
20258
+ /** Open a bidirectional byte stream to the connected owner client. */
20259
+ async openClientStream(request) {
20260
+ const owner = await this.getExecutionOwner();
20261
+ const socket = this.getOpenBridgeSocket(owner?.client_id ?? null);
20262
+ if (!socket) {
20263
+ return {
20264
+ ok: false,
20265
+ error: "The workspace is not available right now. Try again in a moment.",
20266
+ data: { noClient: true }
20267
+ };
20268
+ }
20269
+ const id = crypto.randomUUID();
20270
+ let controller = null;
20271
+ const readable = new ReadableStream({
20272
+ start: (c) => {
20273
+ controller = c;
20274
+ },
20275
+ cancel: () => {
20276
+ this.abortClientStream(id, "The stream reader was canceled.");
20277
+ }
20278
+ });
20279
+ let settleDone;
20280
+ const done = new Promise((resolve) => {
20281
+ settleDone = resolve;
20282
+ });
20283
+ const state = {
20284
+ socket,
20285
+ controller,
20286
+ opened: null,
20287
+ openTimer: null,
20288
+ settleDone,
20289
+ settled: false
20290
+ };
20291
+ this.clientStreams.set(id, state);
20292
+ this.bridgeLastSeen.set(socket, Date.now());
20293
+ const openResult = await new Promise((resolve) => {
20294
+ state.opened = resolve;
20295
+ const timeoutMs = typeof request.timeoutMs === "number" && request.timeoutMs > 0 ? request.timeoutMs : _DurableThread.CLIENT_STREAM_OPEN_TIMEOUT_MS;
20296
+ state.openTimer = setTimeout(() => {
20297
+ resolve({
20298
+ ok: false,
20299
+ error: `The client did not accept the ${request.tool} stream within ${Math.round(timeoutMs / 1e3)}s.`
20300
+ });
20301
+ }, timeoutMs);
20302
+ try {
20303
+ socket.send(
20304
+ JSON.stringify({
20305
+ type: "stream_open",
20306
+ id,
20307
+ tool: request.tool,
20308
+ meta: request.meta ?? {}
20309
+ })
20310
+ );
20311
+ } catch (error) {
20312
+ resolve({
20313
+ ok: false,
20314
+ error: `Failed to reach the client: ${error instanceof Error ? error.message : String(error)}`
20315
+ });
20316
+ }
20317
+ });
20318
+ state.opened = null;
20319
+ if (state.openTimer) {
20320
+ clearTimeout(state.openTimer);
20321
+ state.openTimer = null;
20322
+ }
20323
+ if (!openResult.ok) {
20324
+ this.clientStreams.delete(id);
20325
+ state.settled = true;
20326
+ settleDone({ ok: false, error: openResult.error });
20327
+ return { ok: false, error: openResult.error ?? "Stream open failed." };
20328
+ }
20329
+ const sendChunked = (bytes) => {
20330
+ for (let i = 0; i < bytes.length; i += CLIENT_STREAM_CHUNK_BYTES) {
20331
+ const chunk = bytes.subarray(i, i + CLIENT_STREAM_CHUNK_BYTES);
20332
+ socket.send(JSON.stringify({ type: "stream_data", id, data: encodeStreamChunk(chunk) }));
20333
+ }
20334
+ };
20335
+ const writable = new WritableStream({
20336
+ write: (chunk) => {
20337
+ if (!this.clientStreams.has(id)) {
20338
+ throw new Error("The client stream is closed.");
20339
+ }
20340
+ sendChunked(chunk);
20341
+ },
20342
+ close: () => {
20343
+ try {
20344
+ if (this.clientStreams.has(id)) {
20345
+ socket.send(JSON.stringify({ type: "stream_end", id }));
20346
+ }
20347
+ } catch {
20348
+ }
20349
+ },
20350
+ abort: (reason) => {
20351
+ this.abortClientStream(id, reason instanceof Error ? reason.message : String(reason));
20352
+ }
20353
+ });
20354
+ const duplex = {
20355
+ readable,
20356
+ writable,
20357
+ close: (error) => this.abortClientStream(id, error),
20358
+ done
20359
+ };
20360
+ return { ok: true, duplex };
20361
+ }
20362
+ // ─── Subagent stream proxy-up (chunk-pull, no cross-DO ReadableStream) ─────
20363
+ // A subagent (child thread, no bridge) proxies its whole stream request UP to
20364
+ // this bridge-holding root. We open the client stream locally, then buffer
20365
+ // incoming bytes so the child can drain them over plain request/response RPCs
20366
+ // — sidestepping the unproven DO-to-DO ReadableStream return.
20367
+ proxyStreams = /* @__PURE__ */ new Map();
20368
+ async proxyStreamOpen(request) {
20369
+ const opened = await this.openClientStream(request);
20370
+ if (!opened.ok) {
20371
+ const noClient = !!(opened.data && opened.data.noClient);
20372
+ return { ok: false, error: opened.error, noClient };
20373
+ }
20374
+ const id = crypto.randomUUID();
20375
+ const duplex = opened.duplex;
20376
+ const reader = duplex.readable.getReader();
20377
+ const writer = duplex.writable.getWriter();
20378
+ const entry = {
20379
+ writer,
20380
+ reader,
20381
+ buffer: [],
20382
+ done: false,
20383
+ error: void 0,
20384
+ pump: Promise.resolve(),
20385
+ close: (error) => duplex.close(error)
20386
+ };
20387
+ entry.pump = (async () => {
20388
+ try {
20389
+ while (true) {
20390
+ const { value, done } = await reader.read();
20391
+ if (done) break;
20392
+ if (value) entry.buffer.push(value);
20393
+ }
20394
+ } catch (error) {
20395
+ entry.error = error instanceof Error ? error.message : String(error);
20396
+ } finally {
20397
+ entry.done = true;
20398
+ }
20399
+ })();
20400
+ void duplex.done.then((d) => {
20401
+ entry.done = true;
20402
+ if (!d.ok && !entry.error) entry.error = d.error;
20403
+ });
20404
+ this.proxyStreams.set(id, entry);
20405
+ return { ok: true, id };
20406
+ }
20407
+ async proxyStreamWrite(id, dataB64) {
20408
+ const entry = this.proxyStreams.get(id);
20409
+ if (!entry) return;
20410
+ try {
20411
+ await entry.writer.write(decodeStreamChunk(dataB64));
20412
+ } catch {
20413
+ }
20414
+ }
20415
+ async proxyStreamPull(id) {
20416
+ const entry = this.proxyStreams.get(id);
20417
+ if (!entry) return { chunks: [], done: true, error: "unknown proxy stream" };
20418
+ const chunks = entry.buffer.splice(0).map((b) => encodeStreamChunk(b));
20419
+ if (entry.done && entry.buffer.length === 0) {
20420
+ this.proxyStreams.delete(id);
20421
+ return { chunks, done: true, error: entry.error };
20422
+ }
20423
+ return { chunks, done: false };
20424
+ }
20425
+ async proxyStreamEnd(id) {
20426
+ const entry = this.proxyStreams.get(id);
20427
+ if (!entry) return;
20428
+ try {
20429
+ await entry.writer.close();
20430
+ } catch {
20431
+ }
20432
+ }
20433
+ async proxyStreamAbort(id) {
20434
+ const entry = this.proxyStreams.get(id);
20435
+ if (!entry) return;
20436
+ this.proxyStreams.delete(id);
20437
+ try {
20438
+ entry.close("aborted");
20439
+ } catch {
20440
+ }
20441
+ }
20442
+ /** Settle and remove a client stream exactly once. */
20443
+ settleClientStream(id, result) {
20444
+ const state = this.clientStreams.get(id);
20445
+ if (!state) return;
20446
+ this.clientStreams.delete(id);
20447
+ if (state.openTimer) clearTimeout(state.openTimer);
20448
+ if (state.opened) state.opened({ ok: false, error: result.error ?? "Stream closed." });
20449
+ if (!state.settled) {
20450
+ state.settled = true;
20451
+ try {
20452
+ if (result.ok) state.controller?.close();
20453
+ else state.controller?.error(new Error(result.error ?? "Client stream aborted."));
20454
+ } catch {
20455
+ }
20456
+ state.settleDone(result);
20457
+ }
20458
+ }
20459
+ /** Abort a stream from the Worker side, telling the client to tear down. */
20460
+ abortClientStream(id, error) {
20461
+ const state = this.clientStreams.get(id);
20462
+ if (!state) return;
20463
+ try {
20464
+ if (state.socket.readyState === WebSocket.OPEN) {
20465
+ state.socket.send(JSON.stringify({ type: "stream_abort", id, error: error ?? null }));
20466
+ }
20467
+ } catch {
20468
+ }
20469
+ this.settleClientStream(id, error ? { ok: false, error } : { ok: true });
20470
+ }
20471
+ /** Handle an incoming stream_* frame from a bridge client. */
20472
+ handleClientStreamFrame(ws, parsed) {
20473
+ const id = typeof parsed.id === "string" ? parsed.id : null;
20474
+ if (!id) return;
20475
+ const state = this.clientStreams.get(id);
20476
+ if (!state || state.socket !== ws) return;
20477
+ switch (parsed.type) {
20478
+ case "stream_opened": {
20479
+ if (state.opened) {
20480
+ const resolve = state.opened;
20481
+ state.opened = null;
20482
+ if (state.openTimer) {
20483
+ clearTimeout(state.openTimer);
20484
+ state.openTimer = null;
20485
+ }
20486
+ resolve({ ok: true });
20487
+ }
20488
+ return;
20489
+ }
20490
+ case "stream_error": {
20491
+ const error = typeof parsed.error === "string" ? parsed.error : "Client stream error.";
20492
+ this.settleClientStream(id, { ok: false, error });
20493
+ return;
20494
+ }
20495
+ case "stream_data": {
20496
+ if (typeof parsed.data !== "string") return;
20497
+ try {
20498
+ state.controller?.enqueue(decodeStreamChunk(parsed.data));
20499
+ } catch {
20500
+ this.abortClientStream(id, "Failed to deliver stream data.");
20501
+ }
20502
+ return;
20503
+ }
20504
+ case "stream_end": {
20505
+ this.settleClientStream(id, { ok: true });
20506
+ return;
20507
+ }
20508
+ case "stream_abort": {
20509
+ const error = typeof parsed.error === "string" ? parsed.error : void 0;
20510
+ this.settleClientStream(id, { ok: false, error: error ?? "Client aborted the stream." });
20511
+ return;
20512
+ }
20513
+ }
20514
+ }
20515
+ /** Fail every stream bound to a socket that closed or died. */
20516
+ failClientStreamsForSocket(ws) {
20517
+ for (const [id, state] of Array.from(this.clientStreams.entries())) {
20518
+ if (state.socket === ws) {
20519
+ this.settleClientStream(id, { ok: false, error: "The client connection was lost." });
20520
+ }
20521
+ }
20522
+ }
20523
+ // ─── Parked-call wake + bounded TTL ────────────────────────────────────────
20524
+ /** How long a parked forwarded call waits for a client before failing visibly. */
20525
+ static CLIENT_PARK_TTL_MS = 30 * 60 * 1e3;
20526
+ /**
20527
+ * A forwarded call parked with NO client attached: ask the user's hub to
20528
+ * summon the owning machine's daemon to this thread, and arm the bounded
20529
+ * park TTL that turns an unreachable host into a visible durable failure
20530
+ * instead of a silent hang.
20531
+ */
20532
+ async notifyClientWaiting(toolCallId, threadId, userId) {
20533
+ try {
20534
+ await this.alarmQueue.enqueue(
20535
+ "clientParkTimeout",
20536
+ { toolCallId, threadId },
20537
+ _DurableThread.CLIENT_PARK_TTL_MS
20538
+ );
20539
+ } catch (error) {
20540
+ console.error("[DurableThread] Failed to arm park TTL (non-fatal):", error);
20541
+ }
20542
+ if (!userId) return;
20543
+ try {
20544
+ const namespace = this.env.AGENT_BUILDER_USER_HUB;
20545
+ if (!namespace) return;
20546
+ const hub = namespace.get(namespace.idFromName(userId));
20547
+ await hub.requestWake(threadId);
20548
+ } catch (error) {
20549
+ console.error("[DurableThread] Hub wake failed (non-fatal):", error);
20550
+ }
20551
+ }
20552
+ /**
20553
+ * Park TTL expired: if the call is still pending, settle it as a durable,
20554
+ * VISIBLE failure — rewrite the waiting placeholder, mark the pending row
20555
+ * failed, fail any still-pending assistant messages, and persist a durable
20556
+ * failed assistant message so every connected head settles instead of
20557
+ * showing "Working" forever.
20558
+ */
20559
+ async failParkedClientCall(toolCallId, threadId) {
20560
+ await this.ensureMigrated();
20561
+ await this.ensurePendingClientTable();
20562
+ const cursor = await this.ctx.storage.sql.exec(
20563
+ `SELECT status, tool FROM pending_client_calls WHERE tool_call_id = ? LIMIT 1`,
20564
+ toolCallId
20565
+ );
20566
+ const row = cursor.toArray()[0];
20567
+ if (!row || row.status !== "pending") return;
20568
+ const errorText = "Could not reach your machine to run this operation. Reconnect it and retry.";
20569
+ await this.ctx.storage.sql.exec(
20570
+ `UPDATE pending_client_calls SET status = 'failed' WHERE tool_call_id = ?`,
20571
+ toolCallId
20572
+ );
20573
+ await this.ctx.storage.sql.exec(
20574
+ `UPDATE messages SET content = ?, tool_status = 'error' WHERE tool_call_id = ? AND role = 'tool'`,
20575
+ `Error: ${errorText}`,
20576
+ toolCallId
20577
+ );
20578
+ try {
20579
+ const msgCursor = await this.ctx.storage.sql.exec(
20580
+ `SELECT id, role, content, tool_call_id, tool_status, created_at, depth
20581
+ FROM messages WHERE tool_call_id = ? AND role = 'tool' LIMIT 1`,
20582
+ toolCallId
20583
+ );
20584
+ const msg = msgCursor.toArray()[0];
20585
+ if (msg) this.broadcastMessage(msg);
20586
+ } catch {
20587
+ }
20588
+ await this.settleTerminalFailure(errorText);
20589
+ if (threadId) {
20590
+ try {
20591
+ await this.disarmExecutionWatchdog(threadId);
20592
+ } catch {
20593
+ }
20594
+ }
20595
+ }
19970
20596
  /**
19971
20597
  * Broadcast a log record to all connected WebSocket clients
19972
20598
  */
@@ -20183,6 +20809,10 @@ ${resultContent}${attachmentSummary}`;
20183
20809
  this.resolveClientCall(ws, parsed);
20184
20810
  return;
20185
20811
  }
20812
+ if (parsed && typeof parsed.type === "string" && parsed.type.startsWith("stream_") && parsed.type !== "stream_ping" && parsed.type !== "stream_pong") {
20813
+ this.handleClientStreamFrame(ws, parsed);
20814
+ return;
20815
+ }
20186
20816
  }
20187
20817
  } catch (error) {
20188
20818
  console.error("[DurableThread] Error handling WebSocket message:", error);
@@ -20203,6 +20833,7 @@ ${resultContent}${attachmentSummary}`;
20203
20833
  this.bridgeSockets.delete(ws);
20204
20834
  this.bridgeLastSeen.delete(ws);
20205
20835
  this.bridgeClientInfo.delete(ws);
20836
+ this.failClientStreamsForSocket(ws);
20206
20837
  this.failPendingClientCallsIfNoBridge();
20207
20838
  }
20208
20839
  }
@@ -20224,6 +20855,7 @@ ${resultContent}${attachmentSummary}`;
20224
20855
  this.bridgeSockets.delete(ws);
20225
20856
  this.bridgeLastSeen.delete(ws);
20226
20857
  this.bridgeClientInfo.delete(ws);
20858
+ this.failClientStreamsForSocket(ws);
20227
20859
  this.failPendingClientCallsIfNoBridge();
20228
20860
  }
20229
20861
  }
@@ -20541,6 +21173,11 @@ ${resultContent}${attachmentSummary}`;
20541
21173
  data: { id, is_complete: true, error, error_type: errorType }
20542
21174
  });
20543
21175
  }
21176
+ try {
21177
+ await this.failPendingMessagesAndBroadcast();
21178
+ } catch (error2) {
21179
+ console.error("[DurableThread] Failed to settle pending messages (non-fatal):", error2);
21180
+ }
20544
21181
  }
20545
21182
  /**
20546
21183
  * A subagent whose execution the watchdog gave up on leaves its parent
@@ -23532,11 +24169,24 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
23532
24169
  throw new Error("User value user_id must be a non-empty string");
23533
24170
  }
23534
24171
  }
24172
+ /**
24173
+ * The per-user hub is the sharded home for account-wide KV. When the binding
24174
+ * exists, the singleton delegates so the global object is no longer a
24175
+ * per-user bottleneck; without it (pre-hub deploys, unit tests) it keeps the
24176
+ * local `user_values` table.
24177
+ */
24178
+ userHub(userId) {
24179
+ const namespace = this.env.AGENT_BUILDER_USER_HUB;
24180
+ if (!namespace) return null;
24181
+ return namespace.get(namespace.idFromName(userId));
24182
+ }
23535
24183
  /** Read one JSON value from a user's account-wide key-value store. */
23536
24184
  async getUserValue(userId, key) {
23537
24185
  await this.ensureMigrated();
23538
24186
  this.assertValidUserValueUserId(userId);
23539
24187
  this.assertValidUserValueKey(key);
24188
+ const hub = this.userHub(userId);
24189
+ if (hub) return hub.getUserValue(userId, key);
23540
24190
  const cursor = await this.ctx.storage.sql.exec(
23541
24191
  `SELECT value FROM user_values WHERE user_id = ? AND key = ? LIMIT 1`,
23542
24192
  userId,
@@ -23557,6 +24207,8 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
23557
24207
  await this.ensureMigrated();
23558
24208
  this.assertValidUserValueUserId(userId);
23559
24209
  this.assertValidUserValueKey(key);
24210
+ const hub = this.userHub(userId);
24211
+ if (hub) return hub.setUserValue(userId, key, value);
23560
24212
  if (value === null || value === void 0) {
23561
24213
  await this.ctx.storage.sql.exec(
23562
24214
  `DELETE FROM user_values WHERE user_id = ? AND key = ?`,
@@ -23593,6 +24245,8 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
23593
24245
  async listUserValues(userId, options = {}) {
23594
24246
  await this.ensureMigrated();
23595
24247
  this.assertValidUserValueUserId(userId);
24248
+ const hub = this.userHub(userId);
24249
+ if (hub) return hub.listUserValues(userId, options);
23596
24250
  const limit = Math.min(Math.max(Math.trunc(options.limit ?? 100), 1), 500);
23597
24251
  const offset = Math.max(Math.trunc(options.offset ?? 0), 0);
23598
24252
  const prefix = typeof options.prefix === "string" && options.prefix.length > 0 ? options.prefix : null;
@@ -25341,6 +25995,192 @@ function formatMs(ms) {
25341
25995
  const minutes = Math.floor(seconds / 60);
25342
25996
  return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
25343
25997
  }
25998
+ var DurableUserHub = class extends DurableObject {
25999
+ /** Persistent client (daemon) sockets, restored across hibernation. */
26000
+ clientSockets = /* @__PURE__ */ new Set();
26001
+ clientInfo = /* @__PURE__ */ new Map();
26002
+ migrated = false;
26003
+ constructor(ctx, env) {
26004
+ super(ctx, env);
26005
+ if (typeof WebSocketRequestResponsePair !== "undefined") {
26006
+ ctx.setWebSocketAutoResponse(
26007
+ new WebSocketRequestResponsePair("stream_ping", "stream_pong")
26008
+ );
26009
+ }
26010
+ for (const ws of ctx.getWebSockets()) {
26011
+ const attachment = ws.deserializeAttachment();
26012
+ if (attachment?.type === "hub_client") {
26013
+ this.clientSockets.add(ws);
26014
+ this.clientInfo.set(ws, {
26015
+ clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
26016
+ clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
26017
+ clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
26018
+ });
26019
+ }
26020
+ }
26021
+ }
26022
+ ensureMigrated() {
26023
+ if (this.migrated) return;
26024
+ this.ctx.storage.sql.exec(`
26025
+ CREATE TABLE IF NOT EXISTS user_values (
26026
+ user_id TEXT NOT NULL,
26027
+ key TEXT NOT NULL,
26028
+ value TEXT NOT NULL,
26029
+ created_at INTEGER NOT NULL,
26030
+ updated_at INTEGER NOT NULL,
26031
+ PRIMARY KEY (user_id, key)
26032
+ )
26033
+ `);
26034
+ this.ctx.storage.sql.exec(
26035
+ `CREATE INDEX IF NOT EXISTS idx_user_values_user ON user_values(user_id)`
26036
+ );
26037
+ this.migrated = true;
26038
+ }
26039
+ // ─── Persistent client socket ──────────────────────────────────────────────
26040
+ /**
26041
+ * Accept the user's persistent client (daemon) WebSocket. The daemon holds
26042
+ * this ONE socket for its whole lifetime; per-thread bridge sockets are
26043
+ * opened separately and only while a thread is active.
26044
+ */
26045
+ async fetch(request) {
26046
+ const url = new URL(request.url);
26047
+ if (request.headers.get("Upgrade") === "websocket") {
26048
+ const pair = new WebSocketPair();
26049
+ const [client, server] = [pair[0], pair[1]];
26050
+ const info = {
26051
+ clientId: url.searchParams.get("client_id"),
26052
+ clientName: url.searchParams.get("client_name"),
26053
+ clientKind: url.searchParams.get("client_kind")
26054
+ };
26055
+ this.ctx.acceptWebSocket(server);
26056
+ server.serializeAttachment({ type: "hub_client", ...info });
26057
+ this.clientSockets.add(server);
26058
+ this.clientInfo.set(server, info);
26059
+ try {
26060
+ server.send(JSON.stringify({ type: "hub_ready" }));
26061
+ } catch {
26062
+ }
26063
+ return new Response(null, { status: 101, webSocket: client });
26064
+ }
26065
+ return new Response("Not found", { status: 404 });
26066
+ }
26067
+ async webSocketMessage(ws, _message) {
26068
+ if (!this.clientSockets.has(ws)) this.clientSockets.add(ws);
26069
+ }
26070
+ async webSocketClose(ws) {
26071
+ this.clientSockets.delete(ws);
26072
+ this.clientInfo.delete(ws);
26073
+ }
26074
+ async webSocketError(ws) {
26075
+ this.clientSockets.delete(ws);
26076
+ this.clientInfo.delete(ws);
26077
+ }
26078
+ /** Whether a persistent client (daemon) is connected right now. */
26079
+ async hasClient() {
26080
+ for (const ws of this.clientSockets) {
26081
+ if (ws.readyState === WebSocket.OPEN) return true;
26082
+ }
26083
+ return false;
26084
+ }
26085
+ // ─── Wake orchestration (the load-bearing RPC) ─────────────────────────────
26086
+ /**
26087
+ * Signal the connected daemon to attach a bridge to `threadId`. Called by a
26088
+ * cold thread DO that parked a forwarded call with no client attached. The
26089
+ * daemon reacts by opening a per-thread bridge socket, which triggers the
26090
+ * thread's drain-on-attach. Returns whether a daemon was reachable.
26091
+ */
26092
+ async requestWake(threadId) {
26093
+ const frame = { type: "wake", threadId };
26094
+ const payload = JSON.stringify(frame);
26095
+ let delivered = false;
26096
+ for (const ws of Array.from(this.clientSockets)) {
26097
+ if (ws.readyState !== WebSocket.OPEN) {
26098
+ this.clientSockets.delete(ws);
26099
+ this.clientInfo.delete(ws);
26100
+ continue;
26101
+ }
26102
+ try {
26103
+ ws.send(payload);
26104
+ delivered = true;
26105
+ } catch {
26106
+ this.clientSockets.delete(ws);
26107
+ this.clientInfo.delete(ws);
26108
+ }
26109
+ }
26110
+ return { delivered };
26111
+ }
26112
+ // ─── Account-wide KV (sharded off the singleton) ───────────────────────────
26113
+ async getUserValue(userId, key) {
26114
+ this.ensureMigrated();
26115
+ const cursor = await this.ctx.storage.sql.exec(
26116
+ `SELECT value FROM user_values WHERE user_id = ? AND key = ? LIMIT 1`,
26117
+ userId,
26118
+ key
26119
+ );
26120
+ const raw = cursor.toArray()[0]?.value;
26121
+ if (raw === void 0) return null;
26122
+ try {
26123
+ return JSON.parse(raw);
26124
+ } catch {
26125
+ return null;
26126
+ }
26127
+ }
26128
+ async setUserValue(userId, key, value) {
26129
+ this.ensureMigrated();
26130
+ if (value === null || value === void 0) {
26131
+ await this.ctx.storage.sql.exec(
26132
+ `DELETE FROM user_values WHERE user_id = ? AND key = ?`,
26133
+ userId,
26134
+ key
26135
+ );
26136
+ return;
26137
+ }
26138
+ const serialized = JSON.stringify(value);
26139
+ const now = Date.now();
26140
+ await this.ctx.storage.sql.exec(
26141
+ `INSERT INTO user_values (user_id, key, value, created_at, updated_at)
26142
+ VALUES (?1, ?2, ?3, ?4, ?4)
26143
+ ON CONFLICT(user_id, key) DO UPDATE SET
26144
+ value = excluded.value,
26145
+ updated_at = excluded.updated_at`,
26146
+ userId,
26147
+ key,
26148
+ serialized,
26149
+ now
26150
+ );
26151
+ }
26152
+ async listUserValues(userId, options = {}) {
26153
+ this.ensureMigrated();
26154
+ const limit = Math.min(Math.max(Math.trunc(options.limit ?? 100), 1), 500);
26155
+ const offset = Math.max(Math.trunc(options.offset ?? 0), 0);
26156
+ const prefix = typeof options.prefix === "string" && options.prefix.length > 0 ? options.prefix : null;
26157
+ const escapedPrefix = prefix ? `${prefix.replace(/[\\%_]/g, (m) => `\\${m}`)}%` : null;
26158
+ const where = escapedPrefix ? `WHERE user_id = ? AND key LIKE ? ESCAPE '\\'` : `WHERE user_id = ?`;
26159
+ const whereArgs = escapedPrefix ? [userId, escapedPrefix] : [userId];
26160
+ const totalCursor = await this.ctx.storage.sql.exec(
26161
+ `SELECT COUNT(*) AS total FROM user_values ${where}`,
26162
+ ...whereArgs
26163
+ );
26164
+ const total = Number(totalCursor.toArray()[0]?.total ?? 0);
26165
+ const cursor = await this.ctx.storage.sql.exec(
26166
+ `SELECT key, value, updated_at FROM user_values ${where}
26167
+ ORDER BY key ASC LIMIT ? OFFSET ?`,
26168
+ ...whereArgs,
26169
+ limit,
26170
+ offset
26171
+ );
26172
+ const entries = cursor.toArray().map((row) => {
26173
+ let value = null;
26174
+ try {
26175
+ value = JSON.parse(row.value);
26176
+ } catch {
26177
+ value = null;
26178
+ }
26179
+ return { key: row.key, value, updated_at: Number(row.updated_at) };
26180
+ });
26181
+ return { entries, total, limit, offset };
26182
+ }
26183
+ };
25344
26184
 
25345
26185
  // src/runtime.ts
25346
26186
  init_utilities();
@@ -25703,6 +26543,6 @@ init_context();
25703
26543
  init_ProviderRegistry();
25704
26544
  init_types2();
25705
26545
 
25706
- export { CodeExecutionBridge, DurableAgentBuilder, DurableThread, FlowStateSdk, NamespaceResolutionError, ProviderRegistry, ThreadWatchdog, authenticate, buildImageDescription, cat, createNamespaceContext, createThreadEndpointHandler, defineController2 as defineController, emitThreadEvent, enhanceFlowState, exists, find, forceTurn, generateImageDescription, getFileStats, getMessages, getMessagesToSummarize, getShortName, getThumbnail, getUnsummarizedImageAttachments, getVisibleAgentNames, getVisibleModelNames, getVisiblePromptNames, getVisibleToolNames, grep, hasImageAttachments, head, injectMessage, isQualifiedName, linkFile, mkdir, move, optimizeImageContext, parseQualifiedName, qualifyName, queueTool, readFile, readdir, reloadHistory, rename, replaceImagesWithDescriptions, requireAdmin, requireAuth, resolveAgent, resolveHook, resolveModel, resolvePrompt, resolveTool, rmdir, stat, tail, unlink, updateThread, writeFile, writeImage };
26546
+ export { CLIENT_STREAM_CHUNK_BYTES, CodeExecutionBridge, DurableAgentBuilder, DurableThread, DurableUserHub, FlowStateSdk, NamespaceResolutionError, ProviderRegistry, ThreadWatchdog, authenticate, buildImageDescription, callClient, callClientStream, cat, createNamespaceContext, createThreadEndpointHandler, decodeStreamChunk, defineController2 as defineController, emitThreadEvent, encodeStreamChunk, enhanceFlowState, exists, find, forceTurn, generateImageDescription, getFileStats, getMessages, getMessagesToSummarize, getShortName, getThumbnail, getUnsummarizedImageAttachments, getVisibleAgentNames, getVisibleModelNames, getVisiblePromptNames, getVisibleToolNames, grep, hasImageAttachments, head, injectMessage, isQualifiedName, linkFile, mkdir, move, optimizeImageContext, parseQualifiedName, qualifyName, queueTool, readFile, readdir, reloadHistory, rename, replaceImagesWithDescriptions, requireAdmin, requireAuth, resolveAgent, resolveHook, resolveModel, resolvePrompt, resolveTool, rmdir, stat, supportsClientForwarding, tail, unlink, updateThread, writeFile, writeImage };
25707
26547
  //# sourceMappingURL=runtime.js.map
25708
26548
  //# sourceMappingURL=runtime.js.map