@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/index.js CHANGED
@@ -37,6 +37,55 @@ var __copyProps = (to, from, except, desc) => {
37
37
  };
38
38
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
39
39
 
40
+ // src/agents/client-forwarding.ts
41
+ function asForwarding(state) {
42
+ return state;
43
+ }
44
+ function supportsClientForwarding(state) {
45
+ const s = asForwarding(state);
46
+ return typeof s.callClient === "function" && typeof s.callClientStream === "function";
47
+ }
48
+ function callClient(state, request) {
49
+ const s = asForwarding(state);
50
+ if (typeof s.callClient !== "function") {
51
+ return Promise.resolve({
52
+ ok: false,
53
+ error: "This thread runtime cannot forward tools to a client. Client forwarding requires the DurableThread bridge."
54
+ });
55
+ }
56
+ return s.callClient(request);
57
+ }
58
+ function callClientStream(state, request) {
59
+ const s = asForwarding(state);
60
+ if (typeof s.callClientStream !== "function") {
61
+ return Promise.resolve({
62
+ ok: false,
63
+ error: "This thread runtime cannot open client streams. Client streaming requires the DurableThread bridge."
64
+ });
65
+ }
66
+ return s.callClientStream(request);
67
+ }
68
+ function encodeStreamChunk(bytes) {
69
+ let binary = "";
70
+ const step = 32768;
71
+ for (let i = 0; i < bytes.length; i += step) {
72
+ binary += String.fromCharCode(...bytes.subarray(i, i + step));
73
+ }
74
+ return btoa(binary);
75
+ }
76
+ function decodeStreamChunk(data) {
77
+ const binary = atob(data);
78
+ const bytes = new Uint8Array(binary.length);
79
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
80
+ return bytes;
81
+ }
82
+ var CLIENT_STREAM_CHUNK_BYTES;
83
+ var init_client_forwarding = __esm({
84
+ "src/agents/client-forwarding.ts"() {
85
+ CLIENT_STREAM_CHUNK_BYTES = 256 * 1024;
86
+ }
87
+ });
88
+
40
89
  // src/agents/types.ts
41
90
  var SUBAGENT_INITIAL_ARGUMENTS_THREAD_PROPERTY, MAX_STEPS, MAX_RETRIES_PER_MODEL, TIMESTAMP_MULTIPLIER, STREAM_COOLDOWN, BACKOFF_BASE;
