@standardagents/builder 0.37.3 → 0.37.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/built-in-routes.js +178 -81
  2. package/dist/built-in-routes.js.map +1 -1
  3. package/dist/client/{ApiKeysView-BhuT3pIF.js → ApiKeysView-Pk5oljVU.js} +1 -1
  4. package/dist/client/CenteredContentView-HGpMGAQj.js +1 -0
  5. package/dist/client/CompositionView-DN8dTOCO.js +32 -0
  6. package/dist/client/{ConfirmDialog.vue_vue_type_script_setup_true_lang-BqwwsXWJ.js → ConfirmDialog.vue_vue_type_script_setup_true_lang--j_hnMZ0.js} +1 -1
  7. package/dist/client/{CopyButton.vue_vue_type_script_setup_true_lang-Cqm3Vt3E.js → CopyButton.vue_vue_type_script_setup_true_lang-CpJDcbZx.js} +1 -1
  8. package/dist/client/{DataTable.vue_vue_type_script_setup_true_lang-CA5qgsty.js → DataTable.vue_vue_type_script_setup_true_lang-unLrY-PY.js} +1 -1
  9. package/dist/client/{JsonViewer-ByQKCShF.js → JsonViewer-BYj_yrGB.js} +1 -1
  10. package/dist/client/{LoginView-J4qtHbQK.js → LoginView-CfVqItZV.js} +1 -1
  11. package/dist/client/{MarketplaceView-CshdWDqp.js → MarketplaceView-Cidt0nLf.js} +1 -1
  12. package/dist/client/{Modal.vue_vue_type_script_setup_true_lang-DKdB9A8Z.js → Modal.vue_vue_type_script_setup_true_lang-C1dmm1HX.js} +1 -1
  13. package/dist/client/ModelModal.vue_vue_type_script_setup_true_lang-DIKLuPXT.js +1 -0
  14. package/dist/client/{ModelsView-B_n306yk.js → ModelsView-DH6wNe38.js} +1 -1
  15. package/dist/client/{PromptEditView-C85qRgsE.js → PromptEditView-D_VjoAa-.js} +1 -1
  16. package/dist/client/{PromptModal-PNpoJfqD.js → PromptModal-a9LTOWNt.js} +1 -1
  17. package/dist/client/{PromptsView-BKnfThIF.js → PromptsView-BCCCryKR.js} +1 -1
  18. package/dist/client/{ProvidersView-Df41VhHD.js → ProvidersView-CF4x5F6G.js} +2 -2
  19. package/dist/client/ThreadInspectorPane.vue_vue_type_script_setup_true_lang-D5VDoQiK.js +22 -0
  20. package/dist/client/ThreadsView-BzscU1l2.js +1 -0
  21. package/dist/client/{ToolsView-C0MH9x3N.js → ToolsView-CeEtxDZV.js} +1 -1
  22. package/dist/client/{UiCodeBlock.vue_vue_type_script_setup_true_lang-DjDQOYjB.js → UiCodeBlock.vue_vue_type_script_setup_true_lang-BdyzSxZZ.js} +1 -1
  23. package/dist/client/{UsersView--TQiJYP7.js → UsersView-Di5cmUYL.js} +1 -1
  24. package/dist/client/{VariablesView-BO3UljZu.js → VariablesView-CXUr05jD.js} +1 -1
  25. package/dist/client/assets/{CompositionView-nJBKoNx4.css → CompositionView-0qQUWuc1.css} +1 -1
  26. package/dist/client/{check-DeHZQcv4.js → check-YKi1o6p6.js} +1 -1
  27. package/dist/client/{chevron-right-C8Mb0B59.js → chevron-right-T0zzq_GX.js} +1 -1
  28. package/dist/client/{copy-BxJsl-KZ.js → copy-C_gUGAj1.js} +1 -1
  29. package/dist/client/{formkit-0eyyCEP_.js → formkit-CE26dtFp.js} +1 -1
  30. package/dist/client/index-BMqXblce.js +6 -0
  31. package/dist/client/index.html +3 -3
  32. package/dist/client/{package-B9czwyIu.js → package-B70IFSDB.js} +1 -1
  33. package/dist/client/{plus-DqtuqJFm.js → plus-BN9IHYdt.js} +1 -1
  34. package/dist/client/{timeAgo-CDt-vAhI.js → timeAgo-G1ovbWPH.js} +1 -1
  35. package/dist/client/{useActionFeedback-B6UfBJ7l.js → useActionFeedback-7pfPjKMi.js} +1 -1
  36. package/dist/client/{vue-Dw7WLX4c.js → vue-CZ-ORfS3.js} +1 -1
  37. package/dist/{index-DceFbLG9.d.ts → index-Lfan8QMN.d.ts} +46 -0
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.js +297 -25
  40. package/dist/index.js.map +1 -1
  41. package/dist/plugin.js +6 -0
  42. package/dist/plugin.js.map +1 -1
  43. package/dist/runtime.d.ts +45 -5
  44. package/dist/runtime.js +291 -25
  45. package/dist/runtime.js.map +1 -1
  46. package/dist/test.d.ts +1 -1
  47. package/package.json +5 -5
  48. package/dist/client/CenteredContentView-CfgX3Ao3.js +0 -1
  49. package/dist/client/CompositionView-B9FfWMHd.js +0 -32
  50. package/dist/client/ModelModal.vue_vue_type_script_setup_true_lang-BAxaBZsx.js +0 -1
  51. package/dist/client/ThreadInspectorPane.vue_vue_type_script_setup_true_lang-CtIBWHUR.js +0 -22
  52. package/dist/client/ThreadsView-sjo5Rrcm.js +0 -1
  53. package/dist/client/index-BgZnQr8X.js +0 -6
