@standardagents/builder 0.37.4 → 0.37.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/built-in-routes.js +184 -83
  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 +303 -27
  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 +297 -27
  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/runtime.js CHANGED
@@ -13729,8 +13729,25 @@ var init_ThreadStateImpl = __esm({
13729
13729
  const mustParkForApproval = await this.forwardedCallNeedsApproval(request);
13730
13730
  if (mustParkForApproval && canPark && toolCallId && typeof instance.hasBridge === "function" && await instance.hasBridge()) {
13731
13731
  await instance.dispatchClientTool(request, toolCallId, side);
13732
- const getDelivered = instance.getDeliveredClientToolResult;
13733
- if (typeof getDelivered === "function") {
13732
+ const withDelivery = instance;
13733
+ if (typeof withDelivery.awaitDeliveredClientToolResult === "function") {
13734
+ try {
13735
+ const delivered = await withDelivery.awaitDeliveredClientToolResult.call(
13736
+ instance,
13737
+ toolCallId,
13738
+ 4e3
13739
+ );
13740
+ if (delivered) {
13741
+ return {
13742
+ ok: delivered.toolStatus !== "error",
13743
+ result: delivered.content,
13744
+ data: { toolCallId }
13745
+ };
13746
+ }
13747
+ } catch {
13748
+ }
13749
+ } else if (typeof withDelivery.getDeliveredClientToolResult === "function") {
13750
+ const getDelivered = withDelivery.getDeliveredClientToolResult;
13734
13751
  const graceDeadline = Date.now() + 4e3;
13735
13752
  while (Date.now() < graceDeadline) {
13736
13753
  await new Promise((r) => setTimeout(r, 150));
@@ -13820,7 +13837,14 @@ var init_ThreadStateImpl = __esm({
13820
13837
  if (typeof stub.proxyStreamOpen === "function") {
13821
13838
  const opened = await stub.proxyStreamOpen(withWake(parentId));
13822
13839
  if (opened.ok && opened.id) {
13823
- return { ok: true, duplex: this.buildProxyDuplex(stub, opened.id) };
13840
+ return {
13841
+ ok: true,
13842
+ duplex: this.buildProxyDuplex(
13843
+ stub,
13844
+ opened.id,
13845
+ opened.parkedPull === true
13846
+ )
13847
+ };
13824
13848
  }
13825
13849
  if (opened.error && !opened.noClient) {
13826
13850
  return {
@@ -13838,11 +13862,15 @@ var init_ThreadStateImpl = __esm({
13838
13862
  }
13839
13863
  /**
13840
13864
  * Build a client duplex that bridges to a stream opened on an ancestor DO
13841
- * (the bridge holder) via chunk-pull RPCs. `readable` polls `proxyStreamPull`;
13842
- * `writable` forwards each chunk with `proxyStreamWrite`; teardown calls
13865
+ * (the bridge holder) via chunk-pull RPCs. `readable` issues PARKED pulls —
13866
+ * a current ancestor holds one `proxyStreamPull` until bytes, completion,
13867
+ * error, or abort, so an idle open stream performs zero repeated pulls.
13868
+ * Older ancestors use the bounded long-pull compatibility path. `writable`
13869
+ * forwards each chunk with `proxyStreamWrite`; teardown calls
13843
13870
  * `proxyStreamAbort`.
13844
13871
  */
13845
- buildProxyDuplex(stub, id) {
13872
+ static PROXY_PULL_MAX_WAIT_MS = 2e4;
13873
+ buildProxyDuplex(stub, id, supportsParkedPull = false) {
13846
13874
  let settleDone;
13847
13875
  const done = new Promise((r) => settleDone = r);
13848
13876
  let closed = false;
@@ -13855,9 +13883,13 @@ var init_ThreadStateImpl = __esm({
13855
13883
  const readable = new ReadableStream({
13856
13884
  async pull(controller) {
13857
13885
  while (!closed) {
13886
+ const pullStartedAt = Date.now();
13858
13887
  let res;
13859
13888
  try {
13860
- res = await stub.proxyStreamPull(id);
13889
+ res = await stub.proxyStreamPull(
13890
+ id,
13891
+ supportsParkedPull ? void 0 : _ThreadStateImpl.PROXY_PULL_MAX_WAIT_MS
13892
+ );
13861
13893
  } catch (error) {
13862
13894
  controller.error(error instanceof Error ? error : new Error(String(error)));
13863
13895
  settle({ ok: false, error: "proxy stream pull failed" });
@@ -13872,7 +13904,14 @@ var init_ThreadStateImpl = __esm({
13872
13904
  return;
13873
13905
  }
13874
13906
  if (res.chunks.length > 0) return;
13875
- await new Promise((r) => setTimeout(r, 25));
13907
+ if (supportsParkedPull) {
13908
+ controller.error(new Error("parked proxy pull returned without data"));
13909
+ settle({ ok: false, error: "parked proxy pull returned without data" });
13910
+ return;
13911
+ }
13912
+ if (Date.now() - pullStartedAt < 250) {
13913
+ await new Promise((r) => setTimeout(r, 250));
13914
+ }
13876
13915
  }
13877
13916
  },
13878
13917
  cancel() {
@@ -14047,7 +14086,7 @@ var init_ThreadStateImpl = __esm({
14047
14086
  return bytes.buffer;
14048
14087
  }
14049
14088
  async readFileStream(path, options) {
14050
- const fileInfo = await this._threadInstance.statFile(path);
14089
+ const fileInfo = this._unwrapStatRecord(await this._threadInstance.statFile(path));
14051
14090
  if (!fileInfo) {
14052
14091
  return null;
14053
14092
  }
@@ -14102,12 +14141,32 @@ var init_ThreadStateImpl = __esm({
14102
14141
  }
14103
14142
  };
14104
14143
  }
14144
+ /**
14145
+ * Unwrap the thread DO's `{ success, file }` stat envelope (older shapes
14146
+ * returned the record directly). A failed or absent lookup is NULL — the
14147
+ * old code mapped the envelope itself, producing a truthy record with
14148
+ * every field defaulted (size 0, application/octet-stream) even for
14149
+ * NONEXISTENT paths, which let a phantom asset pass an existence check and
14150
+ * a real 1.2MB image read as "0 bytes" (2026-08-11).
14151
+ */
14152
+ _unwrapStatRecord(result) {
14153
+ const record = result?.file ?? result;
14154
+ if (!record || typeof record !== "object") return null;
14155
+ if (!("path" in record) && !("name" in record)) return null;
14156
+ return record;
14157
+ }
14105
14158
  async statFile(path) {
14106
- const result = await this._threadInstance.statFile(path);
14107
- return result ? this._mapFileRecord(result) : null;
14159
+ const record = this._unwrapStatRecord(await this._threadInstance.statFile(path));
14160
+ return record ? this._mapFileRecord(record) : null;
14108
14161
  }
14109
14162
  async readdirFile(path) {
14110
- const entries = await this._threadInstance.readdirFile(path);
14163
+ const result = await this._threadInstance.readdirFile(path);
14164
+ const entries = Array.isArray(result) ? result : result?.files;
14165
+ if (!Array.isArray(entries)) {
14166
+ throw new Error(
14167
+ result?.error || `Could not list directory: ${path}`
14168
+ );
14169
+ }
14111
14170
  return {
14112
14171
  entries: entries.map((e) => this._mapFileRecord(e))
14113
14172
  };
@@ -15991,6 +16050,9 @@ var AlarmQueue = class {
15991
16050
  options.sourceLogId ?? null
15992
16051
  );
15993
16052
  await this.rescheduleAlarm();
16053
+ if (method === "runEffect") {
16054
+ await this.emitEffectRunChange(id);
16055
+ }
15994
16056
  return id;
15995
16057
  }
15996
16058
  /**
@@ -16029,6 +16091,9 @@ var AlarmQueue = class {
16029
16091
  `UPDATE alarm_queue SET status = 'processing', attempts = attempts + 1 WHERE id = ?`,
16030
16092
  item.id
16031
16093
  );
16094
+ if (item.method === "runEffect") {
16095
+ await this.emitEffectRunChange(item.id);
16096
+ }
16032
16097
  try {
16033
16098
  const args = JSON.parse(item.args);
16034
16099
  await this.executeMethod(item.method, args, item.id);
@@ -16082,6 +16147,7 @@ var AlarmQueue = class {
16082
16147
  now,
16083
16148
  id
16084
16149
  );
16150
+ await this.emitEffectRunChange(id);
16085
16151
  }
16086
16152
  /**
16087
16153
  * Mark an item as failed but keep it for 24 hours for debugging
@@ -16101,6 +16167,7 @@ var AlarmQueue = class {
16101
16167
  now,
16102
16168
  id
16103
16169
  );
16170
+ await this.emitEffectRunChange(id);
16104
16171
  }
16105
16172
  /**
16106
16173
  * Clean up failed items older than 24 hours
@@ -16190,6 +16257,10 @@ var AlarmQueue = class {
16190
16257
  */
16191
16258
  async recoverOrphanedProcessing(maxAttempts = 3) {
16192
16259
  const now = Date.now() * 1e3;
16260
+ const orphanedEffectsCursor = await this.sql.exec(
16261
+ `SELECT id FROM alarm_queue WHERE status = 'processing' AND method = 'runEffect'`
16262
+ );
16263
+ const orphanedEffectIds = orphanedEffectsCursor.toArray().map((row) => row.id);
16193
16264
  await this.sql.exec(
16194
16265
  `UPDATE alarm_queue
16195
16266
  SET status = 'failed',
@@ -16202,6 +16273,9 @@ var AlarmQueue = class {
16202
16273
  await this.sql.exec(
16203
16274
  `UPDATE alarm_queue SET status = 'pending' WHERE status = 'processing'`
16204
16275
  );
16276
+ for (const id of orphanedEffectIds) {
16277
+ await this.emitEffectRunChange(id);
16278
+ }
16205
16279
  }
16206
16280
  // ============================================================
16207
16281
  // Effect-specific Methods
@@ -16380,6 +16454,7 @@ var AlarmQueue = class {
16380
16454
  const canceled = changesRows.length > 0 && changesRows[0].changes > 0;
16381
16455
  if (canceled) {
16382
16456
  await this.rescheduleAlarm();
16457
+ await this.emitEffectRunChange(id);
16383
16458
  }
16384
16459
  return canceled;
16385
16460
  }
@@ -16454,6 +16529,7 @@ var AlarmQueue = class {
16454
16529
  if (!changed) {
16455
16530
  throw new Error(`Effect run ${item.id} is no longer pending`);
16456
16531
  }
16532
+ await this.emitEffectRunChange(item.id);
16457
16533
  try {
16458
16534
  await this.executeMethod(item.method, JSON.parse(item.args), item.id);
16459
16535
  await this.complete(item.id, item.method);
@@ -16465,6 +16541,45 @@ var AlarmQueue = class {
16465
16541
  await this.rescheduleAlarm();
16466
16542
  }
16467
16543
  }
16544
+ /**
16545
+ * Read one effect row and hand its bounded projection (no args payload) to
16546
+ * the change listener. Self-filtering: operational (non-effect) rows and
16547
+ * already-deleted rows emit nothing, and a listener failure must never
16548
+ * disturb queue processing.
16549
+ */
16550
+ async emitEffectRunChange(id) {
16551
+ const listener = this.options.onEffectRunChanged;
16552
+ if (!listener) return;
16553
+ try {
16554
+ const cursor = await this.sql.exec(
16555
+ `
16556
+ SELECT id, args, scheduled_at, created_at, status, attempts, error, completed_at, manual_run, manual_run_at, replayed_from
16557
+ FROM alarm_queue
16558
+ WHERE id = ? AND method = 'runEffect'
16559
+ LIMIT 1
16560
+ `,
16561
+ id
16562
+ );
16563
+ const row = cursor.toArray()[0];
16564
+ if (!row) return;
16565
+ const { effectName } = this.parseEffectPayload(row.args);
16566
+ listener({
16567
+ id: row.id,
16568
+ name: effectName,
16569
+ scheduledAt: row.scheduled_at,
16570
+ createdAt: row.created_at,
16571
+ status: row.status,
16572
+ attempts: row.attempts,
16573
+ error: row.error,
16574
+ completedAt: row.completed_at,
16575
+ manualRun: row.manual_run === 1,
16576
+ manualRunAt: row.manual_run_at,
16577
+ replayedFrom: row.replayed_from
16578
+ });
16579
+ } catch (error) {
16580
+ console.error(`[AlarmQueue] Failed to emit effect run change for ${id}:`, error);
16581
+ }
16582
+ }
16468
16583
  normalizeOffset(value) {
16469
16584
  const parsed = typeof value === "number" ? Math.trunc(value) : Number.NaN;
16470
16585
  if (!Number.isFinite(parsed)) return 0;
@@ -20935,6 +21050,25 @@ var DurableThread = class _DurableThread extends DurableObject {
20935
21050
  alarmQueueRearmed = false;
20936
21051
  currentAbortController = null;
20937
21052
  isExecuting = false;
21053
+ /**
21054
+ * In-memory waiters parked until the running flow finishes (AB-09): the
21055
+ * executeFlow busy-guard awaits this signal instead of re-checking
21056
+ * isExecuting every 500ms. Every isExecuting=false transition (execution
21057
+ * finallys and the watchdog takeover) notifies. In-memory only — a DO
21058
+ * restart drops waiters together with the busy caller.
21059
+ */
21060
+ executionIdleWaiters = /* @__PURE__ */ new Set();
21061
+ notifyExecutionIdleWaiters() {
21062
+ if (!this.executionIdleWaiters || this.executionIdleWaiters.size === 0) return;
21063
+ const waiters = Array.from(this.executionIdleWaiters);
21064
+ this.executionIdleWaiters.clear();
21065
+ for (const wake of waiters) {
21066
+ try {
21067
+ wake();
21068
+ } catch {
21069
+ }
21070
+ }
21071
+ }
20938
21072
  // ── Execution watchdog state (see watchdogCheck) ─────────────────────────
20939
21073
  // In-memory heartbeat: bumped by noteExecutionProgress() on every streamed
20940
21074
  // chunk and request attempt. Deliberately NOT persisted — after a DO restart
@@ -20959,7 +21093,11 @@ var DurableThread = class _DurableThread extends DurableObject {
20959
21093
  this.alarmQueue = new AlarmQueue(ctx, {
20960
21094
  retentionEnabled: env.AGENT_BUILDER_ALARM_RETENTION_ENABLED !== "false",
20961
21095
  isForensicHold: async () => (await this.runtimeSafety.getStatus()).forensicHold,
20962
- invocationSqlLimits: { rowsRead: 1e6, rowsWritten: 1e4 }
21096
+ invocationSqlLimits: { rowsRead: 1e6, rowsWritten: 1e4 },
21097
+ // Effect lifecycle transitions broadcast on the thread event channel so
21098
+ // inspectors stay live from the already-open message socket instead of
21099
+ // polling the effects list.
21100
+ onEffectRunChanged: (change) => this.broadcastEvent("effect_changed", change)
20963
21101
  });
20964
21102
  this.alarmQueue.setExecutor(
20965
21103
  async (method, args, itemId) => {
@@ -21518,6 +21656,7 @@ var DurableThread = class _DurableThread extends DurableObject {
21518
21656
  if (value === null || value === void 0) {
21519
21657
  await this.ctx.storage.sql.exec(`DELETE FROM thread_values WHERE key = ?`, key);
21520
21658
  this.broadcastLog({ type: "kv_data", key, deleted: true, updated_at: updatedAt });
21659
+ this.broadcastEvent("kv_changed", { key, deleted: true, updated_at: updatedAt });
21521
21660
  return;
21522
21661
  }
21523
21662
  const serialized = this.serializeThreadValue(value);
@@ -21534,6 +21673,7 @@ var DurableThread = class _DurableThread extends DurableObject {
21534
21673
  updatedAt
21535
21674
  );
21536
21675
  this.broadcastLog({ type: "kv_data", key, deleted: false, updated_at: updatedAt });
21676
+ this.broadcastEvent("kv_changed", { key, deleted: false, updated_at: updatedAt });
21537
21677
  }
21538
21678
  async listValues(options = {}) {
21539
21679
  await this.ensureMigrated();
@@ -24554,6 +24694,7 @@ ${resultContent}${attachmentSummary}`;
24554
24694
  if (isOwner) {
24555
24695
  void this.resendPendingClientCalls(server);
24556
24696
  }
24697
+ this.notifyOwnerBridgeWaiters();
24557
24698
  return new Response(null, {
24558
24699
  status: 101,
24559
24700
  webSocket: client
@@ -24589,6 +24730,7 @@ ${resultContent}${attachmentSummary}`;
24589
24730
  /** Persist + cache the execution-owner record (null releases ownership). */
24590
24731
  async setExecutionOwner(owner) {
24591
24732
  await this.setValue(EXECUTION_OWNER_KEY, owner);
24733
+ this.notifyOwnerBridgeWaiters();
24592
24734
  }
24593
24735
  /** Whether the given client id has an open bridge socket (excluding `except`).
24594
24736
  * Idle bridge heartbeats are handled by Cloudflare without waking this object,
@@ -24946,6 +25088,55 @@ ${resultContent}${attachmentSummary}`;
24946
25088
  * permission level covers the call) otherwise race the placeholder into
24947
25089
  * stored history, leaving the model blind to its own tool outcomes.
24948
25090
  */
25091
+ /**
25092
+ * In-memory waiters parked on a forwarded call's result delivery (AB-09).
25093
+ * `deliverClientToolResult` resolves them so the approval grace-wait reads
25094
+ * delivered state exactly once instead of re-polling it every 150ms.
25095
+ * In-memory only: a DO restart drops waiters along with the caller that
25096
+ * registered them, and the durable park path still resumes the turn.
25097
+ */
25098
+ deliveredResultWaiters = /* @__PURE__ */ new Map();
25099
+ notifyDeliveredResultWaiters(toolCallId) {
25100
+ const waiters = this.deliveredResultWaiters?.get(toolCallId);
25101
+ if (!waiters) return;
25102
+ this.deliveredResultWaiters.delete(toolCallId);
25103
+ for (const wake of waiters) {
25104
+ try {
25105
+ wake();
25106
+ } catch {
25107
+ }
25108
+ }
25109
+ }
25110
+ /**
25111
+ * Await a parked forwarded call's delivered result: one immediate read,
25112
+ * then a single parked wait resolved by result delivery, bounded by one
25113
+ * hard deadline, then one final read. Replaces the caller-side 150ms
25114
+ * re-poll of {@link getDeliveredClientToolResult}.
25115
+ */
25116
+ async awaitDeliveredClientToolResult(toolCallId, maxWaitMs) {
25117
+ const immediate = await this.getDeliveredClientToolResult(toolCallId);
25118
+ if (immediate) return immediate;
25119
+ if (!(maxWaitMs > 0)) return null;
25120
+ await new Promise((resolve) => {
25121
+ let waiters = this.deliveredResultWaiters.get(toolCallId);
25122
+ if (!waiters) {
25123
+ waiters = /* @__PURE__ */ new Set();
25124
+ this.deliveredResultWaiters.set(toolCallId, waiters);
25125
+ }
25126
+ const registered = waiters;
25127
+ const deadline = setTimeout(() => {
25128
+ registered.delete(wake);
25129
+ if (registered.size === 0) this.deliveredResultWaiters.delete(toolCallId);
25130
+ resolve();
25131
+ }, maxWaitMs);
25132
+ const wake = () => {
25133
+ clearTimeout(deadline);
25134
+ resolve();
25135
+ };
25136
+ registered.add(wake);
25137
+ });
25138
+ return this.getDeliveredClientToolResult(toolCallId);
25139
+ }
24949
25140
  async getDeliveredClientToolResult(toolCallId) {
24950
25141
  await this.ensureMigrated();
24951
25142
  await this.ensurePendingClientTable();
@@ -25081,6 +25272,7 @@ ${resultContent}${attachmentSummary}`;
25081
25272
  `UPDATE pending_client_calls SET status = 'resolved' WHERE tool_call_id = ?`,
25082
25273
  toolCallId
25083
25274
  );
25275
+ this.notifyDeliveredResultWaiters(toolCallId);
25084
25276
  try {
25085
25277
  const msgCursor = await this.ctx.storage.sql.exec(
25086
25278
  `SELECT id, role, content, name, tool_call_id, tool_status, created_at, depth
@@ -25099,8 +25291,24 @@ ${resultContent}${attachmentSummary}`;
25099
25291
  // they are bound to one bridge socket and never park.
25100
25292
  /** Default time to wait for the client to accept a stream open. */
25101
25293
  static CLIENT_STREAM_OPEN_TIMEOUT_MS = 3e4;
25102
- /** Poll cadence while waiting for a disconnected owner's bridge to return. */
25103
- static OWNER_REATTACH_POLL_MS = 250;
25294
+ /**
25295
+ * In-memory waiters parked on the owner's bridge socket returning (AB-09).
25296
+ * Resolved by bridge attach and by execution-owner changes; each waiter
25297
+ * re-evaluates against the CURRENT owner on wake. In-memory only: a DO
25298
+ * restart drops waiters with the stream-open call that registered them.
25299
+ */
25300
+ ownerBridgeWaiters = /* @__PURE__ */ new Set();
25301
+ notifyOwnerBridgeWaiters() {
25302
+ if (!this.ownerBridgeWaiters || this.ownerBridgeWaiters.size === 0) return;
25303
+ const waiters = Array.from(this.ownerBridgeWaiters);
25304
+ this.ownerBridgeWaiters.clear();
25305
+ for (const wake of waiters) {
25306
+ try {
25307
+ wake();
25308
+ } catch {
25309
+ }
25310
+ }
25311
+ }
25104
25312
  /**
25105
25313
  * The thread HAS an elected execution owner, but its bridge socket is gone
25106
25314
  * (daemon restart, network blip, DO handoff). Summon the owning machine
@@ -25109,6 +25317,11 @@ ${resultContent}${attachmentSummary}`;
25109
25317
  * Sama One turns with "workspace unavailable" whenever a slow tool/approval
25110
25318
  * outlived the socket. Threads with NO recorded owner (subagent children)
25111
25319
  * still fail fast so ancestor walk-up routing stays immediate.
25320
+ *
25321
+ * The wait parks on bridge-attach/owner-change signals under ONE hard
25322
+ * deadline — no re-poll cadence. Each wake re-reads the owner because the
25323
+ * returning daemon may reclaim with a new generation, and the socket must
25324
+ * match the CURRENT owner.
25112
25325
  */
25113
25326
  async awaitOwnerBridgeSocket(request) {
25114
25327
  if (request.wake?.userId) {
@@ -25116,15 +25329,25 @@ ${resultContent}${attachmentSummary}`;
25116
25329
  }
25117
25330
  const timeoutMs = typeof request.timeoutMs === "number" && request.timeoutMs > 0 ? request.timeoutMs : _DurableThread.CLIENT_STREAM_OPEN_TIMEOUT_MS;
25118
25331
  const deadline = Date.now() + timeoutMs;
25119
- while (Date.now() < deadline) {
25120
- await new Promise(
25121
- (resolve) => setTimeout(resolve, _DurableThread.OWNER_REATTACH_POLL_MS)
25122
- );
25332
+ this.ownerBridgeWaiters ??= /* @__PURE__ */ new Set();
25333
+ while (true) {
25123
25334
  const owner = await this.getExecutionOwner();
25124
25335
  const socket = this.getOpenBridgeSocket(owner?.client_id ?? null);
25125
25336
  if (socket) return socket;
25337
+ const remainingMs = deadline - Date.now();
25338
+ if (remainingMs <= 0) return null;
25339
+ await new Promise((resolve) => {
25340
+ const timer = setTimeout(() => {
25341
+ this.ownerBridgeWaiters.delete(wake);
25342
+ resolve();
25343
+ }, remainingMs);
25344
+ const wake = () => {
25345
+ clearTimeout(timer);
25346
+ resolve();
25347
+ };
25348
+ this.ownerBridgeWaiters.add(wake);
25349
+ });
25126
25350
  }
25127
- return null;
25128
25351
  }
25129
25352
  /** Open a bidirectional byte stream to the connected owner client. */
25130
25353
  async openClientStream(request) {
@@ -25268,27 +25491,38 @@ ${resultContent}${attachmentSummary}`;
25268
25491
  done: false,
25269
25492
  error: void 0,
25270
25493
  pump: Promise.resolve(),
25271
- close: (error) => duplex.close(error)
25494
+ close: (error) => duplex.close(error),
25495
+ waiter: null,
25496
+ notify: () => {
25497
+ const waiter = entry.waiter;
25498
+ entry.waiter = null;
25499
+ waiter?.();
25500
+ }
25272
25501
  };
25273
25502
  entry.pump = (async () => {
25274
25503
  try {
25275
25504
  while (true) {
25276
25505
  const { value, done } = await reader.read();
25277
25506
  if (done) break;
25278
- if (value) entry.buffer.push(value);
25507
+ if (value) {
25508
+ entry.buffer.push(value);
25509
+ entry.notify();
25510
+ }
25279
25511
  }
25280
25512
  } catch (error) {
25281
25513
  entry.error = error instanceof Error ? error.message : String(error);
25282
25514
  } finally {
25283
25515
  entry.done = true;
25516
+ entry.notify();
25284
25517
  }
25285
25518
  })();
25286
25519
  void duplex.done.then((d) => {
25287
25520
  entry.done = true;
25288
25521
  if (!d.ok && !entry.error) entry.error = d.error;
25522
+ entry.notify();
25289
25523
  });
25290
25524
  this.proxyStreams.set(id, entry);
25291
- return { ok: true, id };
25525
+ return { ok: true, id, parkedPull: true };
25292
25526
  }
25293
25527
  async proxyStreamWrite(id, dataB64) {
25294
25528
  const entry = this.proxyStreams.get(id);
@@ -25298,9 +25532,22 @@ ${resultContent}${attachmentSummary}`;
25298
25532
  } catch {
25299
25533
  }
25300
25534
  }
25301
- async proxyStreamPull(id) {
25535
+ async proxyStreamPull(id, maxWaitMs) {
25302
25536
  const entry = this.proxyStreams.get(id);
25303
25537
  if (!entry) return { chunks: [], done: true, error: "unknown proxy stream" };
25538
+ if (entry.buffer.length === 0 && !entry.done) {
25539
+ await new Promise((resolve) => {
25540
+ const deadline = typeof maxWaitMs === "number" && maxWaitMs > 0 ? setTimeout(() => {
25541
+ if (entry.waiter === wake) entry.waiter = null;
25542
+ resolve();
25543
+ }, maxWaitMs) : null;
25544
+ const wake = () => {
25545
+ if (deadline) clearTimeout(deadline);
25546
+ resolve();
25547
+ };
25548
+ entry.waiter = wake;
25549
+ });
25550
+ }
25304
25551
  const chunks = entry.buffer.splice(0).map((b) => encodeStreamChunk(b));
25305
25552
  if (entry.done && entry.buffer.length === 0) {
25306
25553
  this.proxyStreams.delete(id);
@@ -25320,6 +25567,9 @@ ${resultContent}${attachmentSummary}`;
25320
25567
  const entry = this.proxyStreams.get(id);
25321
25568
  if (!entry) return;
25322
25569
  this.proxyStreams.delete(id);
25570
+ entry.done = true;
25571
+ if (!entry.error) entry.error = "aborted";
25572
+ entry.notify();
25323
25573
  try {
25324
25574
  entry.close("aborted");
25325
25575
  } catch {
@@ -25675,6 +25925,7 @@ ${resultContent}${attachmentSummary}`;
25675
25925
  this.broadcastEvent(type, data);
25676
25926
  }
25677
25927
  broadcastEvent(type, data) {
25928
+ if (!this.messageSockets || this.messageSockets.size === 0) return;
25678
25929
  const payload = JSON.stringify({
25679
25930
  type: "event",
25680
25931
  eventType: type,
@@ -26101,6 +26352,7 @@ ${resultContent}${attachmentSummary}`;
26101
26352
  } catch {
26102
26353
  }
26103
26354
  this.isExecuting = false;
26355
+ this.notifyExecutionIdleWaiters();
26104
26356
  this.currentAbortController = null;
26105
26357
  await this.recordRegistryActivity(threadId, false);
26106
26358
  tookOver = true;
@@ -26293,9 +26545,22 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
26293
26545
  await this.assertNotTerminated();
26294
26546
  if (this.isExecuting) {
26295
26547
  const busyWaitBudgetMs = 3e4;
26296
- const waitStartedAt = Date.now();
26297
- while (this.isExecuting && Date.now() - waitStartedAt < busyWaitBudgetMs) {
26298
- await new Promise((resolve) => setTimeout(resolve, 500));
26548
+ const deadline = Date.now() + busyWaitBudgetMs;
26549
+ this.executionIdleWaiters ??= /* @__PURE__ */ new Set();
26550
+ while (this.isExecuting) {
26551
+ const remainingMs = deadline - Date.now();
26552
+ if (remainingMs <= 0) break;
26553
+ await new Promise((resolve) => {
26554
+ const timer = setTimeout(() => {
26555
+ this.executionIdleWaiters.delete(wake);
26556
+ resolve();
26557
+ }, remainingMs);
26558
+ const wake = () => {
26559
+ clearTimeout(timer);
26560
+ resolve();
26561
+ };
26562
+ this.executionIdleWaiters.add(wake);
26563
+ });
26299
26564
  }
26300
26565
  if (this.isExecuting) {
26301
26566
  throw new Error(
@@ -26434,6 +26699,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
26434
26699
  return flowResult;
26435
26700
  } finally {
26436
26701
  this.isExecuting = false;
26702
+ this.notifyExecutionIdleWaiters();
26437
26703
  this.currentAbortController = null;
26438
26704
  await this.disarmExecutionWatchdog(threadId);
26439
26705
  await this.recordRegistryActivity(threadId, false);
@@ -26647,6 +26913,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
26647
26913
  }, true);
26648
26914
  } finally {
26649
26915
  this.isExecuting = false;
26916
+ this.notifyExecutionIdleWaiters();
26650
26917
  this.currentAbortController = null;
26651
26918
  await this.disarmExecutionWatchdog(threadId);
26652
26919
  await this.recordRegistryActivity(threadId, false);
@@ -26773,6 +27040,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
26773
27040
  );
26774
27041
  } finally {
26775
27042
  this.isExecuting = false;
27043
+ this.notifyExecutionIdleWaiters();
26776
27044
  this.currentAbortController = null;
26777
27045
  await this.disarmExecutionWatchdog(threadId);
26778
27046
  await this.recordRegistryActivity(threadId, false);
@@ -27199,6 +27467,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
27199
27467
  const fs = this.getFileStorage();
27200
27468
  const from = normalizePath2(sourcePath);
27201
27469
  const record = await fs.move(sourcePath, destinationPath);
27470
+ this.broadcastEvent("file_moved", { from, path: record.path, file: record });
27202
27471
  this.broadcastEvent("file_deleted", { path: from });
27203
27472
  this.broadcastEvent("file_created", { path: record.path, file: record });
27204
27473
  return { success: true, file: record };
@@ -27217,6 +27486,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
27217
27486
  const fs = this.getFileStorage();
27218
27487
  const from = normalizePath2(path);
27219
27488
  const record = await fs.rename(path, newName);
27489
+ this.broadcastEvent("file_moved", { from, path: record.path, file: record });
27220
27490
  this.broadcastEvent("file_deleted", { path: from });
27221
27491
  this.broadcastEvent("file_created", { path: record.path, file: record });
27222
27492
  return { success: true, file: record };