42
91
  var init_types = __esm({
@@ -929,6 +978,20 @@ var init_ProviderRegistry = __esm({
929
978
  const threadEnv = state.thread.metadata?.env ?? state.thread.metadata?.tenvs ?? null;
930
979
  const agentBuilderId = state.env.AGENT_BUILDER.idFromName("singleton");
931
980
  const agentBuilder = state.env.AGENT_BUILDER.get(agentBuilderId);
981
+ let instanceEnv = null;
982
+ if (typeof agentBuilder.getInstanceEnvEntries === "function") {
983
+ try {
984
+ const entries = await agentBuilder.getInstanceEnvEntries();
985
+ if (Array.isArray(entries) && entries.length > 0) {
986
+ instanceEnv = {};
987
+ for (const entry of entries) {
988
+ if (entry?.name) instanceEnv[entry.name] = entry.value;
989
+ }
990
+ }
991
+ } catch (error) {
992
+ console.warn("buildThreadScopedEnv: instance env lookup failed:", error);
993
+ }
994
+ }
932
995
  let userId = state.thread.metadata?.user_id ?? null;
933
996
  let parent = state.thread.metadata?.parent ?? null;
934
997
  let hops = 0;
@@ -965,11 +1028,12 @@ var init_ProviderRegistry = __esm({
965
1028
  console.warn("buildThreadScopedEnv:", diagnostic);
966
1029
  }
967
1030
  }
968
- if (!userEnv && !threadEnv) {
1031
+ if (!instanceEnv && !userEnv && !threadEnv) {
969
1032
  this.scopeDiagnostics.set(state.env, diagnostic);
970
1033
  return state.env;
971
1034
  }
972
- const merged = { ...state.env, ...userEnv ?? {}, ...threadEnv ?? {} };
1035
+ if (instanceEnv) diagnostic = `${diagnostic ? diagnostic + "; " : ""}instance env: ${Object.keys(instanceEnv).length} entries`;
1036
+ const merged = { ...state.env, ...instanceEnv ?? {}, ...userEnv ?? {}, ...threadEnv ?? {} };
973
1037
  this.scopeDiagnostics.set(merged, diagnostic);
974
1038
  return merged;
975
1039
  }
@@ -983,7 +1047,7 @@ var init_ProviderRegistry = __esm({
983
1047
  * @param thread - Thread instance for loading model from virtual modules
984
1048
  * @returns Provider and model definition
985
1049
  */
986
- async getProvider(modelName, env, thread) {
1050
+ async getProvider(modelName, env, thread, getState) {
987
1051
  if (!thread) {
988
1052
  throw new Error("Thread instance required for model lookup");
989
1053
  }
@@ -1016,6 +1080,18 @@ var init_ProviderRegistry = __esm({
1016
1080
  );
1017
1081
  }
1018
1082
  const config = resolved.providerConfig;
1083
+ if (typeof providerDefinition.createFetch === "function") {
1084
+ const state = getState ? await getState() : void 0;
1085
+ const configWithFetch = {
1086
+ ...config,
1087
+ fetch: providerDefinition.createFetch({
1088
+ providerName,
1089
+ config,
1090
+ state
1091
+ })
1092
+ };
1093
+ return this.buildResult(providerFactory(configWithFetch), modelDef, false);
1094
+ }
1019
1095
  const fingerprint = this.getProviderFingerprint(providerName, modelDef, config);
1020
1096
  const cached = this.providerCache.get(modelName);
1021
1097
  if (cached?.fingerprint === fingerprint) {
@@ -1888,7 +1964,16 @@ var init_LLMRequest = __esm({
1888
1964
  static async callModel(modelId, context, state, logId) {
1889
1965
  const { ProviderRegistry: ProviderRegistry2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
1890
1966
  const providerEnv2 = await ProviderRegistry2.buildThreadScopedEnv(state);
1891
- const { provider, modelName, modelDef, viaPlatform, platformProxyOrigin } = await ProviderRegistry2.getProvider(modelId, providerEnv2, state.thread.instance);
1967
+ const getThreadState = async () => {
1968
+ const { ThreadStateImpl: ThreadStateImpl2 } = await Promise.resolve().then(() => (init_ThreadStateImpl(), ThreadStateImpl_exports));
1969
+ return ThreadStateImpl2.fromFlowState(state);
1970
+ };
1971
+ const { provider, modelName, modelDef, viaPlatform, platformProxyOrigin } = await ProviderRegistry2.getProvider(
1972
+ modelId,
1973
+ providerEnv2,
1974
+ state.thread.instance,
1975
+ getThreadState
1976
+ );
1892
1977
  const streamAbort = new AbortController();
1893
1978
  const parentSignal = state.abortController?.signal;
1894
1979
  let onParentAbort;
@@ -6442,12 +6527,14 @@ var init_FlowEngine = __esm({
6442
6527
  } catch (error) {
6443
6528
  console.error("Fatal error in executeAsync:", error);
6444
6529
  await this.logError(state, error, "fatal_execution_error", "FlowEngine.executeAsync");
6530
+ await this.settleTerminalFailure(state, error);
6445
6531
  throw error;
6446
6532
  }
6447
6533
  } else {
6448
6534
  this.executeAsync(state, state.threadId).catch(async (error) => {
6449
6535
  console.error("Fatal error in executeAsync:", error);
6450
6536
  await this.logError(state, error, "fatal_execution_error", "FlowEngine.executeAsync");
6537
+ await this.settleTerminalFailure(state, error);
6451
6538
  });
6452
6539
  }
6453
6540
  return {
@@ -6501,10 +6588,27 @@ var init_FlowEngine = __esm({
6501
6588
  console.warn(
6502
6589
  `[FlowEngine] Finalized ${rows.length} orphaned incomplete log(s) for thread ${state.threadId}`
6503
6590
  );
6591
+ await state.thread.instance.failPendingMessagesAndBroadcast?.();
6504
6592
  } catch (err) {
6505
6593
  console.error("[FlowEngine] Failed to finalize orphaned logs (non-fatal):", err);
6506
6594
  }
6507
6595
  }
6596
+ /**
6597
+ * A terminal execution failure ended the run without a normal assistant
6598
+ * reply. Persist a durable failed assistant message + settle pending
6599
+ * messages so every connected head lands on a visible end state instead of
6600
+ * a stuck "Working" spinner. Best-effort — never masks the original error.
6601
+ */
6602
+ static async settleTerminalFailure(state, error) {
6603
+ try {
6604
+ const detail = error instanceof Error ? error.message : String(error);
6605
+ await state.thread.instance.settleTerminalFailure?.(
6606
+ `The agent run failed unexpectedly: ${detail}`
6607
+ );
6608
+ } catch (settleError) {
6609
+ console.error("[FlowEngine] Failed to settle terminal failure (non-fatal):", settleError);
6610
+ }
6611
+ }
6508
6612
  static async executeAsync(state, threadId) {
6509
6613
  const effectiveMaxSteps = Math.max(
6510
6614
  MAX_STEPS,
@@ -12779,6 +12883,7 @@ function runCodeViaThreadRpc(threadInstance, threadId, source, options) {
12779
12883
  var ThreadStateImpl, ExecutionStateImpl;
12780
12884
  var init_ThreadStateImpl = __esm({
12781
12885
  "src/agents/ThreadStateImpl.ts"() {
12886
+ init_client_forwarding();
12782
12887
  init_types();
12783
12888
  init_utilities();
12784
12889
  init_code_execution();
@@ -13363,9 +13468,10 @@ var init_ThreadStateImpl = __esm({
13363
13468
  }
13364
13469
  const instance = this._threadInstance;
13365
13470
  const toolCallId = this._flowState?.currentToolCall?.id;
13366
- const mustPark = request.requestPermission != null && await this.forwardedCallNeedsApproval(request);
13367
- if (mustPark && toolCallId && this._executionState && typeof instance.dispatchClientTool === "function" && typeof instance.hasBridge === "function" && await instance.hasBridge()) {
13368
- const side = this._flowState?.currentSide === "b" ? "b" : "a";
13471
+ const canPark = !!(toolCallId && this._executionState && typeof instance.dispatchClientTool === "function");
13472
+ const side = this._flowState?.currentSide === "b" ? "b" : "a";
13473
+ const mustParkForApproval = request.requestPermission != null && await this.forwardedCallNeedsApproval(request);
13474
+ if (mustParkForApproval && canPark && toolCallId && typeof instance.hasBridge === "function" && await instance.hasBridge()) {
13369
13475
  await instance.dispatchClientTool(request, toolCallId, side);
13370
13476
  this._executionState.stop();
13371
13477
  return {
@@ -13377,6 +13483,55 @@ var init_ThreadStateImpl = __esm({
13377
13483
  const local = await this._threadInstance.callClientTool(request);
13378
13484
  if (local.ok || !this.isNoClient(local)) return local;
13379
13485
  const env = this._threadInstance.env;
13486
+ if (env?.AGENT_BUILDER_THREAD) {
13487
+ const seen = /* @__PURE__ */ new Set([this._metadata.id]);
13488
+ let parentId = await this.resolveParentThreadId(this._metadata.id);
13489
+ while (parentId && !seen.has(parentId)) {
13490
+ seen.add(parentId);
13491
+ try {
13492
+ const stub = env.AGENT_BUILDER_THREAD.get(env.AGENT_BUILDER_THREAD.idFromName(parentId));
13493
+ const res = await stub.callClientTool(request);
13494
+ if (res.ok || !this.isNoClient(res)) return res;
13495
+ } catch {
13496
+ }
13497
+ parentId = await this.resolveParentThreadId(parentId);
13498
+ }
13499
+ }
13500
+ if (canPark && toolCallId) {
13501
+ await instance.dispatchClientTool(request, toolCallId, side);
13502
+ if (typeof instance.notifyClientWaiting === "function") {
13503
+ try {
13504
+ await instance.notifyClientWaiting(toolCallId, this._metadata.id, this._metadata.user_id ?? null);
13505
+ } catch {
13506
+ }
13507
+ }
13508
+ this._executionState.stop();
13509
+ return {
13510
+ ok: true,
13511
+ result: `\u23F3 Waiting for your machine to reconnect to run ${request.tool}\u2026`,
13512
+ data: { deferred: true, parked: true, waitingForClient: true, toolCallId }
13513
+ };
13514
+ }
13515
+ return local;
13516
+ }
13517
+ /**
13518
+ * Open a bidirectional byte stream to the connected client (e.g. a raw TCP
13519
+ * tunnel for Worker-side TLS termination). Streams require a LIVE bridge on
13520
+ * this thread and never park — a stream is only meaningful while the client
13521
+ * is attached. Subagents must not call this directly; they proxy whole
13522
+ * requests up to the root thread instead (request-up / stream-down).
13523
+ */
13524
+ async callClientStream(request) {
13525
+ const instance = this._threadInstance;
13526
+ if (typeof instance.openClientStream !== "function") {
13527
+ return {
13528
+ ok: false,
13529
+ error: "This thread runtime cannot open client streams. Client streaming requires the DurableThread bridge."
13530
+ };
13531
+ }
13532
+ const local = await instance.openClientStream(request);
13533
+ if (local.ok || !this.isNoClient(local)) return local;
13534
+ const env = this._threadInstance.env;
13380
13535
  if (!env?.AGENT_BUILDER_THREAD) return local;
13381
13536
  const seen = /* @__PURE__ */ new Set([this._metadata.id]);
13382
13537
  let parentId = await this.resolveParentThreadId(this._metadata.id);
@@ -13384,14 +13539,86 @@ var init_ThreadStateImpl = __esm({
13384
13539
  seen.add(parentId);
13385
13540
  try {
13386
13541
  const stub = env.AGENT_BUILDER_THREAD.get(env.AGENT_BUILDER_THREAD.idFromName(parentId));
13387
- const res = await stub.callClientTool(request);
13388
- if (res.ok || !this.isNoClient(res)) return res;
13542
+ if (typeof stub.proxyStreamOpen === "function") {
13543
+ const opened = await stub.proxyStreamOpen(request);
13544
+ if (opened.ok && opened.id) {
13545
+ return { ok: true, duplex: this.buildProxyDuplex(stub, opened.id) };
13546
+ }
13547
+ if (opened.error && !opened.noClient) {
13548
+ return { ok: false, error: opened.error };
13549
+ }
13550
+ }
13389
13551
  } catch {
13390
13552
  }
13391
13553
  parentId = await this.resolveParentThreadId(parentId);
13392
13554
  }
13393
13555
  return local;
13394
13556
  }
13557
+ /**
13558
+ * Build a client duplex that bridges to a stream opened on an ancestor DO
13559
+ * (the bridge holder) via chunk-pull RPCs. `readable` polls `proxyStreamPull`;
13560
+ * `writable` forwards each chunk with `proxyStreamWrite`; teardown calls
13561
+ * `proxyStreamAbort`.
13562
+ */
13563
+ buildProxyDuplex(stub, id) {
13564
+ let settleDone;
13565
+ const done = new Promise((r) => settleDone = r);
13566
+ let closed = false;
13567
+ const readable = new ReadableStream({
13568
+ async pull(controller) {
13569
+ while (!closed) {
13570
+ let res;
13571
+ try {
13572
+ res = await stub.proxyStreamPull(id);
13573
+ } catch (error) {
13574
+ controller.error(error instanceof Error ? error : new Error(String(error)));
13575
+ settleDone({ ok: false, error: "proxy stream pull failed" });
13576
+ return;
13577
+ }
13578
+ for (const chunk of res.chunks) {
13579
+ controller.enqueue(decodeStreamChunk(chunk));
13580
+ }
13581
+ if (res.done) {
13582
+ controller.close();
13583
+ settleDone(res.error ? { ok: false, error: res.error } : { ok: true });
13584
+ return;
13585
+ }
13586
+ if (res.chunks.length > 0) return;
13587
+ await new Promise((r) => setTimeout(r, 25));
13588
+ }
13589
+ },
13590
+ cancel() {
13591
+ closed = true;
13592
+ void stub.proxyStreamAbort(id).catch(() => {
13593
+ });
13594
+ }
13595
+ });
13596
+ const writable = new WritableStream({
13597
+ async write(chunk) {
13598
+ await stub.proxyStreamWrite(id, encodeStreamChunk(chunk));
13599
+ },
13600
+ close() {
13601
+ void stub.proxyStreamEnd?.(id).catch?.(() => {
13602
+ });
13603
+ },
13604
+ abort() {
13605
+ closed = true;
13606
+ void stub.proxyStreamAbort(id).catch(() => {
13607
+ });
13608
+ }
13609
+ });
13610
+ return {
13611
+ readable,
13612
+ writable,
13613
+ close: (error) => {
13614
+ closed = true;
13615
+ void stub.proxyStreamAbort(id).catch(() => {
13616
+ });
13617
+ settleDone(error ? { ok: false, error } : { ok: true });
13618
+ },
13619
+ done
13620
+ };
13621
+ }
13395
13622
  isNoClient(res) {
13396
13623
  return res.ok === false && !!res.data && typeof res.data === "object" && res.data.noClient === true;
13397
13624
  }
@@ -14240,6 +14467,7 @@ declare module 'virtual:@standardagents/builder' {
14240
14467
  import type { DurableAgentBuilder as BaseDurableAgentBuilder } from '@standardagents/builder/runtime';
14241
14468
  import type { CodeExecutionBridge as BaseCodeExecutionBridge } from '@standardagents/builder/runtime';
14242
14469
  import type { ThreadWatchdog as BaseThreadWatchdog } from '@standardagents/builder/runtime';
14470
+ import type { DurableUserHub as BaseDurableUserHub } from '@standardagents/builder/runtime';
14243
14471
  import type { ThreadEnv } from '@standardagents/builder/runtime';
14244
14472
 
14245
14473
  // ============================================================
@@ -14724,6 +14952,12 @@ declare module 'virtual:@standardagents/builder' {
14724
14952
  */
14725
14953
  export class ThreadWatchdog extends BaseThreadWatchdog {}
14726
14954
 
14955
+ /**
14956
+ * Per-user hub Durable Object (AGENT_BUILDER_USER_HUB binding). Holds the
14957
+ * persistent daemon socket, wake signal, and sharded account KV.
14958
+ */
14959
+ export class DurableUserHub extends BaseDurableUserHub {}
14960
+
14727
14961
  // ============================================================
14728
14962
  // Router Function
14729
14963
  // ============================================================
@@ -20895,6 +21129,7 @@ import { DurableThread as _BaseDurableThread } from '@standardagents/builder/run
20895
21129
  import { DurableAgentBuilder as _BaseDurableAgentBuilder } from '@standardagents/builder/runtime';
20896
21130
  import { CodeExecutionBridge as _BaseCodeExecutionBridge } from '@standardagents/builder/runtime';
20897
21131
  import { ThreadWatchdog as _BaseThreadWatchdog } from '@standardagents/builder/runtime';
21132
+ import { DurableUserHub as _BaseDurableUserHub } from '@standardagents/builder/runtime';
20898
21133
 
20899
21134
  // Import sip WASM module and readiness helper
20900
21135
  // Static import allows workerd to pre-compile the WASM at bundle time
@@ -21020,6 +21255,12 @@ export class CodeExecutionBridge extends _BaseCodeExecutionBridge {}
21020
21255
  * probes its DurableThread over RPC.
21021
21256
  */
21022
21257
  export class ThreadWatchdog extends _BaseThreadWatchdog {}
21258
+
21259
+ /**
21260
+ * Per-user hub companion. Coordination only (persistent daemon socket, wake
21261
+ * signal, sharded account KV); needs no virtual-module registries.
21262
+ */
21263
+ export class DurableUserHub extends _BaseDurableUserHub {}
21023
21264
  `;
21024
21265
  }
21025
21266
  async function loadRegistryVirtualModule(context) {
@@ -21995,6 +22236,10 @@ function createAgentBuilderWorkerConfig(options) {
21995
22236
  {
21996
22237
  name: "AGENT_BUILDER_WATCHDOG",
21997
22238
  class_name: "ThreadWatchdog"
22239
+ },
22240
+ {
22241
+ name: "AGENT_BUILDER_USER_HUB",
22242
+ class_name: "DurableUserHub"
21998
22243
  }
21999
22244
  ]
22000
22245
  },
@@ -22012,6 +22257,10 @@ function createAgentBuilderWorkerConfig(options) {
22012
22257
  {
22013
22258
  tag: "v3",
22014
22259
  new_sqlite_classes: ["ThreadWatchdog"]
22260
+ },
22261
+ {
22262
+ tag: "v4",
22263
+ new_sqlite_classes: ["DurableUserHub"]
22015
22264
  }
22016
22265
  ]
22017
22266
  };
@@ -22099,7 +22348,7 @@ function agentbuilder(options = {}) {
22099
22348
  function writeGeneratedWorkerEntry() {
22100
22349
  const content = `// Generated by @standardagents/builder.
22101
22350
  // Future agents: update builder/src/plugin.ts when changing this entrypoint.
22102
- import { CodeExecutionBridge, DurableAgentBuilder, DurableThread, ThreadWatchdog, router } from "virtual:@standardagents/builder";
22351
+ import { CodeExecutionBridge, DurableAgentBuilder, DurableThread, ThreadWatchdog, DurableUserHub, router } from "virtual:@standardagents/builder";
22103
22352
 
22104
22353
  export default {
22105
22354
  async fetch(request: Request, env: Parameters<typeof router>[1]) {
@@ -22108,7 +22357,7 @@ export default {
22108
22357
  },
22109
22358
  };
22110
22359
 
22111
- export { CodeExecutionBridge, DurableAgentBuilder, DurableThread, ThreadWatchdog };
22360
+ export { CodeExecutionBridge, DurableAgentBuilder, DurableThread, ThreadWatchdog, DurableUserHub };
22112
22361
  `;
22113
22362
  fs8__default.mkdirSync(outputDir, { recursive: true });
22114
22363
  if (!fs8__default.existsSync(generatedWorkerEntry) || fs8__default.readFileSync(generatedWorkerEntry, "utf-8") !== content) {
@@ -22575,6 +22824,9 @@ async function requireAdmin(request, env) {
22575
22824
  return result;
22576
22825
  }
22577
22826
 
22827
+ // src/index.ts
22828
+ init_client_forwarding();
22829
+
22578
22830
  // src/durable-objects/migrations/001_initial.ts
22579
22831
  var migration = {
22580
22832
  version: 1,
@@ -24250,6 +24502,7 @@ function isQualifiedName(name) {
24250
24502
  }
24251
24503
 
24252
24504
  // src/durable-objects/DurableThread.ts
24505
+ init_client_forwarding();
24253
24506
  init_sessionTools();
24254
24507
 
24255
24508
  // src/durable-objects/bridge-owner.ts
@@ -24480,6 +24733,11 @@ var DurableThread = class _DurableThread extends DurableObject {
24480
24733
  // setValue() so external KV writes (release/reassign) invalidate it too.
24481
24734
  executionOwnerCache = void 0;
24482
24735
  pendingClientCalls = /* @__PURE__ */ new Map();
24736
+ // Live bidirectional client byte streams (e.g. TCP tunnels for Worker-side
24737
+ // TLS). Keyed by stream id; each stream is bound to the bridge socket it was
24738
+ // opened on and dies with it. Streams are in-memory only — they exist solely
24739
+ // while the client is attached and never park durably.
24740
+ clientStreams = /* @__PURE__ */ new Map();
24483
24741
  alarmQueue;
24484
24742
  alarmQueueRearmed = false;
24485
24743
  currentAbortController = null;
@@ -24539,6 +24797,9 @@ var DurableThread = class _DurableThread extends DurableObject {
24539
24797
  case "watchdogCheck":
24540
24798
  await this.watchdogCheck(args.threadId);
24541
24799
  break;
24800
+ case "clientParkTimeout":
24801
+ await this.failParkedClientCall(args.toolCallId, args.threadId);
24802
+ break;
24542
24803
  default:
24543
24804
  throw new Error(`Unknown method: ${method}`);
24544
24805
  }
@@ -26522,6 +26783,69 @@ ${resultContent}${attachmentSummary}`;
26522
26783
  * Stop the currently executing thread (RPC method)
26523
26784
  * Simple "off" switch - stops turns, only cleared by user messages
26524
26785
  */
26786
+ /**
26787
+ * Flip every still-`pending` message to `failed` and broadcast the change to
26788
+ * all connected heads. This is THE settle path for any terminal condition
26789
+ * that leaves a turn half-open — user stop, lost execution, terminal
26790
+ * execution failure, and expired parked forwarded calls — so no head is
26791
+ * left rendering a stuck "Working" state. Returns the failure timestamp.
26792
+ */
26793
+ async failPendingMessagesAndBroadcast() {
26794
+ const now = Date.now() * TIMESTAMP_MULTIPLIER;
26795
+ await this.ctx.storage.sql.exec(
26796
+ `UPDATE messages SET status = 'failed', response_completed_at = ? WHERE status = 'pending'`,
26797
+ now
26798
+ );
26799
+ const cursor = await this.ctx.storage.sql.exec(
26800
+ `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 = ?`,
26801
+ now
26802
+ );
26803
+ for (const row of cursor) {
26804
+ this.broadcastMessage({
26805
+ id: row.id,
26806
+ role: row.role,
26807
+ content: row.content,
26808
+ tool_calls: row.tool_calls,
26809
+ tool_call_id: row.tool_call_id,
26810
+ tool_status: row.tool_status,
26811
+ created_at: row.created_at,
26812
+ log_id: row.log_id,
26813
+ status: "failed",
26814
+ request_sent_at: row.request_sent_at,
26815
+ response_completed_at: row.response_completed_at
26816
+ });
26817
+ }
26818
+ return now;
26819
+ }
26820
+ /**
26821
+ * A terminal failure ended a turn without a normal assistant reply: settle
26822
+ * every pending message and persist a durable `failed` assistant message so
26823
+ * all connected heads land on a real, visible end state — never a stuck
26824
+ * "Working" spinner. Used by the FlowEngine's terminal catch and the parked
26825
+ * forwarded-call TTL.
26826
+ */
26827
+ async settleTerminalFailure(errorMessage) {
26828
+ await this.ensureMigrated();
26829
+ await this.failPendingMessagesAndBroadcast();
26830
+ const messageId = crypto.randomUUID();
26831
+ const timestamp = Date.now() * TIMESTAMP_MULTIPLIER;
26832
+ await this.ctx.storage.sql.exec(
26833
+ `INSERT INTO messages (id, role, content, created_at, status, prompt) VALUES (?, ?, ?, ?, ?, ?)`,
26834
+ messageId,
26835
+ "assistant",
26836
+ errorMessage,
26837
+ timestamp,
26838
+ "failed",
26839
+ null
26840
+ );
26841
+ this.broadcastMessage({
26842
+ id: messageId,
26843
+ role: "assistant",
26844
+ content: errorMessage,
26845
+ created_at: timestamp,
26846
+ status: "failed"
26847
+ });
26848
+ }
26525
26849
  async stop() {
26526
26850
  await this.ensureMigrated();
26527
26851
  try {
@@ -26531,30 +26855,7 @@ ${resultContent}${attachmentSummary}`;
26531
26855
  if (this.currentAbortController) {
26532
26856
  this.currentAbortController.abort();
26533
26857
  }
26534
- const now = Date.now() * TIMESTAMP_MULTIPLIER;
26535
- const updateCursor = await this.ctx.storage.sql.exec(
26536
- `UPDATE messages SET status = 'failed', response_completed_at = ? WHERE status = 'pending'`,
26537
- now
26538
- );
26539
- const cursor = await this.ctx.storage.sql.exec(
26540
- `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 = ?`,
26541
- now
26542
- );
26543
- for (const row of cursor) {
26544
- this.broadcastMessage({
26545
- id: row.id,
26546
- role: row.role,
26547
- content: row.content,
26548
- tool_calls: row.tool_calls,
26549
- tool_call_id: row.tool_call_id,
26550
- tool_status: row.tool_status,
26551
- created_at: row.created_at,
26552
- log_id: row.log_id,
26553
- status: "failed",
26554
- request_sent_at: row.request_sent_at,
26555
- response_completed_at: row.response_completed_at
26556
- });
26557
- }
26858
+ const now = await this.failPendingMessagesAndBroadcast();
26558
26859
  const messagesWithToolsCursor = await this.ctx.storage.sql.exec(
26559
26860
  `SELECT id, tool_calls, depth FROM messages WHERE tool_calls IS NOT NULL AND tool_calls != 'null' AND (depth IS NULL OR depth = 0)`
26560
26861
  );
@@ -28031,6 +28332,7 @@ ${resultContent}${attachmentSummary}`;
28031
28332
  socket.close();
28032
28333
  } catch {
28033
28334
  }
28335
+ this.failClientStreamsForSocket(socket);
28034
28336
  this.failPendingClientCallsIfNoBridge();
28035
28337
  }
28036
28338
  /**
@@ -28286,6 +28588,352 @@ ${resultContent}${attachmentSummary}`;
28286
28588
  }
28287
28589
  return { side };
28288
28590
  }
28591
+ // ─── Client byte streams (Worker-side TLS tunnels) ─────────────────────────
28592
+ // A stream is a bidirectional byte pipe over the bridge WebSocket: the
28593
+ // client opens a raw TCP socket per the stream's `meta` and shuttles bytes
28594
+ // both ways; the Worker terminates TLS over those bytes (node:tls over the
28595
+ // duplex), so the client only ever relays ciphertext. Streams are bound to a
28596
+ // live socket and never park.
28597
+ /** Default time to wait for the client to accept a stream open. */
28598
+ static CLIENT_STREAM_OPEN_TIMEOUT_MS = 3e4;
28599
+ /** Open a bidirectional byte stream to the connected owner client. */
28600
+ async openClientStream(request) {
28601
+ const owner = await this.getExecutionOwner();
28602
+ const socket = this.getOpenBridgeSocket(owner?.client_id ?? null);
28603
+ if (!socket) {
28604
+ return {
28605
+ ok: false,
28606
+ error: "The workspace is not available right now. Try again in a moment.",
28607
+ data: { noClient: true }
28608
+ };
28609
+ }
28610
+ const id = crypto.randomUUID();
28611
+ let controller = null;
28612
+ const readable = new ReadableStream({
28613
+ start: (c) => {
28614
+ controller = c;
28615
+ },
28616
+ cancel: () => {
28617
+ this.abortClientStream(id, "The stream reader was canceled.");
28618
+ }
28619
+ });
28620
+ let settleDone;
28621
+ const done = new Promise((resolve2) => {
28622
+ settleDone = resolve2;
28623
+ });
28624
+ const state = {
28625
+ socket,
28626
+ controller,
28627
+ opened: null,
28628
+ openTimer: null,
28629
+ settleDone,
28630
+ settled: false
28631
+ };
28632
+ this.clientStreams.set(id, state);
28633
+ this.bridgeLastSeen.set(socket, Date.now());
28634
+ const openResult = await new Promise((resolve2) => {
28635
+ state.opened = resolve2;
28636
+ const timeoutMs = typeof request.timeoutMs === "number" && request.timeoutMs > 0 ? request.timeoutMs : _DurableThread.CLIENT_STREAM_OPEN_TIMEOUT_MS;
28637
+ state.openTimer = setTimeout(() => {
28638
+ resolve2({
28639
+ ok: false,
28640
+ error: `The client did not accept the ${request.tool} stream within ${Math.round(timeoutMs / 1e3)}s.`
28641
+ });
28642
+ }, timeoutMs);
28643
+ try {
28644
+ socket.send(
28645
+ JSON.stringify({
28646
+ type: "stream_open",
28647
+ id,
28648
+ tool: request.tool,
28649
+ meta: request.meta ?? {}
28650
+ })
28651
+ );
28652
+ } catch (error) {
28653
+ resolve2({
28654
+ ok: false,
28655
+ error: `Failed to reach the client: ${error instanceof Error ? error.message : String(error)}`
28656
+ });
28657
+ }
28658
+ });
28659
+ state.opened = null;
28660
+ if (state.openTimer) {
28661
+ clearTimeout(state.openTimer);
28662
+ state.openTimer = null;
28663
+ }
28664
+ if (!openResult.ok) {
28665
+ this.clientStreams.delete(id);
28666
+ state.settled = true;
28667
+ settleDone({ ok: false, error: openResult.error });
28668
+ return { ok: false, error: openResult.error ?? "Stream open failed." };
28669
+ }
28670
+ const sendChunked = (bytes) => {
28671
+ for (let i = 0; i < bytes.length; i += CLIENT_STREAM_CHUNK_BYTES) {
28672
+ const chunk = bytes.subarray(i, i + CLIENT_STREAM_CHUNK_BYTES);
28673
+ socket.send(JSON.stringify({ type: "stream_data", id, data: encodeStreamChunk(chunk) }));
28674
+ }
28675
+ };
28676
+ const writable = new WritableStream({
28677
+ write: (chunk) => {
28678
+ if (!this.clientStreams.has(id)) {
28679
+ throw new Error("The client stream is closed.");
28680
+ }
28681
+ sendChunked(chunk);
28682
+ },
28683
+ close: () => {
28684
+ try {
28685
+ if (this.clientStreams.has(id)) {
28686
+ socket.send(JSON.stringify({ type: "stream_end", id }));
28687
+ }
28688
+ } catch {
28689
+ }
28690
+ },
28691
+ abort: (reason) => {
28692
+ this.abortClientStream(id, reason instanceof Error ? reason.message : String(reason));
28693
+ }
28694
+ });
28695
+ const duplex = {
28696
+ readable,
28697
+ writable,
28698
+ close: (error) => this.abortClientStream(id, error),
28699
+ done
28700
+ };
28701
+ return { ok: true, duplex };
28702
+ }
28703
+ // ─── Subagent stream proxy-up (chunk-pull, no cross-DO ReadableStream) ─────
28704
+ // A subagent (child thread, no bridge) proxies its whole stream request UP to
28705
+ // this bridge-holding root. We open the client stream locally, then buffer
28706
+ // incoming bytes so the child can drain them over plain request/response RPCs
28707
+ // — sidestepping the unproven DO-to-DO ReadableStream return.
28708
+ proxyStreams = /* @__PURE__ */ new Map();
28709
+ async proxyStreamOpen(request) {
28710
+ const opened = await this.openClientStream(request);
28711
+ if (!opened.ok) {
28712
+ const noClient = !!(opened.data && opened.data.noClient);
28713
+ return { ok: false, error: opened.error, noClient };
28714
+ }
28715
+ const id = crypto.randomUUID();
28716
+ const duplex = opened.duplex;
28717
+ const reader = duplex.readable.getReader();
28718
+ const writer = duplex.writable.getWriter();
28719
+ const entry = {
28720
+ writer,
28721
+ reader,
28722
+ buffer: [],
28723
+ done: false,
28724
+ error: void 0,
28725
+ pump: Promise.resolve(),
28726
+ close: (error) => duplex.close(error)
28727
+ };
28728
+ entry.pump = (async () => {
28729
+ try {
28730
+ while (true) {
28731
+ const { value, done } = await reader.read();
28732
+ if (done) break;
28733
+ if (value) entry.buffer.push(value);
28734
+ }
28735
+ } catch (error) {
28736
+ entry.error = error instanceof Error ? error.message : String(error);
28737
+ } finally {
28738
+ entry.done = true;
28739
+ }
28740
+ })();
28741
+ void duplex.done.then((d) => {
28742
+ entry.done = true;
28743
+ if (!d.ok && !entry.error) entry.error = d.error;
28744
+ });
28745
+ this.proxyStreams.set(id, entry);
28746
+ return { ok: true, id };
28747
+ }
28748
+ async proxyStreamWrite(id, dataB64) {
28749
+ const entry = this.proxyStreams.get(id);
28750
+ if (!entry) return;
28751
+ try {
28752
+ await entry.writer.write(decodeStreamChunk(dataB64));
28753
+ } catch {
28754
+ }
28755
+ }
28756
+ async proxyStreamPull(id) {
28757
+ const entry = this.proxyStreams.get(id);
28758
+ if (!entry) return { chunks: [], done: true, error: "unknown proxy stream" };
28759
+ const chunks = entry.buffer.splice(0).map((b) => encodeStreamChunk(b));
28760
+ if (entry.done && entry.buffer.length === 0) {
28761
+ this.proxyStreams.delete(id);
28762
+ return { chunks, done: true, error: entry.error };
28763
+ }
28764
+ return { chunks, done: false };
28765
+ }
28766
+ async proxyStreamEnd(id) {
28767
+ const entry = this.proxyStreams.get(id);
28768
+ if (!entry) return;
28769
+ try {
28770
+ await entry.writer.close();
28771
+ } catch {
28772
+ }
28773
+ }
28774
+ async proxyStreamAbort(id) {
28775
+ const entry = this.proxyStreams.get(id);
28776
+ if (!entry) return;
28777
+ this.proxyStreams.delete(id);
28778
+ try {
28779
+ entry.close("aborted");
28780
+ } catch {
28781
+ }
28782
+ }
28783
+ /** Settle and remove a client stream exactly once. */
28784
+ settleClientStream(id, result) {
28785
+ const state = this.clientStreams.get(id);
28786
+ if (!state) return;
28787
+ this.clientStreams.delete(id);
28788
+ if (state.openTimer) clearTimeout(state.openTimer);
28789
+ if (state.opened) state.opened({ ok: false, error: result.error ?? "Stream closed." });
28790
+ if (!state.settled) {
28791
+ state.settled = true;
28792
+ try {
28793
+ if (result.ok) state.controller?.close();
28794
+ else state.controller?.error(new Error(result.error ?? "Client stream aborted."));
28795
+ } catch {
28796
+ }
28797
+ state.settleDone(result);
28798
+ }
28799
+ }
28800
+ /** Abort a stream from the Worker side, telling the client to tear down. */
28801
+ abortClientStream(id, error) {
28802
+ const state = this.clientStreams.get(id);
28803
+ if (!state) return;
28804
+ try {
28805
+ if (state.socket.readyState === WebSocket.OPEN) {
28806
+ state.socket.send(JSON.stringify({ type: "stream_abort", id, error: error ?? null }));
28807
+ }
28808
+ } catch {
28809
+ }
28810
+ this.settleClientStream(id, error ? { ok: false, error } : { ok: true });
28811
+ }
28812
+ /** Handle an incoming stream_* frame from a bridge client. */
28813
+ handleClientStreamFrame(ws, parsed) {
28814
+ const id = typeof parsed.id === "string" ? parsed.id : null;
28815
+ if (!id) return;
28816
+ const state = this.clientStreams.get(id);
28817
+ if (!state || state.socket !== ws) return;
28818
+ switch (parsed.type) {
28819
+ case "stream_opened": {
28820
+ if (state.opened) {
28821
+ const resolve2 = state.opened;
28822
+ state.opened = null;
28823
+ if (state.openTimer) {
28824
+ clearTimeout(state.openTimer);
28825
+ state.openTimer = null;
28826
+ }
28827
+ resolve2({ ok: true });
28828
+ }
28829
+ return;
28830
+ }
28831
+ case "stream_error": {
28832
+ const error = typeof parsed.error === "string" ? parsed.error : "Client stream error.";
28833
+ this.settleClientStream(id, { ok: false, error });
28834
+ return;
28835
+ }
28836
+ case "stream_data": {
28837
+ if (typeof parsed.data !== "string") return;
28838
+ try {
28839
+ state.controller?.enqueue(decodeStreamChunk(parsed.data));
28840
+ } catch {
28841
+ this.abortClientStream(id, "Failed to deliver stream data.");
28842
+ }
28843
+ return;
28844
+ }
28845
+ case "stream_end": {
28846
+ this.settleClientStream(id, { ok: true });
28847
+ return;
28848
+ }
28849
+ case "stream_abort": {
28850
+ const error = typeof parsed.error === "string" ? parsed.error : void 0;
28851
+ this.settleClientStream(id, { ok: false, error: error ?? "Client aborted the stream." });
28852
+ return;
28853
+ }
28854
+ }
28855
+ }
28856
+ /** Fail every stream bound to a socket that closed or died. */
28857
+ failClientStreamsForSocket(ws) {
28858
+ for (const [id, state] of Array.from(this.clientStreams.entries())) {
28859
+ if (state.socket === ws) {
28860
+ this.settleClientStream(id, { ok: false, error: "The client connection was lost." });
28861
+ }
28862
+ }
28863
+ }
28864
+ // ─── Parked-call wake + bounded TTL ────────────────────────────────────────
28865
+ /** How long a parked forwarded call waits for a client before failing visibly. */
28866
+ static CLIENT_PARK_TTL_MS = 30 * 60 * 1e3;
28867
+ /**
28868
+ * A forwarded call parked with NO client attached: ask the user's hub to
28869
+ * summon the owning machine's daemon to this thread, and arm the bounded
28870
+ * park TTL that turns an unreachable host into a visible durable failure
28871
+ * instead of a silent hang.
28872
+ */
28873
+ async notifyClientWaiting(toolCallId, threadId, userId) {
28874
+ try {
28875
+ await this.alarmQueue.enqueue(
28876
+ "clientParkTimeout",
28877
+ { toolCallId, threadId },
28878
+ _DurableThread.CLIENT_PARK_TTL_MS
28879
+ );
28880
+ } catch (error) {
28881
+ console.error("[DurableThread] Failed to arm park TTL (non-fatal):", error);
28882
+ }
28883
+ if (!userId) return;
28884
+ try {
28885
+ const namespace = this.env.AGENT_BUILDER_USER_HUB;
28886
+ if (!namespace) return;
28887
+ const hub = namespace.get(namespace.idFromName(userId));
28888
+ await hub.requestWake(threadId);
28889
+ } catch (error) {
28890
+ console.error("[DurableThread] Hub wake failed (non-fatal):", error);
28891
+ }
28892
+ }
28893
+ /**
28894
+ * Park TTL expired: if the call is still pending, settle it as a durable,
28895
+ * VISIBLE failure — rewrite the waiting placeholder, mark the pending row
28896
+ * failed, fail any still-pending assistant messages, and persist a durable
28897
+ * failed assistant message so every connected head settles instead of
28898
+ * showing "Working" forever.
28899
+ */
28900
+ async failParkedClientCall(toolCallId, threadId) {
28901
+ await this.ensureMigrated();
28902
+ await this.ensurePendingClientTable();
28903
+ const cursor = await this.ctx.storage.sql.exec(
28904
+ `SELECT status, tool FROM pending_client_calls WHERE tool_call_id = ? LIMIT 1`,
28905
+ toolCallId
28906
+ );
28907
+ const row = cursor.toArray()[0];
28908
+ if (!row || row.status !== "pending") return;
28909
+ const errorText = "Could not reach your machine to run this operation. Reconnect it and retry.";
28910
+ await this.ctx.storage.sql.exec(
28911
+ `UPDATE pending_client_calls SET status = 'failed' WHERE tool_call_id = ?`,
28912
+ toolCallId
28913
+ );
28914
+ await this.ctx.storage.sql.exec(
28915
+ `UPDATE messages SET content = ?, tool_status = 'error' WHERE tool_call_id = ? AND role = 'tool'`,
28916
+ `Error: ${errorText}`,
28917
+ toolCallId
28918
+ );
28919
+ try {
28920
+ const msgCursor = await this.ctx.storage.sql.exec(
28921
+ `SELECT id, role, content, tool_call_id, tool_status, created_at, depth
28922
+ FROM messages WHERE tool_call_id = ? AND role = 'tool' LIMIT 1`,
28923
+ toolCallId
28924
+ );
28925
+ const msg = msgCursor.toArray()[0];
28926
+ if (msg) this.broadcastMessage(msg);
28927
+ } catch {
28928
+ }
28929
+ await this.settleTerminalFailure(errorText);
28930
+ if (threadId) {
28931
+ try {
28932
+ await this.disarmExecutionWatchdog(threadId);
28933
+ } catch {
28934
+ }
28935
+ }
28936
+ }
28289
28937
  /**
28290
28938
  * Broadcast a log record to all connected WebSocket clients
28291
28939
  */
@@ -28502,6 +29150,10 @@ ${resultContent}${attachmentSummary}`;
28502
29150
  this.resolveClientCall(ws, parsed);
28503
29151
  return;
28504
29152
  }
29153
+ if (parsed && typeof parsed.type === "string" && parsed.type.startsWith("stream_") && parsed.type !== "stream_ping" && parsed.type !== "stream_pong") {
29154
+ this.handleClientStreamFrame(ws, parsed);
29155
+ return;
29156
+ }
28505
29157
  }
28506
29158
  } catch (error) {
28507
29159
  console.error("[DurableThread] Error handling WebSocket message:", error);
@@ -28522,6 +29174,7 @@ ${resultContent}${attachmentSummary}`;
28522
29174
  this.bridgeSockets.delete(ws);
28523
29175
  this.bridgeLastSeen.delete(ws);
28524
29176
  this.bridgeClientInfo.delete(ws);
29177
+ this.failClientStreamsForSocket(ws);
28525
29178
  this.failPendingClientCallsIfNoBridge();
28526
29179
  }
28527
29180
  }
@@ -28543,6 +29196,7 @@ ${resultContent}${attachmentSummary}`;
28543
29196
  this.bridgeSockets.delete(ws);
28544
29197
  this.bridgeLastSeen.delete(ws);
28545
29198
  this.bridgeClientInfo.delete(ws);
29199
+ this.failClientStreamsForSocket(ws);
28546
29200
  this.failPendingClientCallsIfNoBridge();
28547
29201
  }
28548
29202
  }
@@ -28860,6 +29514,11 @@ ${resultContent}${attachmentSummary}`;
28860
29514
  data: { id, is_complete: true, error, error_type: errorType }
28861
29515
  });
28862
29516
  }
29517
+ try {
29518
+ await this.failPendingMessagesAndBroadcast();
29519
+ } catch (error2) {
29520
+ console.error("[DurableThread] Failed to settle pending messages (non-fatal):", error2);
29521
+ }
28863
29522
  }
28864
29523
  /**
28865
29524
  * A subagent whose execution the watchdog gave up on leaves its parent
@@ -31851,11 +32510,24 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
31851
32510
  throw new Error("User value user_id must be a non-empty string");
31852
32511
  }
31853
32512
  }
32513
+ /**
32514
+ * The per-user hub is the sharded home for account-wide KV. When the binding
32515
+ * exists, the singleton delegates so the global object is no longer a
32516
+ * per-user bottleneck; without it (pre-hub deploys, unit tests) it keeps the
32517
+ * local `user_values` table.
32518
+ */
32519
+ userHub(userId) {
32520
+ const namespace = this.env.AGENT_BUILDER_USER_HUB;
32521
+ if (!namespace) return null;
32522
+ return namespace.get(namespace.idFromName(userId));
32523
+ }
31854
32524
  /** Read one JSON value from a user's account-wide key-value store. */
31855
32525
  async getUserValue(userId, key) {
31856
32526
  await this.ensureMigrated();
31857
32527
  this.assertValidUserValueUserId(userId);
31858
32528
  this.assertValidUserValueKey(key);
32529
+ const hub = this.userHub(userId);
32530
+ if (hub) return hub.getUserValue(userId, key);
31859
32531
  const cursor = await this.ctx.storage.sql.exec(
31860
32532
  `SELECT value FROM user_values WHERE user_id = ? AND key = ? LIMIT 1`,
31861
32533
  userId,
@@ -31876,6 +32548,8 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
31876
32548
  await this.ensureMigrated();
31877
32549
  this.assertValidUserValueUserId(userId);
31878
32550
  this.assertValidUserValueKey(key);
32551
+ const hub = this.userHub(userId);
32552
+ if (hub) return hub.setUserValue(userId, key, value);
31879
32553
  if (value === null || value === void 0) {
31880
32554
  await this.ctx.storage.sql.exec(
31881
32555
  `DELETE FROM user_values WHERE user_id = ? AND key = ?`,
@@ -31912,6 +32586,8 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
31912
32586
  async listUserValues(userId, options = {}) {
31913
32587
  await this.ensureMigrated();
31914
32588
  this.assertValidUserValueUserId(userId);
32589
+ const hub = this.userHub(userId);
32590
+ if (hub) return hub.listUserValues(userId, options);
31915
32591
  const limit = Math.min(Math.max(Math.trunc(options.limit ?? 100), 1), 500);
31916
32592
  const offset = Math.max(Math.trunc(options.offset ?? 0), 0);
31917
32593
  const prefix = typeof options.prefix === "string" && options.prefix.length > 0 ? options.prefix : null;
@@ -33660,6 +34336,192 @@ function formatMs(ms) {
33660
34336
  const minutes = Math.floor(seconds / 60);
33661
34337
  return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
33662
34338
  }
34339
+ var DurableUserHub = class extends DurableObject {
34340
+ /** Persistent client (daemon) sockets, restored across hibernation. */
34341
+ clientSockets = /* @__PURE__ */ new Set();
34342
+ clientInfo = /* @__PURE__ */ new Map();
34343
+ migrated = false;
34344
+ constructor(ctx, env) {
34345
+ super(ctx, env);
34346
+ if (typeof WebSocketRequestResponsePair !== "undefined") {
34347
+ ctx.setWebSocketAutoResponse(
34348
+ new WebSocketRequestResponsePair("stream_ping", "stream_pong")
34349
+ );
34350
+ }
34351
+ for (const ws of ctx.getWebSockets()) {
34352
+ const attachment = ws.deserializeAttachment();
34353
+ if (attachment?.type === "hub_client") {
34354
+ this.clientSockets.add(ws);
34355
+ this.clientInfo.set(ws, {
34356
+ clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
34357
+ clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
34358
+ clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
34359
+ });
34360
+ }
34361
+ }
34362
+ }
34363
+ ensureMigrated() {
34364
+ if (this.migrated) return;
34365
+ this.ctx.storage.sql.exec(`
34366
+ CREATE TABLE IF NOT EXISTS user_values (
34367
+ user_id TEXT NOT NULL,
34368
+ key TEXT NOT NULL,
34369
+ value TEXT NOT NULL,
34370
+ created_at INTEGER NOT NULL,
34371
+ updated_at INTEGER NOT NULL,
34372
+ PRIMARY KEY (user_id, key)
34373
+ )
34374
+ `);
34375
+ this.ctx.storage.sql.exec(
34376
+ `CREATE INDEX IF NOT EXISTS idx_user_values_user ON user_values(user_id)`
34377
+ );
34378
+ this.migrated = true;
34379
+ }
34380
+ // ─── Persistent client socket ──────────────────────────────────────────────
34381
+ /**
34382
+ * Accept the user's persistent client (daemon) WebSocket. The daemon holds
34383
+ * this ONE socket for its whole lifetime; per-thread bridge sockets are
34384
+ * opened separately and only while a thread is active.
34385
+ */
34386
+ async fetch(request) {
34387
+ const url = new URL(request.url);
34388
+ if (request.headers.get("Upgrade") === "websocket") {
34389
+ const pair = new WebSocketPair();
34390
+ const [client, server] = [pair[0], pair[1]];
34391
+ const info = {
34392
+ clientId: url.searchParams.get("client_id"),
34393
+ clientName: url.searchParams.get("client_name"),
34394
+ clientKind: url.searchParams.get("client_kind")
34395
+ };
34396
+ this.ctx.acceptWebSocket(server);
34397
+ server.serializeAttachment({ type: "hub_client", ...info });
34398
+ this.clientSockets.add(server);
34399
+ this.clientInfo.set(server, info);
34400
+ try {
34401
+ server.send(JSON.stringify({ type: "hub_ready" }));
34402
+ } catch {
34403
+ }
34404
+ return new Response(null, { status: 101, webSocket: client });
34405
+ }
34406
+ return new Response("Not found", { status: 404 });
34407
+ }
34408
+ async webSocketMessage(ws, _message) {
34409
+ if (!this.clientSockets.has(ws)) this.clientSockets.add(ws);
34410
+ }
34411
+ async webSocketClose(ws) {
34412
+ this.clientSockets.delete(ws);
34413
+ this.clientInfo.delete(ws);
34414
+ }
34415
+ async webSocketError(ws) {
34416
+ this.clientSockets.delete(ws);
34417
+ this.clientInfo.delete(ws);
34418
+ }
34419
+ /** Whether a persistent client (daemon) is connected right now. */
34420
+ async hasClient() {
34421
+ for (const ws of this.clientSockets) {
34422
+ if (ws.readyState === WebSocket.OPEN) return true;
34423
+ }
34424
+ return false;
34425
+ }
34426
+ // ─── Wake orchestration (the load-bearing RPC) ─────────────────────────────
34427
+ /**
34428
+ * Signal the connected daemon to attach a bridge to `threadId`. Called by a
34429
+ * cold thread DO that parked a forwarded call with no client attached. The
34430
+ * daemon reacts by opening a per-thread bridge socket, which triggers the
34431
+ * thread's drain-on-attach. Returns whether a daemon was reachable.
34432
+ */
34433
+ async requestWake(threadId) {
34434
+ const frame = { type: "wake", threadId };
34435
+ const payload = JSON.stringify(frame);
34436
+ let delivered = false;
34437
+ for (const ws of Array.from(this.clientSockets)) {
34438
+ if (ws.readyState !== WebSocket.OPEN) {
34439
+ this.clientSockets.delete(ws);
34440
+ this.clientInfo.delete(ws);
34441
+ continue;
34442
+ }
34443
+ try {
34444
+ ws.send(payload);
34445
+ delivered = true;
34446
+ } catch {
34447
+ this.clientSockets.delete(ws);
34448
+ this.clientInfo.delete(ws);
34449
+ }
34450
+ }
34451
+ return { delivered };
34452
+ }
34453
+ // ─── Account-wide KV (sharded off the singleton) ───────────────────────────
34454
+ async getUserValue(userId, key) {
34455
+ this.ensureMigrated();
34456
+ const cursor = await this.ctx.storage.sql.exec(
34457
+ `SELECT value FROM user_values WHERE user_id = ? AND key = ? LIMIT 1`,
34458
+ userId,
34459
+ key
34460
+ );
34461
+ const raw = cursor.toArray()[0]?.value;
34462
+ if (raw === void 0) return null;
34463
+ try {
34464
+ return JSON.parse(raw);
34465
+ } catch {
34466
+ return null;
34467
+ }
34468
+ }
34469
+ async setUserValue(userId, key, value) {
34470
+ this.ensureMigrated();
34471
+ if (value === null || value === void 0) {
34472
+ await this.ctx.storage.sql.exec(
34473
+ `DELETE FROM user_values WHERE user_id = ? AND key = ?`,
34474
+ userId,
34475
+ key
34476
+ );
34477
+ return;
34478
+ }
34479
+ const serialized = JSON.stringify(value);
34480
+ const now = Date.now();
34481
+ await this.ctx.storage.sql.exec(
34482
+ `INSERT INTO user_values (user_id, key, value, created_at, updated_at)
34483
+ VALUES (?1, ?2, ?3, ?4, ?4)
34484
+ ON CONFLICT(user_id, key) DO UPDATE SET
34485
+ value = excluded.value,
34486
+ updated_at = excluded.updated_at`,
34487
+ userId,
34488
+ key,
34489
+ serialized,
34490
+ now
34491
+ );
34492
+ }
34493
+ async listUserValues(userId, options = {}) {
34494
+ this.ensureMigrated();
34495
+ const limit = Math.min(Math.max(Math.trunc(options.limit ?? 100), 1), 500);
34496
+ const offset = Math.max(Math.trunc(options.offset ?? 0), 0);
34497
+ const prefix = typeof options.prefix === "string" && options.prefix.length > 0 ? options.prefix : null;
34498
+ const escapedPrefix = prefix ? `${prefix.replace(/[\\%_]/g, (m) => `\\${m}`)}%` : null;
34499
+ const where = escapedPrefix ? `WHERE user_id = ? AND key LIKE ? ESCAPE '\\'` : `WHERE user_id = ?`;
34500
+ const whereArgs = escapedPrefix ? [userId, escapedPrefix] : [userId];
34501
+ const totalCursor = await this.ctx.storage.sql.exec(
34502
+ `SELECT COUNT(*) AS total FROM user_values ${where}`,
34503
+ ...whereArgs
34504
+ );
34505
+ const total = Number(totalCursor.toArray()[0]?.total ?? 0);
34506
+ const cursor = await this.ctx.storage.sql.exec(
34507
+ `SELECT key, value, updated_at FROM user_values ${where}
34508
+ ORDER BY key ASC LIMIT ? OFFSET ?`,
34509
+ ...whereArgs,
34510
+ limit,
34511
+ offset
34512
+ );
34513
+ const entries = cursor.toArray().map((row) => {
34514
+ let value = null;
34515
+ try {
34516
+ value = JSON.parse(row.value);
34517
+ } catch {
34518
+ value = null;
34519
+ }
34520
+ return { key: row.key, value, updated_at: Number(row.updated_at) };
34521
+ });
34522
+ return { entries, total, limit, offset };
34523
+ }
34524
+ };
33663
34525
 
33664
34526
  // src/index.ts
33665
34527
  init_utilities();
@@ -34022,6 +34884,6 @@ init_context();
34022
34884
  init_ProviderRegistry();
34023
34885
  init_types2();
34024
34886
 
34025
- export { CodeExecutionBridge, DurableAgentBuilder, DurableThread, FlowStateSdk, NamespaceResolutionError, PackageDiscoveryService, ProviderRegistry, ThreadWatchdog, agentbuilder, authenticate, buildImageDescription, builder, cat, createNamespaceContext, createThreadEndpointHandler, defineController2 as defineController, discoverPackages, emitThreadEvent, enhanceFlowState, exists, find, forceTurn, generateAgentFile, generateImageDescription, generateModelFile, generatePromptFile, 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 };
34887
+ export { CLIENT_STREAM_CHUNK_BYTES, CodeExecutionBridge, DurableAgentBuilder, DurableThread, DurableUserHub, FlowStateSdk, NamespaceResolutionError, PackageDiscoveryService, ProviderRegistry, ThreadWatchdog, agentbuilder, authenticate, buildImageDescription, builder, callClient, callClientStream, cat, createNamespaceContext, createThreadEndpointHandler, decodeStreamChunk, defineController2 as defineController, discoverPackages, emitThreadEvent, encodeStreamChunk, enhanceFlowState, exists, find, forceTurn, generateAgentFile, generateImageDescription, generateModelFile, generatePromptFile, 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 };
34026
34888
  //# sourceMappingURL=index.js.map
34027
34889
  //# sourceMappingURL=index.js.map