@bastani/atomic 0.9.39 → 0.9.40-alpha.2

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 (280) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +14 -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 +31 -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/CHANGELOG.md +6 -0
  48. package/dist/builtin/web-access/README.md +1 -1
  49. package/dist/builtin/web-access/index.bundle.mjs +2 -5
  50. package/dist/builtin/web-access/package.json +1 -1
  51. package/dist/builtin/workflows/CHANGELOG.md +24 -0
  52. package/dist/builtin/workflows/README.md +1 -1
  53. package/dist/builtin/workflows/package.json +1 -1
  54. package/dist/builtin/workflows/skills/impeccable/SKILL.md +8 -8
  55. package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +3 -3
  56. package/dist/builtin/workflows/skills/impeccable/reference/adapt.native.md +1 -1
  57. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +4 -2
  58. package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
  59. package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +1 -1
  60. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +22 -9
  61. package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +2 -2
  62. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +3 -3
  63. package/dist/builtin/workflows/skills/impeccable/reference/extract.md +3 -3
  64. package/dist/builtin/workflows/skills/impeccable/reference/harden.md +2 -4
  65. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +4 -4
  66. package/dist/builtin/workflows/skills/impeccable/reference/live.md +1 -1
  67. package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +6 -6
  68. package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +3 -3
  69. package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +3 -3
  70. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +3 -3
  71. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
  72. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +10 -0
  73. package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +1 -1
  74. package/dist/builtin/workflows/skills/impeccable/scripts/VERSION +1 -1
  75. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable +6 -0
  76. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable.cmd +47 -22
  77. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +34 -1
  78. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-session.js +25 -1
  79. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +33 -8
  80. package/dist/builtin/workflows/src/durable/journal/resolution.d.ts +7 -7
  81. package/dist/builtin/workflows/src/extension/index.bundle.mjs +22715 -22159
  82. package/dist/builtin/workflows/src/index.js +106 -35
  83. package/dist/cli/args.d.ts +2 -0
  84. package/dist/cli/args.d.ts.map +1 -1
  85. package/dist/cli/args.js +17 -1
  86. package/dist/cli/args.js.map +1 -1
  87. package/dist/core/agent-session-extension-bindings.d.ts +2 -0
  88. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  89. package/dist/core/agent-session-extension-bindings.js +6 -6
  90. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  91. package/dist/core/agent-session-methods.d.ts +6 -3
  92. package/dist/core/agent-session-methods.d.ts.map +1 -1
  93. package/dist/core/agent-session-methods.js.map +1 -1
  94. package/dist/core/agent-session-persistent-custom-messages.d.ts +7 -2
  95. package/dist/core/agent-session-persistent-custom-messages.d.ts.map +1 -1
  96. package/dist/core/agent-session-persistent-custom-messages.js +29 -12
  97. package/dist/core/agent-session-persistent-custom-messages.js.map +1 -1
  98. package/dist/core/agent-session-recovery.d.ts +14 -0
  99. package/dist/core/agent-session-recovery.d.ts.map +1 -0
  100. package/dist/core/agent-session-recovery.js +83 -0
  101. package/dist/core/agent-session-recovery.js.map +1 -0
  102. package/dist/core/agent-session-tasks.d.ts.map +1 -1
  103. package/dist/core/agent-session-tasks.js +4 -6
  104. package/dist/core/agent-session-tasks.js.map +1 -1
  105. package/dist/core/agent-session-types.d.ts +13 -0
  106. package/dist/core/agent-session-types.d.ts.map +1 -1
  107. package/dist/core/agent-session-types.js.map +1 -1
  108. package/dist/core/agent-session.d.ts +2 -1
  109. package/dist/core/agent-session.d.ts.map +1 -1
  110. package/dist/core/agent-session.js +4 -1
  111. package/dist/core/agent-session.js.map +1 -1
  112. package/dist/core/export-html/index.d.ts.map +1 -1
  113. package/dist/core/export-html/index.js +3 -2
  114. package/dist/core/export-html/index.js.map +1 -1
  115. package/dist/core/export-html/template.html +1 -0
  116. package/dist/core/export-html/template.js +37 -20
  117. package/dist/core/export-html/vendor/marked-cjk-friendly.min.js +6 -0
  118. package/dist/core/export-html/vendor/marked.min.js +76 -3
  119. package/dist/core/extensions/api-types.d.ts +6 -2
  120. package/dist/core/extensions/api-types.d.ts.map +1 -1
  121. package/dist/core/extensions/api-types.js.map +1 -1
  122. package/dist/core/extensions/event-results.d.ts +21 -0
  123. package/dist/core/extensions/event-results.d.ts.map +1 -1
  124. package/dist/core/extensions/event-results.js.map +1 -1
  125. package/dist/core/extensions/index.d.ts +1 -1
  126. package/dist/core/extensions/index.d.ts.map +1 -1
  127. package/dist/core/extensions/index.js.map +1 -1
  128. package/dist/core/extensions/loader-api.d.ts.map +1 -1
  129. package/dist/core/extensions/loader-api.js +4 -0
  130. package/dist/core/extensions/loader-api.js.map +1 -1
  131. package/dist/core/extensions/runner-events.d.ts +9 -2
  132. package/dist/core/extensions/runner-events.d.ts.map +1 -1
  133. package/dist/core/extensions/runner-events.js +29 -0
  134. package/dist/core/extensions/runner-events.js.map +1 -1
  135. package/dist/core/extensions/runner.d.ts +5 -3
  136. package/dist/core/extensions/runner.d.ts.map +1 -1
  137. package/dist/core/extensions/runner.js +7 -2
  138. package/dist/core/extensions/runner.js.map +1 -1
  139. package/dist/core/extensions/runtime-types.d.ts +1 -0
  140. package/dist/core/extensions/runtime-types.d.ts.map +1 -1
  141. package/dist/core/extensions/runtime-types.js.map +1 -1
  142. package/dist/core/extensions/session-events.d.ts +30 -2
  143. package/dist/core/extensions/session-events.d.ts.map +1 -1
  144. package/dist/core/extensions/session-events.js.map +1 -1
  145. package/dist/core/sdk-types.d.ts +3 -2
  146. package/dist/core/sdk-types.d.ts.map +1 -1
  147. package/dist/core/sdk-types.js.map +1 -1
  148. package/dist/core/sdk.d.ts.map +1 -1
  149. package/dist/core/sdk.js +3 -0
  150. package/dist/core/sdk.js.map +1 -1
  151. package/dist/core/session-storage.d.ts.map +1 -1
  152. package/dist/core/session-storage.js +15 -9
  153. package/dist/core/session-storage.js.map +1 -1
  154. package/dist/core/storage/process-selection.d.ts +39 -0
  155. package/dist/core/storage/process-selection.d.ts.map +1 -0
  156. package/dist/core/storage/process-selection.js +154 -0
  157. package/dist/core/storage/process-selection.js.map +1 -0
  158. package/dist/core/storage/provider.d.ts +6 -1
  159. package/dist/core/storage/provider.d.ts.map +1 -1
  160. package/dist/core/storage/provider.js +3 -2
  161. package/dist/core/storage/provider.js.map +1 -1
  162. package/dist/core/storage/selection.d.ts +1 -1
  163. package/dist/core/storage/selection.d.ts.map +1 -1
  164. package/dist/core/storage/selection.js.map +1 -1
  165. package/dist/core/storage/session/jsonl-session-store.d.ts +2 -0
  166. package/dist/core/storage/session/jsonl-session-store.d.ts.map +1 -1
  167. package/dist/core/storage/session/jsonl-session-store.js +38 -2
  168. package/dist/core/storage/session/jsonl-session-store.js.map +1 -1
  169. package/dist/core/tasks/completion.d.ts +11 -2
  170. package/dist/core/tasks/completion.d.ts.map +1 -1
  171. package/dist/core/tasks/completion.js +40 -16
  172. package/dist/core/tasks/completion.js.map +1 -1
  173. package/dist/core/tasks/pause.d.ts +4 -1
  174. package/dist/core/tasks/pause.d.ts.map +1 -1
  175. package/dist/core/tasks/pause.js +8 -5
  176. package/dist/core/tasks/pause.js.map +1 -1
  177. package/dist/index-extensions.d.ts +1 -1
  178. package/dist/index-extensions.d.ts.map +1 -1
  179. package/dist/index-extensions.js.map +1 -1
  180. package/dist/index.d.ts +2 -1
  181. package/dist/index.d.ts.map +1 -1
  182. package/dist/index.js +2 -0
  183. package/dist/index.js.map +1 -1
  184. package/dist/main.d.ts.map +1 -1
  185. package/dist/main.js +7 -1
  186. package/dist/main.js.map +1 -1
  187. package/dist/modes/interactive/components/chat-session-host-rendering.d.ts.map +1 -1
  188. package/dist/modes/interactive/components/chat-session-host-rendering.js +11 -1
  189. package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
  190. package/dist/modes/interactive/components/chat-session-host-state.d.ts +2 -0
  191. package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
  192. package/dist/modes/interactive/components/chat-session-host-state.js +2 -0
  193. package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
  194. package/dist/modes/interactive/interactive-deferred-startup.d.ts.map +1 -1
  195. package/dist/modes/interactive/interactive-deferred-startup.js +4 -0
  196. package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
  197. package/dist/modes/interactive/interactive-mode-surface.d.ts +2 -1
  198. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  199. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  200. package/dist/modes/interactive/interactive-render-chat.js +3 -0
  201. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  202. package/dist/modes/interactive/interactive-session-routing.js +2 -0
  203. package/dist/modes/interactive/interactive-session-routing.js.map +1 -1
  204. package/dist/modes/interactive/interactive-session-runtime.d.ts +2 -1
  205. package/dist/modes/interactive/interactive-session-runtime.d.ts.map +1 -1
  206. package/dist/modes/interactive/interactive-session-runtime.js +29 -0
  207. package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
  208. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  209. package/dist/modes/interactive/interactive-startup.js +4 -1
  210. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  211. package/dist/modes/interactive-engine/engine-args.d.ts.map +1 -1
  212. package/dist/modes/interactive-engine/engine-args.js +4 -0
  213. package/dist/modes/interactive-engine/engine-args.js.map +1 -1
  214. package/dist/modes/interactive-engine/engine-render-service.d.ts.map +1 -1
  215. package/dist/modes/interactive-engine/engine-render-service.js +4 -4
  216. package/dist/modes/interactive-engine/engine-render-service.js.map +1 -1
  217. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  218. package/dist/modes/interactive-engine/isolated-runtime.js +4 -0
  219. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  220. package/dist/modes/interactive-engine/remote-renderer.js +1 -1
  221. package/dist/modes/interactive-engine/remote-renderer.js.map +1 -1
  222. package/dist/modes/rpc/rpc-client-api.d.ts +4 -1
  223. package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
  224. package/dist/modes/rpc/rpc-client-api.js +4 -0
  225. package/dist/modes/rpc/rpc-client-api.js.map +1 -1
  226. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  227. package/dist/modes/rpc/rpc-command-handler.js +3 -0
  228. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  229. package/dist/modes/rpc/rpc-command-timeouts.d.ts.map +1 -1
  230. package/dist/modes/rpc/rpc-command-timeouts.js +1 -0
  231. package/dist/modes/rpc/rpc-command-timeouts.js.map +1 -1
  232. package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
  233. package/dist/modes/rpc/rpc-input-scheduler.js +6 -1
  234. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  235. package/dist/modes/rpc/rpc-types.d.ts +12 -1
  236. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  237. package/dist/modes/rpc/rpc-types.js.map +1 -1
  238. package/dist/utils/shell.d.ts.map +1 -1
  239. package/dist/utils/shell.js +44 -11
  240. package/dist/utils/shell.js.map +1 -1
  241. package/dist/utils/tools-manager.js +1 -1
  242. package/dist/utils/tools-manager.js.map +1 -1
  243. package/docs/background-tasks.md +5 -3
  244. package/docs/computer-use.md +2 -2
  245. package/docs/environment-variables.md +4 -1
  246. package/docs/extensions/events.md +29 -2
  247. package/docs/getting-started/first-session.md +1 -1
  248. package/docs/guides/intercom.md +1 -1
  249. package/docs/intercom/operations.md +103 -1
  250. package/docs/intercom/reference.md +10 -3
  251. package/docs/intercom.md +8 -6
  252. package/docs/keybindings.md +1 -1
  253. package/docs/models.md +1 -1
  254. package/docs/providers.md +5 -3
  255. package/docs/reference/cli.md +5 -0
  256. package/docs/rpc/protocol.md +19 -0
  257. package/docs/rpc.md +3 -0
  258. package/docs/sdk/web-server.md +1 -1
  259. package/docs/sdk.md +5 -2
  260. package/docs/sessions.md +17 -1
  261. package/docs/settings.md +6 -4
  262. package/docs/skills.md +1 -1
  263. package/docs/storage.md +54 -3
  264. package/docs/subagents.md +49 -9
  265. package/docs/usage.md +1 -1
  266. package/docs/windows.md +14 -4
  267. package/docs/workflows/api-reference.md +1 -1
  268. package/docs/workflows/authoring.md +1 -1
  269. package/docs/workflows/operations.md +10 -6
  270. package/npm-shrinkwrap.json +40 -40
  271. package/package.json +6 -6
  272. package/dist/core/export-html/template-js/data-tree.js +0 -287
  273. package/dist/core/export-html/template-js/entries-navigation.js +0 -434
  274. package/dist/core/export-html/template-js/initialization.js +0 -335
  275. package/dist/core/export-html/template-js/message-tools.js +0 -428
  276. package/dist/core/export-html/template-js/tree-filter-render.js +0 -419
  277. package/dist/core/export-html/template-script.d.ts +0 -3
  278. package/dist/core/export-html/template-script.d.ts.map +0 -1
  279. package/dist/core/export-html/template-script.js +0 -13
  280. package/dist/core/export-html/template-script.js.map +0 -1
