@bastani/atomic 0.9.39-alpha.2 → 0.9.40-alpha.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.
Files changed (261) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +20 -0
  3. package/dist/builtin/intercom/broker/broker.ts +523 -72
  4. package/dist/builtin/intercom/broker/client.ts +136 -5
  5. package/dist/builtin/intercom/broker/database-wake-up-bus.ts +274 -0
  6. package/dist/builtin/intercom/broker/group-membership.ts +2 -2
  7. package/dist/builtin/intercom/broker/multinode-router.ts +1500 -0
  8. package/dist/builtin/intercom/broker/multinode-state-store.ts +569 -0
  9. package/dist/builtin/intercom/broker/open-broker-state.ts +160 -0
  10. package/dist/builtin/intercom/broker/paths.ts +6 -0
  11. package/dist/builtin/intercom/broker/pending-send-registry.ts +14 -0
  12. package/dist/builtin/intercom/broker/recipient-receipts.ts +93 -0
  13. package/dist/builtin/intercom/broker/restoring-recipients.ts +263 -0
  14. package/dist/builtin/intercom/broker/send-handler.ts +241 -25
  15. package/dist/builtin/intercom/broker/session-id-reclaim.ts +41 -0
  16. package/dist/builtin/intercom/broker/socket-writes.ts +47 -0
  17. package/dist/builtin/intercom/broker/spawn.ts +10 -2
  18. package/dist/builtin/intercom/broker/sql-held-send-store.ts +96 -0
  19. package/dist/builtin/intercom/broker/sql-known-session-store.ts +129 -0
  20. package/dist/builtin/intercom/broker/sql-lease-store.ts +194 -0
  21. package/dist/builtin/intercom/broker/sql-outbox-store.ts +249 -0
  22. package/dist/builtin/intercom/broker/sql-pending-ask-store.ts +105 -0
  23. package/dist/builtin/intercom/broker/sql-restoring-reservation-store.ts +295 -0
  24. package/dist/builtin/intercom/broker/sql-sent-message-authority.ts +126 -0
  25. package/dist/builtin/intercom/broker/supervisor-channel.ts +10 -0
  26. package/dist/builtin/intercom/broker/wake-up-bus.ts +60 -0
  27. package/dist/builtin/intercom/index.bundle.mjs +793 -91
  28. package/dist/builtin/intercom/package.json +1 -1
  29. package/dist/builtin/intercom/retry-policy.ts +4 -0
  30. package/dist/builtin/intercom/session-target.ts +24 -0
  31. package/dist/builtin/intercom/types.ts +57 -3
  32. package/dist/builtin/mcp/package.json +1 -1
  33. package/dist/builtin/subagents/CHANGELOG.md +37 -0
  34. package/dist/builtin/subagents/README.md +18 -12
  35. package/dist/builtin/subagents/package.json +1 -1
  36. package/dist/builtin/subagents/skills/agent-browser/SKILL.md +1 -1
  37. package/dist/builtin/subagents/skills/cua-driver/BROWSER.md +55 -8
  38. package/dist/builtin/subagents/skills/cua-driver/EMBEDDING.md +7 -2
  39. package/dist/builtin/subagents/skills/cua-driver/LINUX.md +7 -1
  40. package/dist/builtin/subagents/skills/cua-driver/MACOS.md +151 -27
  41. package/dist/builtin/subagents/skills/cua-driver/RUNTIME.md +1 -1
  42. package/dist/builtin/subagents/skills/cua-driver/SKILL.md +33 -17
  43. package/dist/builtin/subagents/skills/cua-driver/WINDOWS.md +5 -0
  44. package/dist/builtin/subagents/skills/cua-driver/WORKFLOW.md +46 -8
  45. package/dist/builtin/subagents/skills/subagent/SKILL.md +18 -20
  46. package/dist/builtin/subagents/src/extension/index.bundle.mjs +8032 -6034
  47. package/dist/builtin/web-access/package.json +1 -1
  48. package/dist/builtin/workflows/CHANGELOG.md +32 -0
  49. package/dist/builtin/workflows/README.md +1 -1
  50. package/dist/builtin/workflows/package.json +1 -1
  51. package/dist/builtin/workflows/skills/impeccable/SKILL.md +8 -8
  52. package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +3 -3
  53. package/dist/builtin/workflows/skills/impeccable/reference/adapt.native.md +1 -1
  54. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +4 -2
  55. package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
  56. package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +1 -1
  57. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +22 -9
  58. package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +2 -2
  59. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +3 -3
  60. package/dist/builtin/workflows/skills/impeccable/reference/extract.md +3 -3
  61. package/dist/builtin/workflows/skills/impeccable/reference/harden.md +2 -4
  62. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +4 -4
  63. package/dist/builtin/workflows/skills/impeccable/reference/live.md +1 -1
  64. package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +6 -6
  65. package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +3 -3
  66. package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +3 -3
  67. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +3 -3
  68. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
  69. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +10 -0
  70. package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +1 -1
  71. package/dist/builtin/workflows/skills/impeccable/scripts/VERSION +1 -1
  72. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable +6 -0
  73. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable.cmd +47 -22
  74. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +34 -1
  75. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-session.js +25 -1
  76. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +33 -8
  77. package/dist/builtin/workflows/src/durable/journal/resolution.d.ts +7 -7
  78. package/dist/builtin/workflows/src/extension/index.bundle.mjs +22855 -22308
  79. package/dist/builtin/workflows/src/index.js +106 -35
  80. package/dist/cli/args.d.ts +2 -0
  81. package/dist/cli/args.d.ts.map +1 -1
  82. package/dist/cli/args.js +16 -0
  83. package/dist/cli/args.js.map +1 -1
  84. package/dist/core/agent-session-extension-bindings.d.ts +2 -0
  85. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  86. package/dist/core/agent-session-extension-bindings.js +6 -6
  87. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  88. package/dist/core/agent-session-methods.d.ts +6 -3
  89. package/dist/core/agent-session-methods.d.ts.map +1 -1
  90. package/dist/core/agent-session-methods.js.map +1 -1
  91. package/dist/core/agent-session-persistent-custom-messages.d.ts +7 -2
  92. package/dist/core/agent-session-persistent-custom-messages.d.ts.map +1 -1
  93. package/dist/core/agent-session-persistent-custom-messages.js +29 -12
  94. package/dist/core/agent-session-persistent-custom-messages.js.map +1 -1
  95. package/dist/core/agent-session-recovery.d.ts +14 -0
  96. package/dist/core/agent-session-recovery.d.ts.map +1 -0
  97. package/dist/core/agent-session-recovery.js +83 -0
  98. package/dist/core/agent-session-recovery.js.map +1 -0
  99. package/dist/core/agent-session-tasks.d.ts.map +1 -1
  100. package/dist/core/agent-session-tasks.js +4 -6
  101. package/dist/core/agent-session-tasks.js.map +1 -1
  102. package/dist/core/agent-session-types.d.ts +13 -0
  103. package/dist/core/agent-session-types.d.ts.map +1 -1
  104. package/dist/core/agent-session-types.js.map +1 -1
  105. package/dist/core/agent-session.d.ts +2 -1
  106. package/dist/core/agent-session.d.ts.map +1 -1
  107. package/dist/core/agent-session.js +4 -1
  108. package/dist/core/agent-session.js.map +1 -1
  109. package/dist/core/extensions/api-types.d.ts +6 -2
  110. package/dist/core/extensions/api-types.d.ts.map +1 -1
  111. package/dist/core/extensions/api-types.js.map +1 -1
  112. package/dist/core/extensions/event-results.d.ts +21 -0
  113. package/dist/core/extensions/event-results.d.ts.map +1 -1
  114. package/dist/core/extensions/event-results.js.map +1 -1
  115. package/dist/core/extensions/index.d.ts +1 -1
  116. package/dist/core/extensions/index.d.ts.map +1 -1
  117. package/dist/core/extensions/index.js.map +1 -1
  118. package/dist/core/extensions/loader-api.d.ts.map +1 -1
  119. package/dist/core/extensions/loader-api.js +4 -0
  120. package/dist/core/extensions/loader-api.js.map +1 -1
  121. package/dist/core/extensions/runner-events.d.ts +9 -2
  122. package/dist/core/extensions/runner-events.d.ts.map +1 -1
  123. package/dist/core/extensions/runner-events.js +29 -0
  124. package/dist/core/extensions/runner-events.js.map +1 -1
  125. package/dist/core/extensions/runner.d.ts +5 -3
  126. package/dist/core/extensions/runner.d.ts.map +1 -1
  127. package/dist/core/extensions/runner.js +7 -2
  128. package/dist/core/extensions/runner.js.map +1 -1
  129. package/dist/core/extensions/runtime-types.d.ts +1 -0
  130. package/dist/core/extensions/runtime-types.d.ts.map +1 -1
  131. package/dist/core/extensions/runtime-types.js.map +1 -1
  132. package/dist/core/extensions/session-events.d.ts +30 -2
  133. package/dist/core/extensions/session-events.d.ts.map +1 -1
  134. package/dist/core/extensions/session-events.js.map +1 -1
  135. package/dist/core/sdk-types.d.ts +3 -2
  136. package/dist/core/sdk-types.d.ts.map +1 -1
  137. package/dist/core/sdk-types.js.map +1 -1
  138. package/dist/core/sdk.d.ts.map +1 -1
  139. package/dist/core/sdk.js +3 -0
  140. package/dist/core/sdk.js.map +1 -1
  141. package/dist/core/session-storage.d.ts.map +1 -1
  142. package/dist/core/session-storage.js +15 -9
  143. package/dist/core/session-storage.js.map +1 -1
  144. package/dist/core/storage/process-selection.d.ts +39 -0
  145. package/dist/core/storage/process-selection.d.ts.map +1 -0
  146. package/dist/core/storage/process-selection.js +154 -0
  147. package/dist/core/storage/process-selection.js.map +1 -0
  148. package/dist/core/storage/provider.d.ts +6 -1
  149. package/dist/core/storage/provider.d.ts.map +1 -1
  150. package/dist/core/storage/provider.js +3 -2
  151. package/dist/core/storage/provider.js.map +1 -1
  152. package/dist/core/storage/selection.d.ts +1 -1
  153. package/dist/core/storage/selection.d.ts.map +1 -1
  154. package/dist/core/storage/selection.js.map +1 -1
  155. package/dist/core/storage/session/jsonl-session-store.d.ts +2 -0
  156. package/dist/core/storage/session/jsonl-session-store.d.ts.map +1 -1
  157. package/dist/core/storage/session/jsonl-session-store.js +38 -2
  158. package/dist/core/storage/session/jsonl-session-store.js.map +1 -1
  159. package/dist/core/tasks/completion.d.ts +11 -2
  160. package/dist/core/tasks/completion.d.ts.map +1 -1
  161. package/dist/core/tasks/completion.js +40 -16
  162. package/dist/core/tasks/completion.js.map +1 -1
  163. package/dist/core/tasks/pause.d.ts +4 -1
  164. package/dist/core/tasks/pause.d.ts.map +1 -1
  165. package/dist/core/tasks/pause.js +8 -5
  166. package/dist/core/tasks/pause.js.map +1 -1
  167. package/dist/index-extensions.d.ts +1 -1
  168. package/dist/index-extensions.d.ts.map +1 -1
  169. package/dist/index-extensions.js.map +1 -1
  170. package/dist/index.d.ts +2 -1
  171. package/dist/index.d.ts.map +1 -1
  172. package/dist/index.js +2 -0
  173. package/dist/index.js.map +1 -1
  174. package/dist/main.d.ts.map +1 -1
  175. package/dist/main.js +7 -1
  176. package/dist/main.js.map +1 -1
  177. package/dist/modes/interactive/components/chat-session-host-rendering.d.ts.map +1 -1
  178. package/dist/modes/interactive/components/chat-session-host-rendering.js +11 -1
  179. package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
  180. package/dist/modes/interactive/components/chat-session-host-state.d.ts +2 -0
  181. package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
  182. package/dist/modes/interactive/components/chat-session-host-state.js +2 -0
  183. package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
  184. package/dist/modes/interactive/interactive-deferred-startup.d.ts.map +1 -1
  185. package/dist/modes/interactive/interactive-deferred-startup.js +4 -0
  186. package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
  187. package/dist/modes/interactive/interactive-mode-surface.d.ts +2 -1
  188. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  189. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  190. package/dist/modes/interactive/interactive-render-chat.js +3 -0
  191. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  192. package/dist/modes/interactive/interactive-session-routing.js +2 -0
  193. package/dist/modes/interactive/interactive-session-routing.js.map +1 -1
  194. package/dist/modes/interactive/interactive-session-runtime.d.ts +2 -1
  195. package/dist/modes/interactive/interactive-session-runtime.d.ts.map +1 -1
  196. package/dist/modes/interactive/interactive-session-runtime.js +29 -0
  197. package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
  198. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  199. package/dist/modes/interactive/interactive-startup.js +4 -1
  200. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  201. package/dist/modes/interactive-engine/engine-args.d.ts.map +1 -1
  202. package/dist/modes/interactive-engine/engine-args.js +4 -0
  203. package/dist/modes/interactive-engine/engine-args.js.map +1 -1
  204. package/dist/modes/interactive-engine/engine-render-service.d.ts.map +1 -1
  205. package/dist/modes/interactive-engine/engine-render-service.js +4 -4
  206. package/dist/modes/interactive-engine/engine-render-service.js.map +1 -1
  207. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  208. package/dist/modes/interactive-engine/isolated-runtime.js +4 -0
  209. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  210. package/dist/modes/interactive-engine/remote-renderer.js +1 -1
  211. package/dist/modes/interactive-engine/remote-renderer.js.map +1 -1
  212. package/dist/modes/rpc/rpc-client-api.d.ts +4 -1
  213. package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
  214. package/dist/modes/rpc/rpc-client-api.js +4 -0
  215. package/dist/modes/rpc/rpc-client-api.js.map +1 -1
  216. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  217. package/dist/modes/rpc/rpc-command-handler.js +3 -0
  218. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  219. package/dist/modes/rpc/rpc-command-timeouts.d.ts.map +1 -1
  220. package/dist/modes/rpc/rpc-command-timeouts.js +1 -0
  221. package/dist/modes/rpc/rpc-command-timeouts.js.map +1 -1
  222. package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
  223. package/dist/modes/rpc/rpc-input-scheduler.js +6 -1
  224. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  225. package/dist/modes/rpc/rpc-types.d.ts +12 -1
  226. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  227. package/dist/modes/rpc/rpc-types.js.map +1 -1
  228. package/dist/utils/shell.d.ts.map +1 -1
  229. package/dist/utils/shell.js +44 -11
  230. package/dist/utils/shell.js.map +1 -1
  231. package/dist/utils/tools-manager.js +1 -1
  232. package/dist/utils/tools-manager.js.map +1 -1
  233. package/docs/background-tasks.md +5 -3
  234. package/docs/computer-use.md +2 -2
  235. package/docs/environment-variables.md +4 -1
  236. package/docs/extensions/events.md +29 -2
  237. package/docs/getting-started/first-session.md +1 -1
  238. package/docs/guides/intercom.md +1 -1
  239. package/docs/intercom/operations.md +103 -1
  240. package/docs/intercom/reference.md +10 -3
  241. package/docs/intercom.md +8 -6
  242. package/docs/keybindings.md +1 -1
  243. package/docs/models.md +1 -1
  244. package/docs/providers.md +4 -2
  245. package/docs/reference/cli.md +5 -0
  246. package/docs/rpc/protocol.md +19 -0
  247. package/docs/rpc.md +3 -0
  248. package/docs/sdk/web-server.md +1 -1
  249. package/docs/sdk.md +5 -2
  250. package/docs/sessions.md +17 -1
  251. package/docs/settings.md +5 -3
  252. package/docs/skills.md +1 -1
  253. package/docs/storage.md +54 -3
  254. package/docs/subagents.md +49 -9
  255. package/docs/usage.md +1 -1
  256. package/docs/windows.md +14 -4
  257. package/docs/workflows/api-reference.md +1 -1
  258. package/docs/workflows/authoring.md +1 -1
  259. package/docs/workflows/operations.md +10 -6
  260. package/npm-shrinkwrap.json +37 -37
  261. package/package.json +4 -4