package/dist/index.js CHANGED
@@ -10112,10 +10112,18 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10112
10112
  const mimeType = match[1];
10113
10113
  const base64Data = match[2];
10114
10114
  const ext = mimeType.split("/")[1] || "png";
10115
+ if (base64Data.length < 64) {
10116
+ console.error("[FlowEngine] Generated image payload is empty/truncated, skipping");
10117
+ continue;
10118
+ }
10115
10119
  const attachmentId = crypto.randomUUID();
10116
10120
  const path21 = `/attachments/${Date.now()}-${attachmentId.slice(0, 8)}.${ext}`;
10117
10121
  try {
10118
- await state.thread.instance.writeFile(path21, base64Data, mimeType);
10122
+ const written = await state.thread.instance.writeFile(path21, base64Data, mimeType);
10123
+ if (written && written.success === false) {
10124
+ console.error("[FlowEngine] Generated image write failed:", written.error);
10125
+ continue;
10126
+ }
10119
10127
  if (image.id) {
10120
10128
  imagePathMap.set(image.id, path21);
10121
10129
  }
@@ -10182,10 +10190,18 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10182
10190
  const mimeType = imageUrl.slice(dataPrefix.length, base64Index);
10183
10191
  const base64Data = imageUrl.slice(base64Index + base64Marker.length);
10184
10192
  const ext = mimeType.split("/")[1] || "png";
10193
+ if (base64Data.length < 64) {
10194
+ console.error(`[FlowEngine] ${toolName} image payload is empty/truncated, skipping`);
10195
+ continue;
10196
+ }
10185
10197
  const attachmentId = crypto.randomUUID();
10186
10198
  const path21 = `/attachments/${Date.now()}-${attachmentId.slice(0, 8)}.${ext}`;
10187
10199
  try {
10188
- await state.thread.instance.writeFile(path21, base64Data, mimeType);
10200
+ const written = await state.thread.instance.writeFile(path21, base64Data, mimeType);
10201
+ if (written && written.success === false) {
10202
+ console.error(`[FlowEngine] ${toolName} image write failed:`, written.error);
10203
+ continue;
10204
+ }
10189
10205
  const attachmentRef = {
10190
10206
  id: attachmentId,
10191
10207
  type: "file",
@@ -13722,8 +13738,25 @@ var init_ThreadStateImpl = __esm({
13722
13738
  const mustParkForApproval = await this.forwardedCallNeedsApproval(request);
13723
13739
  if (mustParkForApproval && canPark && toolCallId && typeof instance.hasBridge === "function" && await instance.hasBridge()) {
13724
13740
  await instance.dispatchClientTool(request, toolCallId, side);
13725
- const getDelivered = instance.getDeliveredClientToolResult;
13726
- if (typeof getDelivered === "function") {
13741
+ const withDelivery = instance;
13742
+ if (typeof withDelivery.awaitDeliveredClientToolResult === "function") {
13743
+ try {
13744
+ const delivered = await withDelivery.awaitDeliveredClientToolResult.call(
13745
+ instance,
13746
+ toolCallId,
13747
+ 4e3
13748
+ );
13749
+ if (delivered) {
13750
+ return {
13751
+ ok: delivered.toolStatus !== "error",
13752
+ result: delivered.content,
13753
+ data: { toolCallId }
13754
+ };
13755
+ }
13756
+ } catch {
13757
+ }
13758
+ } else if (typeof withDelivery.getDeliveredClientToolResult === "function") {
13759
+ const getDelivered = withDelivery.getDeliveredClientToolResult;
13727
13760
  const graceDeadline = Date.now() + 4e3;
13728
13761
  while (Date.now() < graceDeadline) {
13729
13762
  await new Promise((r) => setTimeout(r, 150));
@@ -13813,7 +13846,14 @@ var init_ThreadStateImpl = __esm({
13813
13846
  if (typeof stub.proxyStreamOpen === "function") {
13814
13847
  const opened = await stub.proxyStreamOpen(withWake(parentId));
13815
13848
  if (opened.ok && opened.id) {
13816
- return { ok: true, duplex: this.buildProxyDuplex(stub, opened.id) };
13849
+ return {
13850
+ ok: true,
13851
+ duplex: this.buildProxyDuplex(
13852
+ stub,
13853
+ opened.id,
13854
+ opened.parkedPull === true
13855
+ )
13856
+ };
13817
13857
  }
13818
13858
  if (opened.error && !opened.noClient) {
13819
13859
  return {
@@ -13831,11 +13871,15 @@ var init_ThreadStateImpl = __esm({
13831
13871
  }
13832
13872
  /**
13833
13873
  * Build a client duplex that bridges to a stream opened on an ancestor DO
13834
- * (the bridge holder) via chunk-pull RPCs. `readable` polls `proxyStreamPull`;
13835
- * `writable` forwards each chunk with `proxyStreamWrite`; teardown calls
13874
+ * (the bridge holder) via chunk-pull RPCs. `readable` issues PARKED pulls —
13875
+ * a current ancestor holds one `proxyStreamPull` until bytes, completion,
13876
+ * error, or abort, so an idle open stream performs zero repeated pulls.
13877
+ * Older ancestors use the bounded long-pull compatibility path. `writable`
13878
+ * forwards each chunk with `proxyStreamWrite`; teardown calls
13836
13879
  * `proxyStreamAbort`.
13837
13880
  */
13838
- buildProxyDuplex(stub, id) {
13881
+ static PROXY_PULL_MAX_WAIT_MS = 2e4;
13882
+ buildProxyDuplex(stub, id, supportsParkedPull = false) {
13839
13883
  let settleDone;
13840
13884
  const done = new Promise((r) => settleDone = r);
13841
13885
  let closed = false;
@@ -13848,9 +13892,13 @@ var init_ThreadStateImpl = __esm({
13848
13892
  const readable = new ReadableStream({
13849
13893
  async pull(controller) {
13850
13894
  while (!closed) {
13895
+ const pullStartedAt = Date.now();
13851
13896
  let res;
13852
13897
  try {
13853
- res = await stub.proxyStreamPull(id);
13898
+ res = await stub.proxyStreamPull(
13899
+ id,
13900
+ supportsParkedPull ? void 0 : _ThreadStateImpl.PROXY_PULL_MAX_WAIT_MS
13901
+ );
13854
13902
  } catch (error) {
13855
13903
  controller.error(error instanceof Error ? error : new Error(String(error)));
13856
13904
  settle({ ok: false, error: "proxy stream pull failed" });
@@ -13865,7 +13913,14 @@ var init_ThreadStateImpl = __esm({
13865
13913
  return;
13866
13914
  }
13867
13915
  if (res.chunks.length > 0) return;
13868
- await new Promise((r) => setTimeout(r, 25));
13916
+ if (supportsParkedPull) {
13917
+ controller.error(new Error("parked proxy pull returned without data"));
13918
+ settle({ ok: false, error: "parked proxy pull returned without data" });
13919
+ return;
13920
+ }
13921
+ if (Date.now() - pullStartedAt < 250) {
13922
+ await new Promise((r) => setTimeout(r, 250));
13923
+ }
13869
13924
  }
13870
13925
  },
13871
13926
  cancel() {
@@ -22229,6 +22284,10 @@ ${packedThreadRouteCode}
22229
22284
  const isApiRoute = routePath.startsWith('/api/');
22230
22285
 
22231
22286
  let authContext = null;
22287
+ // The router's thread-authorization result, bound to the exact matched
22288
+ // thread id, for same-request reuse by controllers (AB-05). Never cached
22289
+ // across requests.
22290
+ let threadAccess = null;
22232
22291
 
22233
22292
  // Require authentication for all API routes except public ones
22234
22293
  if (isApiRoute && !publicRoute) {
@@ -22261,6 +22320,7 @@ ${packedThreadRouteCode}
22261
22320
  if (!canAccessThread(authContext, thread)) {
22262
22321
  return addCorsHeaders(Response.json({ error: "Forbidden: You don't have access to this thread" }, { status: 403 }));
22263
22322
  }
22323
+ threadAccess = { threadId, thread };
22264
22324
  }
22265
22325
  }
22266
22326
  }
@@ -22281,6 +22341,7 @@ ${packedThreadRouteCode}
22281
22341
  config,
22282
22342
  tools,
22283
22343
  auth: authContext, // Add auth context to controller context
22344
+ threadAccess, // Router's thread authorization, bound to the exact id
22284
22345
  };
22285
22346
  const result = await controller(context);
22286
22347
 
@@ -24384,6 +24445,9 @@ var AlarmQueue = class {
24384
24445
  options.sourceLogId ?? null
24385
24446
  );
24386
24447
  await this.rescheduleAlarm();
24448
+ if (method === "runEffect") {
24449
+ await this.emitEffectRunChange(id);
24450
+ }
24387
24451
  return id;
24388
24452
  }
24389
24453
  /**
@@ -24422,6 +24486,9 @@ var AlarmQueue = class {
24422
24486
  `UPDATE alarm_queue SET status = 'processing', attempts = attempts + 1 WHERE id = ?`,
24423
24487
  item.id
24424
24488
  );
24489
+ if (item.method === "runEffect") {
24490
+ await this.emitEffectRunChange(item.id);
24491
+ }
24425
24492
  try {
24426
24493
  const args = JSON.parse(item.args);
24427
24494
  await this.executeMethod(item.method, args, item.id);
@@ -24475,6 +24542,7 @@ var AlarmQueue = class {
24475
24542
  now,
24476
24543
  id
24477
24544
  );
24545
+ await this.emitEffectRunChange(id);
24478
24546
  }
24479
24547
  /**
24480
24548
  * Mark an item as failed but keep it for 24 hours for debugging
@@ -24494,6 +24562,7 @@ var AlarmQueue = class {
24494
24562
  now,
24495
24563
  id
24496
24564
  );
24565
+ await this.emitEffectRunChange(id);
24497
24566
  }
24498
24567
  /**
24499
24568
  * Clean up failed items older than 24 hours
@@ -24583,6 +24652,10 @@ var AlarmQueue = class {
24583
24652
  */
24584
24653
  async recoverOrphanedProcessing(maxAttempts = 3) {
24585
24654
  const now = Date.now() * 1e3;
24655
+ const orphanedEffectsCursor = await this.sql.exec(
24656
+ `SELECT id FROM alarm_queue WHERE status = 'processing' AND method = 'runEffect'`
24657
+ );
24658
+ const orphanedEffectIds = orphanedEffectsCursor.toArray().map((row) => row.id);
24586
24659
  await this.sql.exec(
24587
24660
  `UPDATE alarm_queue
24588
24661
  SET status = 'failed',
@@ -24595,6 +24668,9 @@ var AlarmQueue = class {
24595
24668
  await this.sql.exec(
24596
24669
  `UPDATE alarm_queue SET status = 'pending' WHERE status = 'processing'`
24597
24670
  );
24671
+ for (const id of orphanedEffectIds) {
24672
+ await this.emitEffectRunChange(id);
24673
+ }
24598
24674
  }
24599
24675
  // ============================================================
24600
24676
  // Effect-specific Methods
@@ -24773,6 +24849,7 @@ var AlarmQueue = class {
24773
24849
  const canceled = changesRows.length > 0 && changesRows[0].changes > 0;
24774
24850
  if (canceled) {
24775
24851
  await this.rescheduleAlarm();
24852
+ await this.emitEffectRunChange(id);
24776
24853
  }
24777
24854
  return canceled;
24778
24855
  }
@@ -24847,6 +24924,7 @@ var AlarmQueue = class {
24847
24924
  if (!changed) {
24848
24925
  throw new Error(`Effect run ${item.id} is no longer pending`);
24849
24926
  }
24927
+ await this.emitEffectRunChange(item.id);
24850
24928
  try {
24851
24929
  await this.executeMethod(item.method, JSON.parse(item.args), item.id);
24852
24930
  await this.complete(item.id, item.method);
@@ -24858,6 +24936,45 @@ var AlarmQueue = class {
24858
24936
  await this.rescheduleAlarm();
24859
24937
  }
24860
24938
  }
24939
+ /**
24940
+ * Read one effect row and hand its bounded projection (no args payload) to
24941
+ * the change listener. Self-filtering: operational (non-effect) rows and
24942
+ * already-deleted rows emit nothing, and a listener failure must never
24943
+ * disturb queue processing.
24944
+ */
24945
+ async emitEffectRunChange(id) {
24946
+ const listener = this.options.onEffectRunChanged;
24947
+ if (!listener) return;
24948
+ try {
24949
+ const cursor = await this.sql.exec(
24950
+ `
24951
+ SELECT id, args, scheduled_at, created_at, status, attempts, error, completed_at, manual_run, manual_run_at, replayed_from
24952
+ FROM alarm_queue
24953
+ WHERE id = ? AND method = 'runEffect'
24954
+ LIMIT 1
24955
+ `,
24956
+ id
24957
+ );
24958
+ const row = cursor.toArray()[0];
24959
+ if (!row) return;
24960
+ const { effectName } = this.parseEffectPayload(row.args);
24961
+ listener({
24962
+ id: row.id,
24963
+ name: effectName,
24964
+ scheduledAt: row.scheduled_at,
24965
+ createdAt: row.created_at,
24966
+ status: row.status,
24967
+ attempts: row.attempts,
24968
+ error: row.error,
24969
+ completedAt: row.completed_at,
24970
+ manualRun: row.manual_run === 1,
24971
+ manualRunAt: row.manual_run_at,
24972
+ replayedFrom: row.replayed_from
24973
+ });
24974
+ } catch (error) {
24975
+ console.error(`[AlarmQueue] Failed to emit effect run change for ${id}:`, error);
24976
+ }
24977
+ }
24861
24978
  normalizeOffset(value) {
24862
24979
  const parsed = typeof value === "number" ? Math.trunc(value) : Number.NaN;
24863
24980
  if (!Number.isFinite(parsed)) return 0;
@@ -29328,6 +29445,25 @@ var DurableThread = class _DurableThread extends DurableObject {
29328
29445
  alarmQueueRearmed = false;
29329
29446
  currentAbortController = null;
29330
29447
  isExecuting = false;
29448
+ /**
29449
+ * In-memory waiters parked until the running flow finishes (AB-09): the
29450
+ * executeFlow busy-guard awaits this signal instead of re-checking
29451
+ * isExecuting every 500ms. Every isExecuting=false transition (execution
29452
+ * finallys and the watchdog takeover) notifies. In-memory only — a DO
29453
+ * restart drops waiters together with the busy caller.
29454
+ */
29455
+ executionIdleWaiters = /* @__PURE__ */ new Set();
29456
+ notifyExecutionIdleWaiters() {
29457
+ if (!this.executionIdleWaiters || this.executionIdleWaiters.size === 0) return;
29458
+ const waiters = Array.from(this.executionIdleWaiters);
29459
+ this.executionIdleWaiters.clear();
29460
+ for (const wake of waiters) {
29461
+ try {
29462
+ wake();
29463
+ } catch {
29464
+ }
29465
+ }
29466
+ }
29331
29467
  // ── Execution watchdog state (see watchdogCheck) ─────────────────────────
29332
29468
  // In-memory heartbeat: bumped by noteExecutionProgress() on every streamed
29333
29469
  // chunk and request attempt. Deliberately NOT persisted — after a DO restart
@@ -29352,7 +29488,11 @@ var DurableThread = class _DurableThread extends DurableObject {
29352
29488
  this.alarmQueue = new AlarmQueue(ctx, {
29353
29489
  retentionEnabled: env.AGENT_BUILDER_ALARM_RETENTION_ENABLED !== "false",
29354
29490
  isForensicHold: async () => (await this.runtimeSafety.getStatus()).forensicHold,
29355
- invocationSqlLimits: { rowsRead: 1e6, rowsWritten: 1e4 }
29491
+ invocationSqlLimits: { rowsRead: 1e6, rowsWritten: 1e4 },
29492
+ // Effect lifecycle transitions broadcast on the thread event channel so
29493
+ // inspectors stay live from the already-open message socket instead of
29494
+ // polling the effects list.
29495
+ onEffectRunChanged: (change) => this.broadcastEvent("effect_changed", change)
29356
29496
  });
29357
29497
  this.alarmQueue.setExecutor(
29358
29498
  async (method, args, itemId) => {
@@ -29911,6 +30051,7 @@ var DurableThread = class _DurableThread extends DurableObject {
29911
30051
  if (value === null || value === void 0) {
29912
30052
  await this.ctx.storage.sql.exec(`DELETE FROM thread_values WHERE key = ?`, key);
29913
30053
  this.broadcastLog({ type: "kv_data", key, deleted: true, updated_at: updatedAt });
30054
+ this.broadcastEvent("kv_changed", { key, deleted: true, updated_at: updatedAt });
29914
30055
  return;
29915
30056
  }
29916
30057
  const serialized = this.serializeThreadValue(value);
@@ -29927,6 +30068,7 @@ var DurableThread = class _DurableThread extends DurableObject {
29927
30068
  updatedAt
29928
30069
  );
29929
30070
  this.broadcastLog({ type: "kv_data", key, deleted: false, updated_at: updatedAt });
30071
+ this.broadcastEvent("kv_changed", { key, deleted: false, updated_at: updatedAt });
29930
30072
  }
29931
30073
  async listValues(options = {}) {
29932
30074
  await this.ensureMigrated();
@@ -32947,6 +33089,7 @@ ${resultContent}${attachmentSummary}`;
32947
33089
  if (isOwner) {
32948
33090
  void this.resendPendingClientCalls(server);
32949
33091
  }
33092
+ this.notifyOwnerBridgeWaiters();
32950
33093
  return new Response(null, {
32951
33094
  status: 101,
32952
33095
  webSocket: client
@@ -32982,6 +33125,7 @@ ${resultContent}${attachmentSummary}`;
32982
33125
  /** Persist + cache the execution-owner record (null releases ownership). */
32983
33126
  async setExecutionOwner(owner) {
32984
33127
  await this.setValue(EXECUTION_OWNER_KEY, owner);
33128
+ this.notifyOwnerBridgeWaiters();
32985
33129
  }
32986
33130
  /** Whether the given client id has an open bridge socket (excluding `except`).
32987
33131
  * Idle bridge heartbeats are handled by Cloudflare without waking this object,
@@ -33339,6 +33483,55 @@ ${resultContent}${attachmentSummary}`;
33339
33483
  * permission level covers the call) otherwise race the placeholder into
33340
33484
  * stored history, leaving the model blind to its own tool outcomes.
33341
33485
  */
33486
+ /**
33487
+ * In-memory waiters parked on a forwarded call's result delivery (AB-09).
33488
+ * `deliverClientToolResult` resolves them so the approval grace-wait reads
33489
+ * delivered state exactly once instead of re-polling it every 150ms.
33490
+ * In-memory only: a DO restart drops waiters along with the caller that
33491
+ * registered them, and the durable park path still resumes the turn.
33492
+ */
33493
+ deliveredResultWaiters = /* @__PURE__ */ new Map();
33494
+ notifyDeliveredResultWaiters(toolCallId) {
33495
+ const waiters = this.deliveredResultWaiters?.get(toolCallId);
33496
+ if (!waiters) return;
33497
+ this.deliveredResultWaiters.delete(toolCallId);
33498
+ for (const wake of waiters) {
33499
+ try {
33500
+ wake();
33501
+ } catch {
33502
+ }
33503
+ }
33504
+ }
33505
+ /**
33506
+ * Await a parked forwarded call's delivered result: one immediate read,
33507
+ * then a single parked wait resolved by result delivery, bounded by one
33508
+ * hard deadline, then one final read. Replaces the caller-side 150ms
33509
+ * re-poll of {@link getDeliveredClientToolResult}.
33510
+ */
33511
+ async awaitDeliveredClientToolResult(toolCallId, maxWaitMs) {
33512
+ const immediate = await this.getDeliveredClientToolResult(toolCallId);
33513
+ if (immediate) return immediate;
33514
+ if (!(maxWaitMs > 0)) return null;
33515
+ await new Promise((resolve3) => {
33516
+ let waiters = this.deliveredResultWaiters.get(toolCallId);
33517
+ if (!waiters) {
33518
+ waiters = /* @__PURE__ */ new Set();
33519
+ this.deliveredResultWaiters.set(toolCallId, waiters);
33520
+ }
33521
+ const registered = waiters;
33522
+ const deadline = setTimeout(() => {
33523
+ registered.delete(wake);
33524
+ if (registered.size === 0) this.deliveredResultWaiters.delete(toolCallId);
33525
+ resolve3();
33526
+ }, maxWaitMs);
33527
+ const wake = () => {
33528
+ clearTimeout(deadline);
33529
+ resolve3();
33530
+ };
33531
+ registered.add(wake);
33532
+ });
33533
+ return this.getDeliveredClientToolResult(toolCallId);
33534
+ }
33342
33535
  async getDeliveredClientToolResult(toolCallId) {
33343
33536
  await this.ensureMigrated();
33344
33537
  await this.ensurePendingClientTable();
@@ -33474,6 +33667,7 @@ ${resultContent}${attachmentSummary}`;
33474
33667
  `UPDATE pending_client_calls SET status = 'resolved' WHERE tool_call_id = ?`,
33475
33668
  toolCallId
33476
33669
  );
33670
+ this.notifyDeliveredResultWaiters(toolCallId);
33477
33671
  try {
33478
33672
  const msgCursor = await this.ctx.storage.sql.exec(
33479
33673
  `SELECT id, role, content, name, tool_call_id, tool_status, created_at, depth
@@ -33492,8 +33686,24 @@ ${resultContent}${attachmentSummary}`;
33492
33686
  // they are bound to one bridge socket and never park.
33493
33687
  /** Default time to wait for the client to accept a stream open. */
33494
33688
  static CLIENT_STREAM_OPEN_TIMEOUT_MS = 3e4;
33495
- /** Poll cadence while waiting for a disconnected owner's bridge to return. */
33496
- static OWNER_REATTACH_POLL_MS = 250;
33689
+ /**
33690
+ * In-memory waiters parked on the owner's bridge socket returning (AB-09).
33691
+ * Resolved by bridge attach and by execution-owner changes; each waiter
33692
+ * re-evaluates against the CURRENT owner on wake. In-memory only: a DO
33693
+ * restart drops waiters with the stream-open call that registered them.
33694
+ */
33695
+ ownerBridgeWaiters = /* @__PURE__ */ new Set();
33696
+ notifyOwnerBridgeWaiters() {
33697
+ if (!this.ownerBridgeWaiters || this.ownerBridgeWaiters.size === 0) return;
33698
+ const waiters = Array.from(this.ownerBridgeWaiters);
33699
+ this.ownerBridgeWaiters.clear();
33700
+ for (const wake of waiters) {
33701
+ try {
33702
+ wake();
33703
+ } catch {
33704
+ }
33705
+ }
33706
+ }
33497
33707
  /**
33498
33708
  * The thread HAS an elected execution owner, but its bridge socket is gone
33499
33709
  * (daemon restart, network blip, DO handoff). Summon the owning machine
@@ -33502,6 +33712,11 @@ ${resultContent}${attachmentSummary}`;
33502
33712
  * Sama One turns with "workspace unavailable" whenever a slow tool/approval
33503
33713
  * outlived the socket. Threads with NO recorded owner (subagent children)
33504
33714
  * still fail fast so ancestor walk-up routing stays immediate.
33715
+ *
33716
+ * The wait parks on bridge-attach/owner-change signals under ONE hard
33717
+ * deadline — no re-poll cadence. Each wake re-reads the owner because the
33718
+ * returning daemon may reclaim with a new generation, and the socket must
33719
+ * match the CURRENT owner.
33505
33720
  */
33506
33721
  async awaitOwnerBridgeSocket(request) {
33507
33722
  if (request.wake?.userId) {
@@ -33509,15 +33724,25 @@ ${resultContent}${attachmentSummary}`;
33509
33724
  }
33510
33725
  const timeoutMs = typeof request.timeoutMs === "number" && request.timeoutMs > 0 ? request.timeoutMs : _DurableThread.CLIENT_STREAM_OPEN_TIMEOUT_MS;
33511
33726
  const deadline = Date.now() + timeoutMs;
33512
- while (Date.now() < deadline) {
33513
- await new Promise(
33514
- (resolve3) => setTimeout(resolve3, _DurableThread.OWNER_REATTACH_POLL_MS)
33515
- );
33727
+ this.ownerBridgeWaiters ??= /* @__PURE__ */ new Set();
33728
+ while (true) {
33516
33729
  const owner = await this.getExecutionOwner();
33517
33730
  const socket = this.getOpenBridgeSocket(owner?.client_id ?? null);
33518
33731
  if (socket) return socket;
33732
+ const remainingMs = deadline - Date.now();
33733
+ if (remainingMs <= 0) return null;
33734
+ await new Promise((resolve3) => {
33735
+ const timer = setTimeout(() => {
33736
+ this.ownerBridgeWaiters.delete(wake);
33737
+ resolve3();
33738
+ }, remainingMs);
33739
+ const wake = () => {
33740
+ clearTimeout(timer);
33741
+ resolve3();
33742
+ };
33743
+ this.ownerBridgeWaiters.add(wake);
33744
+ });
33519
33745
  }
33520
- return null;
33521
33746
  }
33522
33747
  /** Open a bidirectional byte stream to the connected owner client. */
33523
33748
  async openClientStream(request) {
@@ -33661,27 +33886,38 @@ ${resultContent}${attachmentSummary}`;
33661
33886
  done: false,
33662
33887
  error: void 0,
33663
33888
  pump: Promise.resolve(),
33664
- close: (error) => duplex.close(error)
33889
+ close: (error) => duplex.close(error),
33890
+ waiter: null,
33891
+ notify: () => {
33892
+ const waiter = entry.waiter;
33893
+ entry.waiter = null;
33894
+ waiter?.();
33895
+ }
33665
33896
  };
33666
33897
  entry.pump = (async () => {
33667
33898
  try {
33668
33899
  while (true) {
33669
33900
  const { value, done } = await reader.read();
33670
33901
  if (done) break;
33671
- if (value) entry.buffer.push(value);
33902
+ if (value) {
33903
+ entry.buffer.push(value);
33904
+ entry.notify();
33905
+ }
33672
33906
  }
33673
33907
  } catch (error) {
33674
33908
  entry.error = error instanceof Error ? error.message : String(error);
33675
33909
  } finally {
33676
33910
  entry.done = true;
33911
+ entry.notify();
33677
33912
  }
33678
33913
  })();
33679
33914
  void duplex.done.then((d) => {
33680
33915
  entry.done = true;
33681
33916
  if (!d.ok && !entry.error) entry.error = d.error;
33917
+ entry.notify();
33682
33918
  });
33683
33919
  this.proxyStreams.set(id, entry);
33684
- return { ok: true, id };
33920
+ return { ok: true, id, parkedPull: true };
33685
33921
  }
33686
33922
  async proxyStreamWrite(id, dataB64) {
33687
33923
  const entry = this.proxyStreams.get(id);
@@ -33691,9 +33927,22 @@ ${resultContent}${attachmentSummary}`;
33691
33927
  } catch {
33692
33928
  }
33693
33929
  }
33694
- async proxyStreamPull(id) {
33930
+ async proxyStreamPull(id, maxWaitMs) {
33695
33931
  const entry = this.proxyStreams.get(id);
33696
33932
  if (!entry) return { chunks: [], done: true, error: "unknown proxy stream" };
33933
+ if (entry.buffer.length === 0 && !entry.done) {
33934
+ await new Promise((resolve3) => {
33935
+ const deadline = typeof maxWaitMs === "number" && maxWaitMs > 0 ? setTimeout(() => {
33936
+ if (entry.waiter === wake) entry.waiter = null;
33937
+ resolve3();
33938
+ }, maxWaitMs) : null;
33939
+ const wake = () => {
33940
+ if (deadline) clearTimeout(deadline);
33941
+ resolve3();
33942
+ };
33943
+ entry.waiter = wake;
33944
+ });
33945
+ }
33697
33946
  const chunks = entry.buffer.splice(0).map((b) => encodeStreamChunk(b));
33698
33947
  if (entry.done && entry.buffer.length === 0) {
33699
33948
  this.proxyStreams.delete(id);
@@ -33713,6 +33962,9 @@ ${resultContent}${attachmentSummary}`;
33713
33962
  const entry = this.proxyStreams.get(id);
33714
33963
  if (!entry) return;
33715
33964
  this.proxyStreams.delete(id);
33965
+ entry.done = true;
33966
+ if (!entry.error) entry.error = "aborted";
33967
+ entry.notify();
33716
33968
  try {
33717
33969
  entry.close("aborted");
33718
33970
  } catch {
@@ -34068,6 +34320,7 @@ ${resultContent}${attachmentSummary}`;
34068
34320
  this.broadcastEvent(type, data);
34069
34321
  }
34070
34322
  broadcastEvent(type, data) {
34323
+ if (!this.messageSockets || this.messageSockets.size === 0) return;
34071
34324
  const payload = JSON.stringify({
34072
34325
  type: "event",
34073
34326
  eventType: type,
@@ -34494,6 +34747,7 @@ ${resultContent}${attachmentSummary}`;
34494
34747
  } catch {
34495
34748
  }
34496
34749
  this.isExecuting = false;
34750
+ this.notifyExecutionIdleWaiters();
34497
34751
  this.currentAbortController = null;
34498
34752
  await this.recordRegistryActivity(threadId, false);
34499
34753
  tookOver = true;
@@ -34686,9 +34940,22 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
34686
34940
  await this.assertNotTerminated();
34687
34941
  if (this.isExecuting) {
34688
34942
  const busyWaitBudgetMs = 3e4;
34689
- const waitStartedAt = Date.now();
34690
- while (this.isExecuting && Date.now() - waitStartedAt < busyWaitBudgetMs) {
34691
- await new Promise((resolve3) => setTimeout(resolve3, 500));
34943
+ const deadline = Date.now() + busyWaitBudgetMs;
34944
+ this.executionIdleWaiters ??= /* @__PURE__ */ new Set();
34945
+ while (this.isExecuting) {
34946
+ const remainingMs = deadline - Date.now();
34947
+ if (remainingMs <= 0) break;
34948
+ await new Promise((resolve3) => {
34949
+ const timer = setTimeout(() => {
34950
+ this.executionIdleWaiters.delete(wake);
34951
+ resolve3();
34952
+ }, remainingMs);
34953
+ const wake = () => {
34954
+ clearTimeout(timer);
34955
+ resolve3();
34956
+ };
34957
+ this.executionIdleWaiters.add(wake);
34958
+ });
34692
34959
  }
34693
34960
  if (this.isExecuting) {
34694
34961
  throw new Error(
@@ -34827,6 +35094,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
34827
35094
  return flowResult;
34828
35095
  } finally {
34829
35096
  this.isExecuting = false;
35097
+ this.notifyExecutionIdleWaiters();
34830
35098
  this.currentAbortController = null;
34831
35099
  await this.disarmExecutionWatchdog(threadId);
34832
35100
  await this.recordRegistryActivity(threadId, false);
@@ -35040,6 +35308,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
35040
35308
  }, true);
35041
35309
  } finally {
35042
35310
  this.isExecuting = false;
35311
+ this.notifyExecutionIdleWaiters();
35043
35312
  this.currentAbortController = null;
35044
35313
  await this.disarmExecutionWatchdog(threadId);
35045
35314
  await this.recordRegistryActivity(threadId, false);
@@ -35166,6 +35435,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
35166
35435
  );
35167
35436
  } finally {
35168
35437
  this.isExecuting = false;
35438
+ this.notifyExecutionIdleWaiters();
35169
35439
  this.currentAbortController = null;
35170
35440
  await this.disarmExecutionWatchdog(threadId);
35171
35441
  await this.recordRegistryActivity(threadId, false);
@@ -35592,6 +35862,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
35592
35862
  const fs19 = this.getFileStorage();
35593
35863
  const from = normalizePath2(sourcePath);
35594
35864
  const record = await fs19.move(sourcePath, destinationPath);
35865
+ this.broadcastEvent("file_moved", { from, path: record.path, file: record });
35595
35866
  this.broadcastEvent("file_deleted", { path: from });
35596
35867
  this.broadcastEvent("file_created", { path: record.path, file: record });
35597
35868
  return { success: true, file: record };
@@ -35610,6 +35881,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
35610
35881
  const fs19 = this.getFileStorage();
35611
35882
  const from = normalizePath2(path21);
35612
35883
  const record = await fs19.rename(path21, newName);
35884
+ this.broadcastEvent("file_moved", { from, path: record.path, file: record });
35613
35885
  this.broadcastEvent("file_deleted", { path: from });
35614
35886
  this.broadcastEvent("file_created", { path: record.path, file: record });
35615
35887
  return { success: true, file: record };