@@ -3,9 +3,18 @@
3
3
  import "./bounded-stderr-install.js";
4
4
  import net from "net";
5
5
  import { chmodSync, writeFileSync, unlinkSync, mkdirSync, readFileSync, statSync } from "fs";
6
+ import { dirname } from "path";
6
7
  import { randomUUID } from "crypto";
7
8
  import { createMessageReader, type JsonWireValue } from "./framing.js";
8
- import { writeMessageIfOpen, writeMessageWithOutcome } from "./socket-writes.js";
9
+ import {
10
+ discardHeldWrites,
11
+ flushHeldWrites,
12
+ type HeldWrite,
13
+ holdWrites,
14
+ releaseHeldWrites,
15
+ writeMessageIfOpen,
16
+ writeMessageWithOutcome,
17
+ } from "./socket-writes.js";
9
18
  import { isSocketAnswering } from "./socket-liveness.js";
10
19
  import { acquireSocketReplacementLock } from "./socket-replacement-lock.js";
11
20
  import {
@@ -39,9 +48,24 @@ import {
39
48
  handleBrokerSend,
40
49
  type BrokerConnectedSession,
41
50
  type PendingStageRoute,
51
+ type RemoteSendRequest,
52
+ type RemoteSendRouter,
53
+ type RestoringRecipientHold,
54
+ type SendClientMessage,
42
55
  senderGroupIdentity,
43
56
  } from "./send-handler.js";
57
+ import {
58
+ type BrokerStateOpener,
59
+ LOCAL_RETRY_LOOKUP_BOUND_MS,
60
+ MultiNodeRouter,
61
+ readSharedStorageSetting,
62
+ resolveMultiNodeStartup,
63
+ SHARED_STORAGE_UNAVAILABLE_REASON,
64
+ } from "./multinode-router.js";
65
+ import { OUTBOX_BACKUP_POLL_INTERVAL_MS } from "./multinode-state-store.js";
44
66
  import { SupervisorChannelCache } from "./supervisor-channel.js";
67
+ import { SessionIdReclaim } from "./session-id-reclaim.js";
68
+ import { type HeldSend, RestoringRecipients } from "./restoring-recipients.js";
45
69
  import { hasGroup, normalizeGroup, normalizeGroups } from "../group.js";
46
70
  import { handleBrokerPresence } from "./presence-handler.js";
47
71
  import {
@@ -52,6 +76,7 @@ import {
52
76
  setSessionGroups,
53
77
  } from "./group-membership.js";
54
78
  import { PendingQuestionIndex } from "./pending-question-index.js";
79
+ import { type ReceiptSender, RecipientReceipts, UNCERTAIN_RECEIPT_REASON } from "./recipient-receipts.js";
55
80
  import { matchStagePathSegments } from "../workflow-stage-path-matching.js";
56
81
  import { DELIVERED_MESSAGE_TTL_MS } from "../retry-policy.js";
57
82
  import { isAgentRecipient } from "../recipient-purpose.js";
@@ -69,6 +94,7 @@ const PENDING_STAGE_MESSAGE_TIMEOUT_MS = 10_000;
69
94
  const LISTEN_ATTEMPTS = 5;
70
95
  const LISTEN_RETRY_DELAY_MS = 100;
71
96
  const SOCKET_OWNERSHIP_CHECK_MS = 1_000;
97
+ const MULTI_NODE_CLOSE_BOUND_MS = 2_000;
72
98
 
73
99
  function exitBecauseLiveBrokerOwnsSocket(): never {
74
100
  console.error(`Intercom broker not started: a live broker already answers on ${SOCKET_PATH}`);
@@ -79,6 +105,8 @@ interface PendingStageRouteRegistration {
79
105
  readonly sessionId: string;
80
106
  readonly group: string;
81
107
  readonly capability: string;
108
+ /** Registered by a restarted owner for an interrupted run; yields to the process that resumes it (#3760). */
109
+ readonly dormant?: true;
82
110
  }
83
111
 
84
112
  interface WorkflowRosterRegistration {
@@ -173,6 +201,22 @@ function isSupervisorRegistration(value: unknown): value is SupervisorRegistrati
173
201
  && typeof registration.supervisorSessionId === "string";
174
202
  }
175
203
 
204
+ const RESTORING_RECIPIENT_ALIASES_MAX = 4;
205
+ const RESTORING_RECIPIENT_GONE_REASON_MAX = 1_000;
206
+ const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
207
+
208
+ function isRestoringRecipientAliasList(value: unknown): value is string[] {
209
+ return Array.isArray(value) && value.length <= RESTORING_RECIPIENT_ALIASES_MAX && value.every((alias) => typeof alias === "string");
210
+ }
211
+
212
+ /**
213
+ * A restoring subagent's extra targets are ids only: its Atomic session id and its pre-restart Intercom id (#3760).
214
+ * Its name comes from the supervisor capability, so no reservation can claim another child's name.
215
+ */
216
+ function isRestoringRecipientAlias(alias: string): boolean {
217
+ return SESSION_ID_PATTERN.test(alias.trim());
218
+ }
219
+
176
220
  function invocationOwnsGroup(invocationGroup: string, candidateGroup: string): boolean {
177
221
  const owner = normalizeGroup(invocationGroup);
178
222
  const candidate = normalizeGroup(candidateGroup);
@@ -269,13 +313,24 @@ class IntercomBroker {
269
313
  getBrokerDeliveredMessagesPath(),
270
314
  );
271
315
  private supervisorChannel = new SupervisorChannelCache();
316
+ private sessionIdReclaim = new SessionIdReclaim();
272
317
  private pendingQuestions = new PendingQuestionIndex();
318
+ private receipts = new RecipientReceipts<ConnectedSession>();
319
+ private restoringRecipients = new RestoringRecipients((held, reason) => this.failHeldSend(held, reason));
273
320
  private pendingStageRoutes = new Map<string, PendingStageRouteRegistration>();
274
321
  private pendingStageAcknowledgments = new Map<string, PendingStageAcknowledgment>();
275
322
  private pendingStageNotificationAcknowledgments = new Map<string, PendingStageNotificationAcknowledgment>();
276
323
  private liveWorkflowStageRoutes = new Map<string, LiveWorkflowStageRouteRegistration>();
277
324
  private workflowRosters = new Map<string, WorkflowRosterRegistration>();
278
325
  private liveWorkflowStageRouteActivations = new Map<string, LiveWorkflowStageRouteActivation>();
326
+ /** Multi-node S5: set only when the agent directory's settings select a top-level shared `storage`. */
327
+ private readonly sharedStorageSetting = readSharedStorageSetting(dirname(INTERCOM_DIR));
328
+ private readonly routeRemote: RemoteSendRouter | undefined =
329
+ this.sharedStorageSetting === undefined ? undefined : (request) => this.routeRemoteSend(request);
330
+ private multiNode: MultiNodeRouter | undefined;
331
+ private multiNodeStatus: "starting" | "on" | "off" | "unavailable" = "off";
332
+ private multiNodeFirstAttempt: Promise<void> = Promise.resolve();
333
+ private shuttingDown = false;
279
334
 
280
335
  constructor() {
281
336
  mkdirSync(INTERCOM_DIR, { recursive: true, mode: 0o700 });
@@ -285,6 +340,7 @@ class IntercomBroker {
285
340
 
286
341
  start(): void {
287
342
  this.listen(1);
343
+ if (this.sharedStorageSetting !== undefined) this.startMultiNode(this.sharedStorageSetting);
288
344
  process.on("SIGTERM", () => this.shutdown());
289
345
  process.on("SIGINT", () => this.shutdown());
290
346
  }
@@ -957,7 +1013,9 @@ class IntercomBroker {
957
1013
  pending.signature,
958
1014
  targetIdentity,
959
1015
  );
960
- if (deliveredMatch !== "miss") {
1016
+ // #3761: a recipient that confirms saves ignores a copy it already saved, so a reserved message is sent again.
1017
+ const confirmsSaves = target.acknowledgesReceipt === true;
1018
+ if (deliveredMatch !== "miss" && !(deliveredMatch === "uncertain" && confirmsSaves)) {
961
1019
  if (deliveredMatch === "match") {
962
1020
  if (pending.message.expectsReply === true) {
963
1021
  const acceptedQuestionTarget = this.deliveredMessages.lookupQuestionTarget(
@@ -1041,7 +1099,7 @@ class IntercomBroker {
1041
1099
  });
1042
1100
  return;
1043
1101
  }
1044
- if (reservation !== "recorded") {
1102
+ if (reservation !== "recorded" && !(reservation === "uncertain" && confirmsSaves)) {
1045
1103
  writeMessageIfOpen(pending.senderSocket, {
1046
1104
  type: "delivery_failed",
1047
1105
  messageId: pending.messageId,
@@ -1058,49 +1116,81 @@ class IntercomBroker {
1058
1116
  }
1059
1117
  if (pending.message.expectsReply === true && pending.resolveReplyTarget === true && pending.attemptId !== undefined)
1060
1118
  writeMessageIfOpen(pending.senderSocket, { type: "question_target", messageId: pending.messageId, attemptId: pending.attemptId, sessionId: target.info.id });
1061
- if (!writeMessageIfOpen(target.socket, { type: "message", from: pending.sender, message: pending.message })) {
1062
- this.deliveredMessages.forget(pending.messageId, pending.signature);
1063
- writeMessageIfOpen(pending.senderSocket, {
1064
- type: "delivery_failed",
1065
- messageId: pending.messageId,
1066
- ...(pending.attemptId ? { attemptId: pending.attemptId } : {}),
1067
- reason: "Session not found",
1068
- });
1069
- return;
1070
- }
1071
- let accepted: ReturnType<DeliveredMessageCache["accept"]>;
1072
- try {
1073
- accepted = this.deliveredMessages.accept(pending.messageId, pending.signature, targetIdentity);
1074
- } catch {
1075
- writeMessageIfOpen(pending.senderSocket, {
1076
- type: "delivery_failed",
1077
- messageId: pending.messageId,
1078
- ...(pending.attemptId ? { attemptId: pending.attemptId } : {}),
1079
- reason: "Intercom could not durably confirm the forwarded operation; refusing a possibly unsafe retry",
1080
- });
1081
- return;
1119
+ const frame: BrokerMessage = { type: "message", from: pending.sender, message: pending.message };
1120
+ const signature = pending.signature;
1121
+ const finish = (recipient: ConnectedSession, sender: ReceiptSender | undefined): void => {
1122
+ const notify = (message: BrokerMessage): void => {
1123
+ if (sender !== undefined) writeMessageIfOpen(sender.socket, message);
1124
+ };
1125
+ const senderAttempt = sender?.attemptId === undefined ? {} : { attemptId: sender.attemptId };
1126
+ let accepted: ReturnType<DeliveredMessageCache["accept"]>;
1127
+ try {
1128
+ accepted = this.deliveredMessages.accept(pending.messageId, signature, targetIdentity);
1129
+ } catch {
1130
+ notify({
1131
+ type: "delivery_failed",
1132
+ messageId: pending.messageId,
1133
+ ...senderAttempt,
1134
+ reason: "Intercom could not durably confirm the forwarded operation; refusing a possibly unsafe retry",
1135
+ });
1136
+ return;
1137
+ }
1138
+ if (accepted !== "accepted") {
1139
+ notify({
1140
+ type: "delivery_failed",
1141
+ messageId: pending.messageId,
1142
+ ...senderAttempt,
1143
+ reason: "Intercom could not durably confirm the forwarded operation; refusing a possibly unsafe retry",
1144
+ ...(accepted === "conflict" ? { reasonCode: "message_id_conflict" as const } : {}),
1145
+ });
1146
+ return;
1147
+ }
1148
+ if (pending.message.expectsReply === true && sender !== undefined) {
1149
+ this.pendingQuestions.record(from.info.id, recipient.info.id, pending.messageId);
1150
+ // #3761: a recipient that reconnected before confirming answers under its new session id.
1151
+ if (recipient.info.id !== target.info.id && pending.resolveReplyTarget === true && sender.attemptId !== undefined)
1152
+ notify({ type: "question_target", messageId: pending.messageId, attemptId: sender.attemptId, sessionId: recipient.info.id });
1153
+ }
1154
+ if (pending.message.replyTo !== undefined && this.pendingQuestions.clearReply(from.info.id, recipient.info.id, pending.message.replyTo)) {
1155
+ this.deliveredMessages.settleQuestion(pending.message.replyTo);
1156
+ }
1157
+ notify({ type: "delivered", messageId: pending.messageId, ...senderAttempt });
1158
+ };
1159
+ const sender: ReceiptSender = {
1160
+ socket: pending.senderSocket,
1161
+ ...(pending.attemptId ? { attemptId: pending.attemptId } : {}),
1162
+ };
1163
+ if (confirmsSaves) {
1164
+ this.receipts.await(
1165
+ {
1166
+ messageId: pending.messageId,
1167
+ targetIdentity,
1168
+ frame,
1169
+ complete: finish,
1170
+ expire: (waiting) => {
1171
+ writeMessageIfOpen(waiting.socket, {
1172
+ type: "delivery_failed",
1173
+ messageId: pending.messageId,
1174
+ ...(waiting.attemptId === undefined ? {} : { attemptId: waiting.attemptId }),
1175
+ reason: UNCERTAIN_RECEIPT_REASON,
1176
+ });
1177
+ },
1178
+ },
1179
+ sender,
1180
+ );
1082
1181
  }
1083
- if (accepted !== "accepted") {
1182
+ if (!writeMessageIfOpen(target.socket, frame)) {
1183
+ if (confirmsSaves) this.receipts.drop(pending.messageId);
1184
+ if (reservation === "recorded") this.deliveredMessages.forget(pending.messageId, pending.signature);
1084
1185
  writeMessageIfOpen(pending.senderSocket, {
1085
1186
  type: "delivery_failed",
1086
1187
  messageId: pending.messageId,
1087
1188
  ...(pending.attemptId ? { attemptId: pending.attemptId } : {}),
1088
- reason: "Intercom could not durably confirm the forwarded operation; refusing a possibly unsafe retry",
1089
- ...(accepted === "conflict" ? { reasonCode: "message_id_conflict" as const } : {}),
1189
+ reason: "Session not found",
1090
1190
  });
1091
1191
  return;
1092
1192
  }
1093
- if (pending.message.expectsReply === true) {
1094
- this.pendingQuestions.record(from.info.id, target.info.id, pending.messageId);
1095
- }
1096
- if (pending.message.replyTo !== undefined && this.pendingQuestions.clearReply(from.info.id, target.info.id, pending.message.replyTo)) {
1097
- this.deliveredMessages.settleQuestion(pending.message.replyTo);
1098
- }
1099
- writeMessageIfOpen(pending.senderSocket, {
1100
- type: "delivered",
1101
- messageId: pending.messageId,
1102
- ...(pending.attemptId ? { attemptId: pending.attemptId } : {}),
1103
- });
1193
+ if (!confirmsSaves) finish(target, sender);
1104
1194
  }
1105
1195
 
1106
1196
 
@@ -1405,6 +1495,12 @@ class IntercomBroker {
1405
1495
  && (typeof clientMessage.supervisorOwnerToken !== "string" || !clientMessage.supervisorOwnerToken)) {
1406
1496
  throw new Error("Invalid supervisor owner token");
1407
1497
  }
1498
+ if (clientMessage.preferredSessionId !== undefined && typeof clientMessage.preferredSessionId !== "string") {
1499
+ throw new Error("Invalid preferred session id");
1500
+ }
1501
+ if (clientMessage.acknowledgesReceipt !== undefined && clientMessage.acknowledgesReceipt !== true) {
1502
+ throw new Error("Invalid receipt acknowledgement capability");
1503
+ }
1408
1504
 
1409
1505
  if (currentId) {
1410
1506
  throw new Error("Received duplicate register message");
@@ -1424,7 +1520,12 @@ class IntercomBroker {
1424
1520
  supervisorId = claimedSupervisorId;
1425
1521
  }
1426
1522
 
1427
- const id = randomUUID();
1523
+ // #3760: a restored or reconnected session keeps the id peers know when it is free; never a live one.
1524
+ const id = this.sessionIdReclaim.grant(
1525
+ clientMessage.preferredSessionId,
1526
+ clientMessage.returnAddress,
1527
+ (sessionId) => this.sessions.has(sessionId),
1528
+ ) ?? randomUUID();
1428
1529
  setId(id);
1429
1530
  const initialGroups = normalizeGroups(clientMessage.session.groups, clientMessage.session.group);
1430
1531
  const legacyGroup = normalizeGroup(clientMessage.session.group ?? initialGroups.values().next().value);
@@ -1448,6 +1549,7 @@ class IntercomBroker {
1448
1549
  ...(typeof clientMessage.supervisorOwnerToken === "string"
1449
1550
  ? { supervisorOwnerToken: clientMessage.supervisorOwnerToken }
1450
1551
  : {}),
1552
+ ...(clientMessage.acknowledgesReceipt === true ? { acknowledgesReceipt: true as const } : {}),
1451
1553
  });
1452
1554
 
1453
1555
  if (this.shutdownTimer) {
@@ -1455,10 +1557,63 @@ class IntercomBroker {
1455
1557
  this.shutdownTimer = null;
1456
1558
  }
1457
1559
 
1458
- writeMessageIfOpen(socket, supervisorId
1459
- ? { type: "registered", sessionId: id, supervisorSessionId: supervisorId }
1460
- : { type: "registered", sessionId: id });
1461
- if (isAgentRecipient(info)) this.broadcastToMemberships({ type: "session_joined", session: info }, sessionGroups(info), id);
1560
+ const rejoined = this.sessions.get(id);
1561
+ const consumedCapability =
1562
+ supervisorId && isSupervisorRegistration(clientMessage.supervisor) ? clientMessage.supervisor.capability : undefined;
1563
+ const completeRegistration = (held: readonly HeldWrite[] = []): void => {
1564
+ // #3761: echoing the capability tells the client this broker understands `received`; an old broker never does.
1565
+ writeMessageIfOpen(socket, {
1566
+ type: "registered",
1567
+ sessionId: id,
1568
+ ...(supervisorId ? { supervisorSessionId: supervisorId } : {}),
1569
+ ...(clientMessage.acknowledgesReceipt === true ? { acknowledgesReceipt: true as const } : {}),
1570
+ });
1571
+ if (isAgentRecipient(info)) this.broadcastToMemberships({ type: "session_joined", session: info }, sessionGroups(info), id);
1572
+ // #3761: messages written to this recipient's previous connection that it never confirmed saving go out again.
1573
+ if (rejoined?.acknowledgesReceipt === true) {
1574
+ const heldFrames = new Set(held.map((write) => write.message));
1575
+ for (const frame of this.receipts.framesFor(deliveryTargetIdentity(rejoined))) {
1576
+ if (!heldFrames.has(frame)) writeMessageIfOpen(socket, frame);
1577
+ }
1578
+ }
1579
+ // Multi-node S5: frames written while registration waited for the presence lease follow `registered`.
1580
+ flushHeldWrites(socket, held);
1581
+ // #3760: a restored subagent is back under the capability its owner reserved it with; the sends held
1582
+ // for it while it was being restored go out now, once each and in the order they arrived.
1583
+ if (consumedCapability !== undefined) {
1584
+ const registered = info.name?.trim();
1585
+ for (const send of this.restoringRecipients.consume(
1586
+ consumedCapability,
1587
+ registered ? { childName: registered, groups: sessionGroups(info) } : undefined,
1588
+ ))
1589
+ this.dispatchSend(send.socket, send.clientMessage, send.senderId);
1590
+ }
1591
+ };
1592
+ // Multi-node S5: brokers on other hosts find this session through its presence lease, so registration
1593
+ // completes once the lease is written (or bounded attempts found it still held elsewhere). The session is
1594
+ // routable meanwhile, so its frames are held until `registered` is written.
1595
+ const presence =
1596
+ rejoined === undefined
1597
+ ? undefined
1598
+ : this.multiNode?.sessionRegistered(
1599
+ rejoined,
1600
+ consumedCapability === undefined
1601
+ ? undefined
1602
+ : {
1603
+ capability: consumedCapability,
1604
+ ...(info.name?.trim() ? { childName: info.name.trim() } : {}),
1605
+ groups: sessionGroups(info),
1606
+ },
1607
+ );
1608
+ if (presence === undefined) completeRegistration();
1609
+ else {
1610
+ holdWrites(socket);
1611
+ void presence.then(() => {
1612
+ const held = releaseHeldWrites(socket);
1613
+ if (this.sessions.get(id) === rejoined) completeRegistration(held);
1614
+ else discardHeldWrites(held);
1615
+ });
1616
+ }
1462
1617
  break;
1463
1618
  }
1464
1619
 
@@ -1479,19 +1634,27 @@ class IntercomBroker {
1479
1634
 
1480
1635
  const requester = currentId ? this.sessions.get(currentId) : undefined;
1481
1636
  if (requester === undefined) throw new Error("Session not found");
1482
- const sessions =
1483
- typeof clientMessage.group === "string"
1484
- ? this.canInspectSelectedGroup(requester, clientMessage.group)
1485
- ? sessionsInGroup(this.sessions, clientMessage.group)
1637
+ const requestId = clientMessage.requestId;
1638
+ const selectedGroup = clientMessage.group;
1639
+ const visible = (sessions: ReadonlyMap<string, Pick<ConnectedSession, "info">>): SessionInfo[] =>
1640
+ typeof selectedGroup === "string"
1641
+ ? this.canInspectSelectedGroup(requester, selectedGroup)
1642
+ ? sessionsInGroup(sessions, selectedGroup)
1486
1643
  : []
1487
- : sessionsVisibleTo(this.sessions, requester.info);
1488
- writeMessageIfOpen(socket, {
1489
- type: "sessions",
1490
- requestId: clientMessage.requestId,
1491
- sessions,
1492
- workflowStages: this.workflowStagesVisibleTo(requester, clientMessage.group),
1493
- workflowFutureStages: this.workflowFutureStagesVisibleTo(requester, clientMessage.group),
1494
- });
1644
+ : sessionsVisibleTo(sessions, requester.info);
1645
+ const respond = (sessions: SessionInfo[]): void => {
1646
+ writeMessageIfOpen(socket, {
1647
+ type: "sessions",
1648
+ requestId,
1649
+ sessions,
1650
+ workflowStages: this.workflowStagesVisibleTo(requester, selectedGroup),
1651
+ workflowFutureStages: this.workflowFutureStagesVisibleTo(requester, selectedGroup),
1652
+ });
1653
+ };
1654
+ const multiNode = this.multiNode;
1655
+ if (multiNode === undefined) respond(visible(this.sessions));
1656
+ // Multi-node S5: a session connected through another node is a live peer too, under the same visibility.
1657
+ else void multiNode.remoteSessions().then((remote) => respond([...visible(this.sessions), ...visible(remote)]));
1495
1658
  break;
1496
1659
  }
1497
1660
 
@@ -1531,6 +1694,78 @@ class IntercomBroker {
1531
1694
  break;
1532
1695
  }
1533
1696
 
1697
+ case "reserve_restoring_recipient": {
1698
+ const owner = this.sessions.get(currentId);
1699
+ if (
1700
+ owner === undefined || typeof clientMessage.requestId !== "string" ||
1701
+ typeof clientMessage.capability !== "string" || !clientMessage.capability ||
1702
+ (clientMessage.group !== undefined && typeof clientMessage.group !== "string") ||
1703
+ (clientMessage.dormant !== undefined && typeof clientMessage.dormant !== "boolean") ||
1704
+ (clientMessage.aliases !== undefined && !isRestoringRecipientAliasList(clientMessage.aliases))
1705
+ ) {
1706
+ throw new Error("Invalid reserve_restoring_recipient message");
1707
+ }
1708
+ // Only the supervisor the capability was issued to may hold sends for that child (#3760).
1709
+ const childName = this.supervisorChannel.supervisedChildName(clientMessage.capability, currentId);
1710
+ const aliases = (clientMessage.aliases ?? []) as string[];
1711
+ if (childName === undefined || !aliases.every(isRestoringRecipientAlias)) {
1712
+ writeMessageIfOpen(socket, {
1713
+ type: "restoring_recipient_ack",
1714
+ requestId: clientMessage.requestId,
1715
+ error: childName === undefined
1716
+ ? "Only the supervisor authorized for this subagent can reserve its Intercom targets"
1717
+ : "A restoring subagent can be reserved only under its name and session ids",
1718
+ });
1719
+ break;
1720
+ }
1721
+ this.restoringRecipients.reserve(
1722
+ clientMessage.capability,
1723
+ currentId,
1724
+ childName,
1725
+ normalizeGroup(typeof clientMessage.group === "string" ? clientMessage.group : owner.registrationGroup),
1726
+ aliases,
1727
+ { dormant: clientMessage.dormant === true },
1728
+ );
1729
+ this.multiNode?.restoringReserved(
1730
+ clientMessage.capability,
1731
+ currentId,
1732
+ childName,
1733
+ normalizeGroup(typeof clientMessage.group === "string" ? clientMessage.group : owner.registrationGroup),
1734
+ aliases,
1735
+ clientMessage.dormant === true,
1736
+ );
1737
+ writeMessageIfOpen(socket, { type: "restoring_recipient_ack", requestId: clientMessage.requestId });
1738
+ break;
1739
+ }
1740
+
1741
+ case "release_restoring_recipient": {
1742
+ if (
1743
+ typeof clientMessage.requestId !== "string" || typeof clientMessage.capability !== "string" ||
1744
+ (clientMessage.gone !== undefined && typeof clientMessage.gone !== "string") ||
1745
+ (clientMessage.refusal !== undefined && typeof clientMessage.refusal !== "string")
1746
+ ) {
1747
+ throw new Error("Invalid release_restoring_recipient message");
1748
+ }
1749
+ const released = this.restoringRecipients.release(
1750
+ clientMessage.capability,
1751
+ currentId,
1752
+ typeof clientMessage.gone === "string" ? clientMessage.gone.slice(0, RESTORING_RECIPIENT_GONE_REASON_MAX) : undefined,
1753
+ typeof clientMessage.refusal === "string" && clientMessage.refusal.trim()
1754
+ ? clientMessage.refusal.slice(0, RESTORING_RECIPIENT_GONE_REASON_MAX)
1755
+ : undefined,
1756
+ );
1757
+ if (released !== undefined) {
1758
+ this.multiNode?.restoringReleased(clientMessage.capability, currentId, released.gone, released.refusal);
1759
+ }
1760
+ writeMessageIfOpen(socket, { type: "restoring_recipient_ack", requestId: clientMessage.requestId });
1761
+ for (const send of released?.held ?? []) {
1762
+ if (released?.gone !== undefined) this.failHeldSend(send, released.gone);
1763
+ else if (released?.refusal !== undefined) this.failHeldSend(send, released.refusal);
1764
+ else this.dispatchSend(send.socket, send.clientMessage, send.senderId);
1765
+ }
1766
+ break;
1767
+ }
1768
+
1534
1769
  case "register_pending_stage_route": {
1535
1770
  if (
1536
1771
  typeof clientMessage.runId !== "string" ||
@@ -1544,7 +1779,21 @@ class IntercomBroker {
1544
1779
  const owner = this.sessions.get(currentId);
1545
1780
  const existing = this.pendingStageRoutes.get(clientMessage.runId);
1546
1781
  const ownerGroup = normalizeGroup(owner?.registrationGroup);
1547
- const activeExisting = existing !== undefined && this.sessions.has(existing.sessionId) ? existing : undefined;
1782
+ const dormant = clientMessage.dormant === true;
1783
+ const liveExisting = existing !== undefined && this.sessions.has(existing.sessionId) ? existing : undefined;
1784
+ // #3760: a restarted owner queues for its interrupted run under a dormant route. The process that
1785
+ // resumes the run holds the run's lease and a capability of its own, so it takes the route over and
1786
+ // the dormant owner is told to stop answering for it; a dormant route never takes over another.
1787
+ const yieldedDormant =
1788
+ liveExisting?.dormant === true &&
1789
+ !dormant &&
1790
+ liveExisting.sessionId !== currentId &&
1791
+ liveExisting.capability !== clientMessage.capability &&
1792
+ normalizeGroup(clientMessage.group) === ownerGroup &&
1793
+ liveExisting.group === ownerGroup
1794
+ ? liveExisting
1795
+ : undefined;
1796
+ const activeExisting = yieldedDormant === undefined ? liveExisting : undefined;
1548
1797
  if (
1549
1798
  owner === undefined ||
1550
1799
  normalizeGroup(clientMessage.group) !== ownerGroup ||
@@ -1611,7 +1860,13 @@ class IntercomBroker {
1611
1860
  sessionId: currentId,
1612
1861
  group: ownerGroup,
1613
1862
  capability: clientMessage.capability,
1863
+ ...(dormant ? { dormant: true as const } : {}),
1614
1864
  });
1865
+ if (yieldedDormant !== undefined) {
1866
+ const revoked = this.sessions.get(yieldedDormant.sessionId);
1867
+ if (revoked !== undefined)
1868
+ writeMessageIfOpen(revoked.socket, { type: "pending_stage_route_revoked", runId: clientMessage.runId });
1869
+ }
1615
1870
  if (clientMessage.stages !== undefined || possibleStages !== undefined) {
1616
1871
  this.workflowRosters.set(clientMessage.runId, {
1617
1872
  ownerSessionId: currentId,
@@ -1728,22 +1983,17 @@ class IntercomBroker {
1728
1983
 
1729
1984
  case "send":
1730
1985
  case "supervisor_send": {
1731
- handleBrokerSend(
1732
- socket,
1733
- clientMessage,
1734
- currentId,
1735
- this.sessions,
1736
- this.deliveredMessages,
1737
- writeMessageIfOpen,
1738
- this.supervisorChannel,
1739
- this.pendingQuestions,
1740
- this.routePendingStage,
1741
- this.resolveLiveWorkflowStage,
1742
- this.canControlLiveWorkflowStage,
1743
- this.resolveLegacyWorkflowStageTarget,
1744
- writeMessageWithOutcome,
1745
- this.isNonAgentWorkflowTarget,
1746
- );
1986
+ this.dispatchSend(socket, clientMessage, currentId);
1987
+ break;
1988
+ }
1989
+
1990
+ case "received": {
1991
+ if (typeof clientMessage.messageId !== "string") throw new Error("Invalid received message");
1992
+ const recipient = this.sessions.get(currentId);
1993
+ if (recipient?.acknowledgesReceipt === true) {
1994
+ this.multiNode?.received(recipient, clientMessage.messageId);
1995
+ this.receipts.settle(clientMessage.messageId, deliveryTargetIdentity(recipient), recipient);
1996
+ }
1747
1997
  break;
1748
1998
  }
1749
1999
 
@@ -1771,6 +2021,8 @@ class IntercomBroker {
1771
2021
  (target, message) => writeMessageIfOpen(target, message),
1772
2022
  this.pendingQuestions,
1773
2023
  );
2024
+ const updated = this.sessions.get(currentId);
2025
+ if (updated !== undefined) this.multiNode?.sessionUpdated(updated);
1774
2026
  break;
1775
2027
  }
1776
2028
 
@@ -1779,10 +2031,191 @@ class IntercomBroker {
1779
2031
  }
1780
2032
  }
1781
2033
 
2034
+ /** Route one send or ask frame, as received or again after a restoring subagent's hold ends (#3760). */
2035
+ private dispatchSend(socket: net.Socket, clientMessage: SendClientMessage, senderId: string | null): void {
2036
+ handleBrokerSend(
2037
+ socket,
2038
+ clientMessage,
2039
+ senderId,
2040
+ this.sessions,
2041
+ this.deliveredMessages,
2042
+ writeMessageIfOpen,
2043
+ this.supervisorChannel,
2044
+ this.pendingQuestions,
2045
+ this.routePendingStage,
2046
+ this.resolveLiveWorkflowStage,
2047
+ this.canControlLiveWorkflowStage,
2048
+ this.resolveLegacyWorkflowStageTarget,
2049
+ writeMessageWithOutcome,
2050
+ this.isNonAgentWorkflowTarget,
2051
+ this.holdForRestoringRecipient,
2052
+ this.receipts,
2053
+ this.routeRemote,
2054
+ );
2055
+ }
2056
+
2057
+ /** Multi-node S5: a send no local session resolved goes to the shared state once it is open. */
2058
+ private routeRemoteSend(request: RemoteSendRequest): void {
2059
+ if (this.multiNode !== undefined) {
2060
+ this.multiNode.routeSend(request);
2061
+ return;
2062
+ }
2063
+ // Off, or a send to a session on this broker: local delivery never depends on the shared database (spec §8).
2064
+ if (this.multiNodeStatus === "off" || (this.multiNodeStatus === "unavailable" && request.localTarget !== undefined)) {
2065
+ request.notFound();
2066
+ return;
2067
+ }
2068
+ if (this.multiNodeStatus === "starting") {
2069
+ if (request.localTarget === undefined) {
2070
+ void this.multiNodeFirstAttempt.then(() => this.routeRemoteSend(request));
2071
+ return;
2072
+ }
2073
+ // A send to a session on this broker waits for the first opening at most as long as the router's own retry
2074
+ // lookup would, then is delivered locally; whichever comes second does nothing, so it is answered once.
2075
+ let answered = false;
2076
+ const bound = setTimeout(() => {
2077
+ answered = true;
2078
+ request.notFound();
2079
+ }, LOCAL_RETRY_LOOKUP_BOUND_MS);
2080
+ void this.multiNodeFirstAttempt.then(() => {
2081
+ if (answered) return;
2082
+ answered = true;
2083
+ clearTimeout(bound);
2084
+ this.routeRemoteSend(request);
2085
+ });
2086
+ return;
2087
+ }
2088
+ writeMessageIfOpen(request.socket, {
2089
+ type: "delivery_failed",
2090
+ messageId: request.message.id,
2091
+ ...(request.attemptId === undefined ? {} : { attemptId: request.attemptId }),
2092
+ reason: SHARED_STORAGE_UNAVAILABLE_REASON,
2093
+ ...(request.requirePendingReply ? { reasonCode: "session_not_found" as const } : {}),
2094
+ });
2095
+ }
2096
+
2097
+ /**
2098
+ * The shared state is loaded lazily, so a single-node broker never loads the host storage layer. A database that
2099
+ * cannot be opened is retried with exponential backoff; meanwhile local delivery continues.
2100
+ */
2101
+ private startMultiNode(setting: Record<string, unknown>): void {
2102
+ this.multiNodeStatus = "starting";
2103
+ let settleFirstAttempt: () => void = () => {};
2104
+ this.multiNodeFirstAttempt = new Promise<void>((resolve) => {
2105
+ settleFirstAttempt = resolve;
2106
+ });
2107
+ const settle = (status: "on" | "off" | "unavailable"): void => {
2108
+ this.multiNodeStatus = status;
2109
+ settleFirstAttempt();
2110
+ };
2111
+ const report = (error: unknown): void => {
2112
+ console.error(`Intercom multi-node routing is unavailable: ${error instanceof Error ? error.message : String(error)}`);
2113
+ };
2114
+ void (async () => {
2115
+ const startup = await resolveMultiNodeStartup(setting, dirname(INTERCOM_DIR), async () => {
2116
+ const loaded = await import("./open-broker-state.js");
2117
+ type Source = Parameters<typeof loaded.openIntercomBrokerState>[0];
2118
+ const opener: BrokerStateOpener<Source> = {
2119
+ resolveBrokerStateSource: (value, agentDir) =>
2120
+ loaded.resolveBrokerStateSource(value as Parameters<typeof loaded.resolveBrokerStateSource>[0], agentDir),
2121
+ openIntercomBrokerState: (source) => loaded.openIntercomBrokerState(source),
2122
+ };
2123
+ return opener;
2124
+ });
2125
+ if (startup.status === "off") {
2126
+ // A standalone binary has no host storage module, and a selection may not resolve to a database: both
2127
+ // leave this broker single-node, exactly as without a shared `storage`.
2128
+ if (startup.error !== undefined) console.error(`Intercom multi-node routing is off: ${startup.reason}`);
2129
+ settle("off");
2130
+ return;
2131
+ }
2132
+ const { opener, source } = startup;
2133
+ for (let attempt = 0; !this.shuttingDown; attempt++) {
2134
+ try {
2135
+ const opened = await opener.openIntercomBrokerState(source);
2136
+ try {
2137
+ this.multiNode = await MultiNodeRouter.start({
2138
+ ...opened,
2139
+ host: {
2140
+ sessions: this.sessions,
2141
+ write: writeMessageIfOpen,
2142
+ writeConfirmed: writeMessageWithOutcome,
2143
+ supervisorCache: this.supervisorChannel,
2144
+ pendingQuestions: this.pendingQuestions,
2145
+ deliveredMessages: this.deliveredMessages,
2146
+ dispatch: (socket, clientMessage, senderId) => this.dispatchSend(socket, clientMessage, senderId),
2147
+ },
2148
+ });
2149
+ } catch (error) {
2150
+ await opened.bus.close().catch(() => {});
2151
+ await opened.state.close().catch(() => {});
2152
+ throw error;
2153
+ }
2154
+ // Reservations acknowledged before routing started (or while an earlier opening failed) were kept only
2155
+ // here; mirror the ones still in force so other brokers hold sends for them too. Releases and owner
2156
+ // departures from now on reach the router behind these reserves.
2157
+ for (const reservation of this.restoringRecipients.active()) {
2158
+ this.multiNode.restoringReserved(
2159
+ reservation.capability,
2160
+ reservation.ownerSessionId,
2161
+ reservation.childName,
2162
+ reservation.group,
2163
+ reservation.aliases,
2164
+ reservation.dormant,
2165
+ );
2166
+ if (reservation.gone !== undefined) {
2167
+ this.multiNode.restoringReleased(reservation.capability, reservation.ownerSessionId, reservation.gone);
2168
+ }
2169
+ }
2170
+ if (this.shuttingDown) await this.multiNode.close().catch(() => {});
2171
+ settle("on");
2172
+ return;
2173
+ } catch (error) {
2174
+ report(error);
2175
+ settle("unavailable");
2176
+ await new Promise<void>((resolve) => {
2177
+ setTimeout(resolve, Math.min(1_000 * 2 ** attempt, OUTBOX_BACKUP_POLL_INTERVAL_MS)).unref();
2178
+ });
2179
+ }
2180
+ }
2181
+ })();
2182
+ }
2183
+
2184
+ /**
2185
+ * #3760: a send to a subagent its owner is restoring is held, by whichever sender, instead of failing with
2186
+ * `Session not found`; a send to one its owner reported gone gets the gone refusal.
2187
+ */
2188
+ private readonly holdForRestoringRecipient: RestoringRecipientHold = ({ socket, from, clientMessage, target, messageId, attemptId }) => {
2189
+ const held: HeldSend = { socket, senderId: from.info.id, messageId, clientMessage, ...(attemptId === undefined ? {} : { attemptId }) };
2190
+ const admission = this.restoringRecipients.admit(
2191
+ target,
2192
+ { id: from.info.id, info: from.info },
2193
+ held,
2194
+ clientMessage.awaitRestoringRecipient === true,
2195
+ );
2196
+ if (admission.kind === "none") return false;
2197
+ if (admission.kind === "refused") this.failHeldSend(held, admission.reason);
2198
+ else writeMessageIfOpen(socket, { type: "send_held", messageId, ...(attemptId === undefined ? {} : { attemptId }), timeoutMs: admission.timeoutMs });
2199
+ return true;
2200
+ };
2201
+
2202
+ private failHeldSend(held: HeldSend, reason: string): void {
2203
+ writeMessageIfOpen(held.socket, {
2204
+ type: "delivery_failed",
2205
+ messageId: held.messageId,
2206
+ ...(held.attemptId === undefined ? {} : { attemptId: held.attemptId }),
2207
+ reason,
2208
+ });
2209
+ }
2210
+
1782
2211
  private disconnectSession(sessionId: string): void {
1783
2212
  const departed = this.sessions.get(sessionId);
1784
2213
  if (!departed) return;
1785
2214
 
2215
+ this.restoringRecipients.dropSender(sessionId);
2216
+ // #3760: a departed owner restores nothing more; what it held goes through ordinary routing once it is gone.
2217
+ const unreserved = this.restoringRecipients.dropOwner(sessionId);
2218
+
1786
2219
  this.pendingQuestions.pruneSender(sessionId);
1787
2220
  for (const question of this.pendingQuestions.takeForTarget(sessionId)) {
1788
2221
  const asker = this.sessions.get(question.senderSessionId);
@@ -1832,7 +2265,10 @@ class IntercomBroker {
1832
2265
  if (activation.sessionId === sessionId) this.liveWorkflowStageRouteActivations.delete(requestId);
1833
2266
  }
1834
2267
  this.sessions.delete(sessionId);
2268
+ this.multiNode?.sessionLeft(departed);
2269
+ this.sessionIdReclaim.retire(sessionId, departed.registrationReturnAddress);
1835
2270
  if (isAgentRecipient(departed.info)) this.broadcastToMemberships({ type: "session_left", sessionId }, sessionGroups(departed.info), sessionId);
2271
+ for (const send of unreserved) this.dispatchSend(send.socket, send.clientMessage, send.senderId);
1836
2272
  }
1837
2273
  /** Deliver a broadcast once to each session represented in any given group. */
1838
2274
  private broadcastToMemberships(msg: BrokerMessage, groups: ReadonlySet<string>, exclude?: string): void {
@@ -1863,6 +2299,8 @@ class IntercomBroker {
1863
2299
  }
1864
2300
 
1865
2301
  private shutdown(): void {
2302
+ if (this.shuttingDown) return;
2303
+ this.shuttingDown = true;
1866
2304
  console.log("Broker shutting down");
1867
2305
 
1868
2306
  for (const session of this.sessions.values()) {
@@ -1873,9 +2311,22 @@ class IntercomBroker {
1873
2311
  this.pendingStageAcknowledgments.clear();
1874
2312
  this.liveWorkflowStageRoutes.clear();
1875
2313
  this.liveWorkflowStageRouteActivations.clear();
2314
+ this.restoringRecipients.clear();
2315
+ this.receipts.clear();
1876
2316
  for (const pending of this.pendingStageNotificationAcknowledgments.values()) clearTimeout(pending.timeout);
1877
2317
  this.pendingStageNotificationAcknowledgments.clear();
1878
2318
  this.deliveredMessages.close();
2319
+ const multiNode = this.multiNode;
2320
+ if (multiNode === undefined) {
2321
+ this.exitAfterShutdown();
2322
+ return;
2323
+ }
2324
+ // Give this broker's leases up so other brokers see its sessions offline at once, within a bound.
2325
+ const bound = new Promise<void>((resolve) => setTimeout(resolve, MULTI_NODE_CLOSE_BOUND_MS).unref());
2326
+ void Promise.race([multiNode.close().catch(() => {}), bound]).finally(() => this.exitAfterShutdown());
2327
+ }
2328
+
2329
+ private exitAfterShutdown(): never {
1879
2330
  const ownsRuntime = this.readPidFile() === process.pid;
1880
2331
  this.unlinkRuntimeFilesIfOwned();
1881
2332
  // Node unlinks a Unix socket path on server.close() even after a successor