@@ -22,11 +22,14 @@ import { isMessage, isSessionInfo } from "./client-message-validation.js";
22
22
  import { normalizeGroups } from "../group.js";
23
23
  import { IntercomClientDisconnectedError } from "../recoverable-disconnect.js";
24
24
  import { IntercomLiveRouteRefusedError, isLiveWorkflowStageRouteRefusalCode } from "../live-route-refusal.js";
25
+ import { SEND_RESPONSE_TIMEOUT_MS } from "../retry-policy.js";
25
26
 
26
27
  const BROKER_SOCKET = getBrokerSocketPath();
27
28
  const GROUP_REQUEST_TIMEOUT_MS = 5000;
28
29
  const PRESENCE_ACK_TIMEOUT_MS = 5000;
29
-
30
+ const RESTORING_RECIPIENT_REQUEST_TIMEOUT_MS = 5000;
31
+ /** Extra wait past a broker hold, so the broker's own outcome for a held send always arrives first (#3760). */
32
+ const HELD_SEND_GRACE_MS = 5000;
30
33
 
31
34
  export interface SendOptions {
32
35
  text: string;
@@ -41,10 +44,17 @@ export interface SendOptions {
41
44
  requirePendingReply?: true;
42
45
  /** An explicit reply selector must resolve to this frozen recipient, never a namesake. */
43
46
  expectedRecipientId?: string;
44
- /** Broker-authorized identity for a canonical-path ask, delivered before its recipient can reply. */
47
+ /** Broker-authorized recipient identity for an ask, delivered before its recipient can reply. */
45
48
  onReplyTarget?: (sessionId: string) => void;
46
49
  }
47
50
 
51
+ export interface IntercomClientOptions {
52
+ /** Intercom session id held before a restart or reconnect; the broker grants it only when it is free (#3760). */
53
+ readonly preferredSessionId?: string;
54
+ /** #3761: this client confirms each received message with `acknowledgeReceived` once it has saved it. */
55
+ readonly acknowledgesReceipt?: true;
56
+ }
57
+
48
58
  export interface SendResult {
49
59
  id: string;
50
60
  delivered: boolean;
@@ -152,6 +162,10 @@ function isWorkflowStageRosterEntries(value: unknown): value is WorkflowStageRos
152
162
 
153
163
  export class IntercomClient extends EventEmitter {
154
164
  private readonly returnAddress: string;
165
+ private readonly preferredSessionId: string | undefined;
166
+ private readonly acknowledgesReceipt: boolean;
167
+ /** #3761: the broker of the current registration echoed `acknowledgesReceipt`, so it understands `received`. */
168
+ private brokerAcknowledgesReceipt = false;
155
169
  private socket: net.Socket | null = null;
156
170
  private _sessionId: string | null = null;
157
171
  private _supervisorSessionId: string | null = null;
@@ -174,13 +188,16 @@ export class IntercomClient extends EventEmitter {
174
188
  resolve: (authorization: SupervisorAuthorization) => void;
175
189
  reject: (error: Error) => void;
176
190
  }>();
191
+ private pendingRestoringRecipientRequests = new Map<string, { resolve(): void; reject(error: Error): void }>();
177
192
  private disconnecting = false;
178
193
  private disconnectError: Error | null = null;
179
194
 
180
- constructor(returnAddress: string = randomUUID()) {
195
+ constructor(returnAddress: string = randomUUID(), options: IntercomClientOptions = {}) {
181
196
  super();
182
197
  if (returnAddress.length === 0) throw new Error("Intercom return address must not be empty");
183
198
  this.returnAddress = returnAddress;
199
+ this.preferredSessionId = options.preferredSessionId;
200
+ this.acknowledgesReceipt = options.acknowledgesReceipt === true;
184
201
  }
185
202
 
186
203
  private failPending(error: Error): void {
@@ -188,12 +205,14 @@ export class IntercomClient extends EventEmitter {
188
205
  for (const pending of this.pendingGroupLists.values()) pending.reject(error);
189
206
  for (const pending of this.pendingLists.values()) pending.reject(error);
190
207
  for (const pending of this.pendingSupervisorAuthorizations.values()) pending.reject(error);
208
+ for (const pending of this.pendingRestoringRecipientRequests.values()) pending.reject(error);
191
209
  for (const pending of this.pendingPresence.values()) pending.reject(error);
192
210
  for (const pending of this.pendingMemberships.values()) pending.reject(error);
193
211
  for (const pending of this.pendingLiveWorkflowStageRoutes.values()) pending.reject(error);
194
212
  this.pendingLists.clear();
195
213
  this.pendingGroupLists.clear();
196
214
  this.pendingSupervisorAuthorizations.clear();
215
+ this.pendingRestoringRecipientRequests.clear();
197
216
  this.pendingPresence.clear();
198
217
  this.pendingMemberships.clear();
199
218
  this.pendingLiveWorkflowStageRoutes.clear();
@@ -298,6 +317,7 @@ export class IntercomClient extends EventEmitter {
298
317
  }
299
318
  this._sessionId = null;
300
319
  this._supervisorSessionId = null;
320
+ this.brokerAcknowledgesReceipt = false;
301
321
  this._messageSource = undefined;
302
322
  this.disconnectError = null;
303
323
  if (connectionEstablished && !wasDisconnecting) {
@@ -371,6 +391,8 @@ export class IntercomClient extends EventEmitter {
371
391
  session,
372
392
  ...(registrationGroup !== undefined ? { registrationGroup } : {}),
373
393
  returnAddress: this.returnAddress,
394
+ ...(this.preferredSessionId === undefined ? {} : { preferredSessionId: this.preferredSessionId }),
395
+ ...(this.acknowledgesReceipt ? { acknowledgesReceipt: true as const } : {}),
374
396
  ...(supervisor ? { supervisor } : {}),
375
397
  ...(supervisorOwnerToken ? { supervisorOwnerToken } : {}),
376
398
  });
@@ -402,7 +424,8 @@ export class IntercomClient extends EventEmitter {
402
424
  switch (brokerMessage.type) {
403
425
  case "registered": {
404
426
  if (typeof brokerMessage.sessionId !== "string"
405
- || (brokerMessage.supervisorSessionId !== undefined && typeof brokerMessage.supervisorSessionId !== "string")) {
427
+ || (brokerMessage.supervisorSessionId !== undefined && typeof brokerMessage.supervisorSessionId !== "string")
428
+ || (brokerMessage.acknowledgesReceipt !== undefined && brokerMessage.acknowledgesReceipt !== true)) {
406
429
  throw new Error("Invalid registered message");
407
430
  }
408
431
 
@@ -413,6 +436,8 @@ export class IntercomClient extends EventEmitter {
413
436
  this._supervisorSessionId = typeof brokerMessage.supervisorSessionId === "string"
414
437
  ? brokerMessage.supervisorSessionId
415
438
  : null;
439
+ // #3761: a broker from before receipts never echoes the capability and drops a client that sends `received`.
440
+ this.brokerAcknowledgesReceipt = this.acknowledgesReceipt && brokerMessage.acknowledgesReceipt === true;
416
441
  this.emit("_registered", { type: "registered", sessionId: brokerMessage.sessionId });
417
442
  break;
418
443
  }
@@ -573,6 +598,12 @@ export class IntercomClient extends EventEmitter {
573
598
  } satisfies StickyLiveDeliveredNotice);
574
599
  break;
575
600
  }
601
+ case "pending_stage_route_revoked": {
602
+ const { runId } = brokerMessage;
603
+ if (typeof runId !== "string") throw new Error("Invalid pending-stage route revocation");
604
+ this.emit("pending_stage_route_revoked", { runId });
605
+ break;
606
+ }
576
607
  case "live_workflow_stage_route_registered": {
577
608
  const { requestId } = brokerMessage;
578
609
  if (typeof requestId !== "string") throw new Error("Invalid live workflow-stage route registration");
@@ -606,6 +637,33 @@ export class IntercomClient extends EventEmitter {
606
637
  else this.pendingSends.resolve(messageId, attemptId, result);
607
638
  break;
608
639
  }
640
+ case "send_held": {
641
+ const { messageId, attemptId, timeoutMs } = brokerMessage;
642
+ if (
643
+ typeof messageId !== "string" ||
644
+ (attemptId !== undefined && typeof attemptId !== "string") ||
645
+ typeof timeoutMs !== "number" ||
646
+ !Number.isFinite(timeoutMs) ||
647
+ timeoutMs <= 0
648
+ ) {
649
+ throw new Error("Invalid send_held message");
650
+ }
651
+ // #3760: the recipient is a subagent being restored; the broker answers within `timeoutMs`.
652
+ this.pendingSends.extend(messageId, attemptId, timeoutMs + HELD_SEND_GRACE_MS);
653
+ break;
654
+ }
655
+ case "restoring_recipient_ack": {
656
+ const { requestId, error } = brokerMessage;
657
+ if (typeof requestId !== "string" || (error !== undefined && typeof error !== "string")) {
658
+ throw new Error("Invalid restoring_recipient_ack message");
659
+ }
660
+ const pending = this.pendingRestoringRecipientRequests.get(requestId);
661
+ if (!pending) return;
662
+ this.pendingRestoringRecipientRequests.delete(requestId);
663
+ if (error === undefined) pending.resolve();
664
+ else pending.reject(new Error(error));
665
+ break;
666
+ }
609
667
  case "delivered": {
610
668
  const { messageId, attemptId } = brokerMessage;
611
669
  if (typeof messageId !== "string" || (attemptId !== undefined && typeof attemptId !== "string")) {
@@ -890,6 +948,66 @@ export class IntercomClient extends EventEmitter {
890
948
  }
891
949
  });
892
950
  }
951
+
952
+ /**
953
+ * #3760: hold sends to the child that `capability` (issued to this session by `authorizeSupervisorChild`)
954
+ * names, and to its `aliases`, until the child registers with that capability. Calling again adds aliases.
955
+ * A `dormant` hold keeps the child addressable until the supervisor that restores it takes the hold over.
956
+ */
957
+ reserveRestoringRecipient(
958
+ capability: string,
959
+ options: { group?: string; aliases?: readonly string[]; dormant?: boolean } = {},
960
+ ): Promise<void> {
961
+ return this.restoringRecipientRequest({
962
+ type: "reserve_restoring_recipient",
963
+ capability,
964
+ ...(options.group === undefined ? {} : { group: options.group }),
965
+ ...(options.aliases === undefined ? {} : { aliases: [...options.aliases] }),
966
+ ...(options.dormant === true ? { dormant: true } : {}),
967
+ });
968
+ }
969
+
970
+ /**
971
+ * #3760: end that hold. With `gone` (the reason), held and later senders are told the child is gone. With
972
+ * `refusal`, the senders held now are told that reason and later sends are routed normally. With neither,
973
+ * held sends are routed normally.
974
+ */
975
+ releaseRestoringRecipient(capability: string, outcome?: string | { gone?: string; refusal?: string }): Promise<void> {
976
+ const { gone, refusal } = typeof outcome === "string" ? { gone: outcome, refusal: undefined } : (outcome ?? {});
977
+ return this.restoringRecipientRequest({
978
+ type: "release_restoring_recipient",
979
+ capability,
980
+ ...(gone === undefined ? {} : { gone }),
981
+ ...(refusal === undefined ? {} : { refusal }),
982
+ });
983
+ }
984
+
985
+ private restoringRecipientRequest(frame: Record<string, unknown>): Promise<void> {
986
+ let socket: net.Socket;
987
+ try {
988
+ socket = this.requireActiveSocket();
989
+ } catch (error) {
990
+ return Promise.reject(toError(error));
991
+ }
992
+ const requestId = randomUUID();
993
+ return new Promise((resolve, reject) => {
994
+ const timeout = setTimeout(() => {
995
+ if (!this.pendingRestoringRecipientRequests.delete(requestId)) return;
996
+ reject(new Error("Restoring-recipient request timeout"));
997
+ }, RESTORING_RECIPIENT_REQUEST_TIMEOUT_MS);
998
+ this.pendingRestoringRecipientRequests.set(requestId, {
999
+ resolve: () => { clearTimeout(timeout); resolve(); },
1000
+ reject: (error) => { clearTimeout(timeout); reject(error); },
1001
+ });
1002
+ try {
1003
+ writeMessage(socket, { ...frame, requestId });
1004
+ } catch (error) {
1005
+ clearTimeout(timeout);
1006
+ this.pendingRestoringRecipientRequests.delete(requestId);
1007
+ reject(toError(error));
1008
+ }
1009
+ });
1010
+ }
893
1011
  registerPendingStageRoute(
894
1012
  runId: string,
895
1013
  group: string,
@@ -897,6 +1015,7 @@ export class IntercomClient extends EventEmitter {
897
1015
  stages?: WorkflowStageRosterAnnouncement[],
898
1016
  possibleStages?: WorkflowPossibleStageAnnouncement[],
899
1017
  parent?: WorkflowRunParentAnnouncement,
1018
+ dormant?: boolean,
900
1019
  ): void {
901
1020
  writeMessage(this.requireActiveSocket(), {
902
1021
  type: "register_pending_stage_route",
@@ -906,6 +1025,7 @@ export class IntercomClient extends EventEmitter {
906
1025
  stages,
907
1026
  possibleStages,
908
1027
  ...(parent === undefined ? {} : { parent }),
1028
+ ...(dormant === true ? { dormant: true as const } : {}),
909
1029
  });
910
1030
  }
911
1031
 
@@ -969,6 +1089,15 @@ export class IntercomClient extends EventEmitter {
969
1089
  return this.sendFrame("send", to, options);
970
1090
  }
971
1091
 
1092
+ /**
1093
+ * #3761: tell the broker this message is saved, so it reports `delivered` to its sender. A no-op unless the
1094
+ * current registration's broker echoed the capability: that broker already reported `delivered` on the socket write.
1095
+ */
1096
+ acknowledgeReceived(messageId: string): void {
1097
+ if (!this.brokerAcknowledgesReceipt) return;
1098
+ writeMessage(this.requireActiveSocket(), { type: "received", messageId });
1099
+ }
1100
+
972
1101
  sendPendingStageNotification(
973
1102
  runId: string,
974
1103
  capability: string,
@@ -1015,7 +1144,7 @@ export class IntercomClient extends EventEmitter {
1015
1144
  requirePendingReply: options.requirePendingReply ?? false,
1016
1145
  ...(options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId }),
1017
1146
  });
1018
- acquired = this.pendingSends.acquire(messageId, pendingSignature, 10000);
1147
+ acquired = this.pendingSends.acquire(messageId, pendingSignature, SEND_RESPONSE_TIMEOUT_MS);
1019
1148
  } catch (error) {
1020
1149
  return Promise.reject(toError(error));
1021
1150
  }
@@ -1040,6 +1169,8 @@ export class IntercomClient extends EventEmitter {
1040
1169
  ...(options.requirePendingReply === undefined ? {} : { requirePendingReply: options.requirePendingReply }),
1041
1170
  ...(options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId }),
1042
1171
  ...(options.onReplyTarget === undefined ? {} : { resolveReplyTarget: true as const }),
1172
+ // #3760: this client understands `send_held`, so the broker may hold the send for a subagent being restored.
1173
+ ...(type === "send" ? { awaitRestoringRecipient: true as const } : {}),
1043
1174
  message,
1044
1175
  attemptId: acquired.attempt.attemptId,
1045
1176
  ...extra,
@@ -0,0 +1,274 @@
1
+ import { type SqliteDatabase, type SqliteRow, validateStoragePrefix } from "@bastani/atomic/storage";
2
+ import { OUTBOX_BACKUP_POLL_INTERVAL_MS } from "./multinode-state-store.js";
3
+ import type {
4
+ WakeUpBus,
5
+ WakeUpEvent,
6
+ WakeUpHint,
7
+ WakeUpListener,
8
+ WakeUpPublishResult,
9
+ WakeUpSubscribeResult,
10
+ WakeUpSubscription,
11
+ } from "./wake-up-bus.js";
12
+
13
+ /**
14
+ * S4 wake-up bus (specs/2026-10-19-intercom-multi-node.md §5.6): a hint log in the shared storage database. Each
15
+ * connection polls the rows addressed to its subscribed brokers, delivers them and deletes them. A failed query drops
16
+ * the connection; the next poll or publish reopens it and every subscription gets a `reconnected` resync, because
17
+ * hints written meanwhile may have been pruned or missed. While the database stays unavailable, reopening backs off
18
+ * exponentially from the poll interval up to `WAKE_UP_RECONNECT_MAX_DELAY_MS`.
19
+ */
20
+
21
+ /** How often a connection with subscriptions reads its hints. */
22
+ export const WAKE_UP_POLL_INTERVAL_MS = 500;
23
+ /** Hints nobody consumed (their broker exited) are deleted after this age; the backup poll has covered them. */
24
+ export const WAKE_UP_HINT_RETENTION_MS = OUTBOX_BACKUP_POLL_INTERVAL_MS;
25
+ /**
26
+ * Hints read and deleted per statement; a poll repeats until a batch comes back short. Each batch's delete binds one
27
+ * parameter per hint, so a backlog never exceeds SQLite's bound-parameter limit (999 on old builds).
28
+ */
29
+ export const WAKE_UP_POLL_BATCH_SIZE = 500;
30
+ /** Longest wait between attempts to reopen an unavailable database; the backup poll covers the gap. */
31
+ export const WAKE_UP_RECONNECT_MAX_DELAY_MS = OUTBOX_BACKUP_POLL_INTERVAL_MS;
32
+
33
+ /** Runs `callback` once after `delayMs` and returns a function that cancels it. */
34
+ export type WakeUpPollScheduler = (callback: () => void, delayMs: number) => () => void;
35
+
36
+ export interface DatabaseWakeUpBusOptions {
37
+ /** Opens a connection to the shared storage database; called again after every dropped connection. */
38
+ readonly openDatabase: () => Promise<SqliteDatabase>;
39
+ /** The validated storage prefix; the table is `<prefix>intercom_wakeups`. */
40
+ readonly prefix?: string;
41
+ readonly pollIntervalMs?: number;
42
+ readonly schedule?: WakeUpPollScheduler;
43
+ }
44
+
45
+ interface HintRow extends SqliteRow {
46
+ readonly id: number;
47
+ readonly broker_id: string;
48
+ readonly reason: string;
49
+ readonly version: number;
50
+ }
51
+
52
+ interface DatabaseSubscription {
53
+ readonly brokerId: string;
54
+ readonly listener: WakeUpListener;
55
+ open: boolean;
56
+ }
57
+
58
+ const DATABASE_NOW_MS = "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
59
+
60
+ const scheduleTimeout: WakeUpPollScheduler = (callback, delayMs) => {
61
+ const timer = setTimeout(callback, delayMs);
62
+ timer.unref?.();
63
+ return () => clearTimeout(timer);
64
+ };
65
+
66
+ async function createSchema(database: SqliteDatabase, table: string): Promise<void> {
67
+ await database.exec(
68
+ `CREATE TABLE IF NOT EXISTS ${table} (
69
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
70
+ broker_id TEXT NOT NULL,
71
+ reason TEXT NOT NULL CHECK (reason IN ('outbox', 'outcome')),
72
+ version INTEGER NOT NULL,
73
+ created_at INTEGER NOT NULL
74
+ );
75
+ CREATE INDEX IF NOT EXISTS ${table}_broker ON ${table} (broker_id, id);
76
+ CREATE INDEX IF NOT EXISTS ${table}_created ON ${table} (created_at);`,
77
+ );
78
+ }
79
+
80
+ async function openSchemaConnection(options: DatabaseWakeUpBusOptions, table: string): Promise<SqliteDatabase> {
81
+ const database = await options.openDatabase();
82
+ try {
83
+ await createSchema(database, table);
84
+ } catch (cause) {
85
+ await database.close().catch(() => {});
86
+ throw cause;
87
+ }
88
+ return database;
89
+ }
90
+
91
+ class DatabaseWakeUpBus implements WakeUpBus {
92
+ private readonly subscriptions = new Set<DatabaseSubscription>();
93
+ private readonly options: DatabaseWakeUpBusOptions;
94
+ private readonly table: string;
95
+ private readonly pollIntervalMs: number;
96
+ private readonly schedule: WakeUpPollScheduler;
97
+ private database: SqliteDatabase | undefined;
98
+ private opening: Promise<SqliteDatabase | undefined> | undefined;
99
+ private cancelPoll: (() => void) | undefined;
100
+ private polling: Promise<void> | undefined;
101
+ private closed = false;
102
+ private closing: Promise<void> | undefined;
103
+ /** Consecutive failed attempts to reopen the database; each doubles the wait before the next. */
104
+ private reconnectFailures = 0;
105
+ private reconnectNotBefore = 0;
106
+
107
+ constructor(options: DatabaseWakeUpBusOptions, table: string, database: SqliteDatabase) {
108
+ this.options = options;
109
+ this.table = table;
110
+ this.database = database;
111
+ this.pollIntervalMs = options.pollIntervalMs ?? WAKE_UP_POLL_INTERVAL_MS;
112
+ this.schedule = options.schedule ?? scheduleTimeout;
113
+ }
114
+
115
+ async publish(hint: WakeUpHint): Promise<WakeUpPublishResult> {
116
+ if (hint.brokerId.length === 0) return { status: "invalid", reason: "empty-broker-id" };
117
+ const database = await this.connection(false);
118
+ if (database === undefined) return { status: "unavailable" };
119
+ try {
120
+ await database.run(
121
+ `INSERT INTO ${this.table} (broker_id, reason, version, created_at) VALUES (?, ?, ?, ${DATABASE_NOW_MS})`,
122
+ hint.brokerId,
123
+ hint.reason,
124
+ hint.version,
125
+ );
126
+ await database.run(
127
+ `DELETE FROM ${this.table} WHERE created_at < ${DATABASE_NOW_MS} - ?`,
128
+ WAKE_UP_HINT_RETENTION_MS,
129
+ );
130
+ } catch {
131
+ this.drop(database);
132
+ return { status: "unavailable" };
133
+ }
134
+ return { status: "published" };
135
+ }
136
+
137
+ async subscribe(brokerId: string, listener: WakeUpListener): Promise<WakeUpSubscribeResult> {
138
+ if (brokerId.length === 0) return { status: "invalid", reason: "empty-broker-id" };
139
+ if (this.closed) return { status: "unavailable" };
140
+ const subscription: DatabaseSubscription = { brokerId, listener, open: true };
141
+ this.subscriptions.add(subscription);
142
+ this.deliver(subscription, { kind: "resync", cause: "subscribed" });
143
+ this.armPoll();
144
+ const handle: WakeUpSubscription = {
145
+ brokerId,
146
+ close: async () => {
147
+ subscription.open = false;
148
+ this.subscriptions.delete(subscription);
149
+ },
150
+ };
151
+ return { status: "subscribed", subscription: handle };
152
+ }
153
+
154
+ close(): Promise<void> {
155
+ this.closing ??= this.shutdown();
156
+ return this.closing;
157
+ }
158
+
159
+ private async shutdown(): Promise<void> {
160
+ this.closed = true;
161
+ this.cancelPoll?.();
162
+ this.cancelPoll = undefined;
163
+ for (const subscription of this.subscriptions) subscription.open = false;
164
+ this.subscriptions.clear();
165
+ await this.polling;
166
+ await this.opening;
167
+ const database = this.database;
168
+ this.database = undefined;
169
+ await database?.close().catch(() => {});
170
+ }
171
+
172
+ private deliver(subscription: DatabaseSubscription, event: WakeUpEvent): void {
173
+ queueMicrotask(() => {
174
+ if (subscription.open) subscription.listener(event);
175
+ });
176
+ }
177
+
178
+ private armPoll(): void {
179
+ if (this.closed || this.cancelPoll !== undefined || this.polling !== undefined) return;
180
+ if (this.subscriptions.size === 0) return;
181
+ this.cancelPoll = this.schedule(() => {
182
+ this.cancelPoll = undefined;
183
+ this.polling = this.poll().finally(() => {
184
+ this.polling = undefined;
185
+ this.armPoll();
186
+ });
187
+ }, this.nextPollDelayMs());
188
+ }
189
+
190
+ private nextPollDelayMs(): number {
191
+ if (this.database !== undefined || this.reconnectFailures === 0) return this.pollIntervalMs;
192
+ return Math.min(this.pollIntervalMs * 2 ** this.reconnectFailures, WAKE_UP_RECONNECT_MAX_DELAY_MS);
193
+ }
194
+
195
+ private async poll(): Promise<void> {
196
+ const database = await this.connection(true);
197
+ const brokerIds = [...new Set([...this.subscriptions].map((subscription) => subscription.brokerId))];
198
+ if (database === undefined || brokerIds.length === 0) return;
199
+ const placeholders = brokerIds.map(() => "?").join(", ");
200
+ for (;;) {
201
+ let rows: HintRow[];
202
+ try {
203
+ rows = await database.all<HintRow>(
204
+ `SELECT id, broker_id, reason, version FROM ${this.table} WHERE broker_id IN (${placeholders}) ORDER BY id LIMIT ${WAKE_UP_POLL_BATCH_SIZE}`,
205
+ ...brokerIds,
206
+ );
207
+ if (rows.length > 0) {
208
+ await database.run(
209
+ `DELETE FROM ${this.table} WHERE id IN (${rows.map(() => "?").join(", ")})`,
210
+ ...rows.map((row) => row.id),
211
+ );
212
+ }
213
+ } catch {
214
+ this.drop(database);
215
+ return;
216
+ }
217
+ for (const row of rows) {
218
+ const hint: WakeUpHint = {
219
+ brokerId: row.broker_id,
220
+ reason: row.reason === "outcome" ? "outcome" : "outbox",
221
+ version: row.version,
222
+ };
223
+ for (const subscription of this.subscriptions) {
224
+ if (subscription.brokerId === hint.brokerId) this.deliver(subscription, { kind: "hint", hint });
225
+ }
226
+ }
227
+ if (rows.length < WAKE_UP_POLL_BATCH_SIZE || this.closed) return;
228
+ }
229
+ }
230
+
231
+ /** `force` (the poll) reopens even while backing off; a publish waits for the backoff to pass. */
232
+ private connection(force: boolean): Promise<SqliteDatabase | undefined> {
233
+ if (this.closed) return Promise.resolve(undefined);
234
+ if (this.database !== undefined) return Promise.resolve(this.database);
235
+ if (!force && this.opening === undefined && Date.now() < this.reconnectNotBefore) return Promise.resolve(undefined);
236
+ this.opening ??= this.reconnect().finally(() => {
237
+ this.opening = undefined;
238
+ });
239
+ return this.opening;
240
+ }
241
+
242
+ private async reconnect(): Promise<SqliteDatabase | undefined> {
243
+ let database: SqliteDatabase;
244
+ try {
245
+ database = await openSchemaConnection(this.options, this.table);
246
+ } catch {
247
+ this.reconnectFailures++;
248
+ this.reconnectNotBefore = Date.now() + this.nextPollDelayMs();
249
+ return undefined;
250
+ }
251
+ this.reconnectFailures = 0;
252
+ if (this.closed) {
253
+ await database.close().catch(() => {});
254
+ return undefined;
255
+ }
256
+ this.database = database;
257
+ for (const subscription of this.subscriptions) {
258
+ this.deliver(subscription, { kind: "resync", cause: "reconnected" });
259
+ }
260
+ return database;
261
+ }
262
+
263
+ private drop(database: SqliteDatabase): void {
264
+ if (this.database !== database) return;
265
+ this.database = undefined;
266
+ void database.close().catch(() => {});
267
+ }
268
+ }
269
+
270
+ /** Opens one broker's connection to the database wake-up bus; a failure to open the database is thrown. */
271
+ export async function openDatabaseWakeUpBus(options: DatabaseWakeUpBusOptions): Promise<WakeUpBus> {
272
+ const table = `${validateStoragePrefix(options.prefix)}intercom_wakeups`;
273
+ return new DatabaseWakeUpBus(options, table, await openSchemaConnection(options, table));
274
+ }
@@ -26,7 +26,7 @@ export function sessionsShareGroup(a: SessionInfo, b: SessionInfo): boolean {
26
26
 
27
27
  /** Return each session visible through at least one of the requester's memberships. */
28
28
  export function sessionsVisibleTo(
29
- sessions: ReadonlyMap<string, BrokerConnectedSession>,
29
+ sessions: ReadonlyMap<string, Pick<BrokerConnectedSession, "info">>,
30
30
  requester: SessionInfo,
31
31
  ): SessionInfo[] {
32
32
  return [...sessions.values()].map(({ info }) => info).filter((info) => isAgentRecipient(info) && sessionsShareGroup(requester, info));
@@ -34,7 +34,7 @@ export function sessionsVisibleTo(
34
34
 
35
35
  /** Return sessions that belong to one explicit group. */
36
36
  export function sessionsInGroup(
37
- sessions: ReadonlyMap<string, BrokerConnectedSession>,
37
+ sessions: ReadonlyMap<string, Pick<BrokerConnectedSession, "info">>,
38
38
  group: string | undefined,
39
39
  ): SessionInfo[] {
40
40
  return [...sessions.values()].map(({ info }) => info).filter((info) => isAgentRecipient(info) && hasGroup(sessionGroups(info), group));