@bastani/atomic 0.9.41 → 0.9.42-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 (270) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +13 -0
  3. package/dist/builtin/intercom/README.md +7 -3
  4. package/dist/builtin/intercom/broker/broker-status.ts +48 -0
  5. package/dist/builtin/intercom/broker/broker.ts +90 -9
  6. package/dist/builtin/intercom/broker/client.ts +112 -12
  7. package/dist/builtin/intercom/broker/control.ts +225 -0
  8. package/dist/builtin/intercom/broker/multinode-router.ts +32 -39
  9. package/dist/builtin/intercom/broker/paths.ts +4 -0
  10. package/dist/builtin/intercom/broker/pending-send-registry.ts +27 -5
  11. package/dist/builtin/intercom/broker/shared-storage-setting.ts +60 -0
  12. package/dist/builtin/intercom/broker/spawn.ts +13 -4
  13. package/dist/builtin/intercom/broker/sql-lease-store.ts +23 -0
  14. package/dist/builtin/intercom/broker/watchdog.ts +105 -0
  15. package/dist/builtin/intercom/index.bundle.mjs +200 -22
  16. package/dist/builtin/intercom/package.json +1 -1
  17. package/dist/builtin/intercom/types.ts +34 -2
  18. package/dist/builtin/mcp/package.json +1 -1
  19. package/dist/builtin/subagents/package.json +1 -1
  20. package/dist/builtin/subagents/src/extension/index.bundle.mjs +1 -0
  21. package/dist/builtin/web-access/package.json +1 -1
  22. package/dist/builtin/workflows/CHANGELOG.md +12 -0
  23. package/dist/builtin/workflows/README.md +10 -0
  24. package/dist/builtin/workflows/builtin/{chunk-txbc93t6.js → chunk-3s95f7ry.js} +1 -1
  25. package/dist/builtin/workflows/builtin/{chunk-n2gkzkty.js → chunk-edhf30ef.js} +1 -1
  26. package/dist/builtin/workflows/builtin/{chunk-8x61e0s7.js → chunk-frqxd4d1.js} +18 -9
  27. package/dist/builtin/workflows/builtin/goal.js +2 -2
  28. package/dist/builtin/workflows/builtin/index.js +3 -3
  29. package/dist/builtin/workflows/builtin/ralph.js +2 -2
  30. package/dist/builtin/workflows/package.json +6 -1
  31. package/dist/builtin/workflows/src/authoring.d.ts +3 -1
  32. package/dist/builtin/workflows/src/durable/journal/internal.d.ts +13 -0
  33. package/dist/builtin/workflows/src/durable/journal/resolution.d.ts +2 -0
  34. package/dist/builtin/workflows/src/durable/journal/sql-run-journal.d.ts +1 -0
  35. package/dist/builtin/workflows/src/extension/index.bundle.mjs +88554 -82820
  36. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +72635 -0
  37. package/dist/builtin/workflows/src/index.js +53824 -5907
  38. package/dist/builtin/workflows/src/local-worker-entry.mjs +72635 -0
  39. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +7 -0
  40. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +2 -0
  41. package/dist/builtin/workflows/src/workers/public-options.d.ts +31 -0
  42. package/dist/bun/app-host-modules.d.ts +9 -0
  43. package/dist/bun/app-host-modules.d.ts.map +1 -0
  44. package/dist/bun/app-host-modules.js +10 -0
  45. package/dist/bun/app-host-modules.js.map +1 -0
  46. package/dist/bun/cli.d.ts +3 -1
  47. package/dist/bun/cli.d.ts.map +1 -1
  48. package/dist/bun/cli.js +15 -1
  49. package/dist/bun/cli.js.map +1 -1
  50. package/dist/bun/internal-intercom-broker.d.ts +17 -0
  51. package/dist/bun/internal-intercom-broker.d.ts.map +1 -1
  52. package/dist/bun/internal-intercom-broker.js +29 -0
  53. package/dist/bun/internal-intercom-broker.js.map +1 -1
  54. package/dist/bun/internal-workflow-worker.d.ts +4 -0
  55. package/dist/bun/internal-workflow-worker.d.ts.map +1 -0
  56. package/dist/bun/internal-workflow-worker.js +37 -0
  57. package/dist/bun/internal-workflow-worker.js.map +1 -0
  58. package/dist/bun/split-loader.js +10 -2
  59. package/dist/bun/split-loader.js.map +1 -1
  60. package/dist/cli/args.d.ts.map +1 -1
  61. package/dist/cli/args.js +3 -1
  62. package/dist/cli/args.js.map +1 -1
  63. package/dist/cli/intercom-command.d.ts +15 -0
  64. package/dist/cli/intercom-command.d.ts.map +1 -0
  65. package/dist/cli/intercom-command.js +87 -0
  66. package/dist/cli/intercom-command.js.map +1 -0
  67. package/dist/cli/storage-command.d.ts +39 -0
  68. package/dist/cli/storage-command.d.ts.map +1 -0
  69. package/dist/cli/storage-command.js +367 -0
  70. package/dist/cli/storage-command.js.map +1 -0
  71. package/dist/core/agent-session-events.d.ts.map +1 -1
  72. package/dist/core/agent-session-events.js +5 -1
  73. package/dist/core/agent-session-events.js.map +1 -1
  74. package/dist/core/agent-session-methods.d.ts +1 -0
  75. package/dist/core/agent-session-methods.d.ts.map +1 -1
  76. package/dist/core/agent-session-methods.js.map +1 -1
  77. package/dist/core/agent-session-tasks.d.ts.map +1 -1
  78. package/dist/core/agent-session-tasks.js +1 -0
  79. package/dist/core/agent-session-tasks.js.map +1 -1
  80. package/dist/core/agent-session-types.d.ts +1 -0
  81. package/dist/core/agent-session-types.d.ts.map +1 -1
  82. package/dist/core/agent-session-types.js.map +1 -1
  83. package/dist/core/agent-session.d.ts +1 -0
  84. package/dist/core/agent-session.d.ts.map +1 -1
  85. package/dist/core/agent-session.js +2 -0
  86. package/dist/core/agent-session.js.map +1 -1
  87. package/dist/core/builtin-install-layout.d.ts +6 -0
  88. package/dist/core/builtin-install-layout.d.ts.map +1 -1
  89. package/dist/core/builtin-install-layout.js +9 -0
  90. package/dist/core/builtin-install-layout.js.map +1 -1
  91. package/dist/core/child-session-options.d.ts.map +1 -1
  92. package/dist/core/child-session-options.js +3 -0
  93. package/dist/core/child-session-options.js.map +1 -1
  94. package/dist/core/exec.js +2 -2
  95. package/dist/core/exec.js.map +1 -1
  96. package/dist/core/export-html/template.css +9 -0
  97. package/dist/core/export-html/template.js +165 -45
  98. package/dist/core/extensions/loader-host-modules.d.ts +6 -0
  99. package/dist/core/extensions/loader-host-modules.d.ts.map +1 -1
  100. package/dist/core/extensions/loader-host-modules.js +12 -1
  101. package/dist/core/extensions/loader-host-modules.js.map +1 -1
  102. package/dist/core/extensions/loader-virtual-modules.d.ts +6 -0
  103. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  104. package/dist/core/extensions/loader-virtual-modules.js +18 -0
  105. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  106. package/dist/core/extensions/message-types.d.ts +4 -0
  107. package/dist/core/extensions/message-types.d.ts.map +1 -1
  108. package/dist/core/extensions/message-types.js.map +1 -1
  109. package/dist/core/fast-model-routing-transport.d.ts.map +1 -1
  110. package/dist/core/fast-model-routing-transport.js +5 -1
  111. package/dist/core/fast-model-routing-transport.js.map +1 -1
  112. package/dist/core/intercom-broker-control.d.ts +67 -0
  113. package/dist/core/intercom-broker-control.d.ts.map +1 -0
  114. package/dist/core/intercom-broker-control.js +17 -0
  115. package/dist/core/intercom-broker-control.js.map +1 -0
  116. package/dist/core/model-runtime-streaming.d.ts.map +1 -1
  117. package/dist/core/model-runtime-streaming.js +38 -1
  118. package/dist/core/model-runtime-streaming.js.map +1 -1
  119. package/dist/core/package-manager-git.d.ts +2 -2
  120. package/dist/core/package-manager-git.d.ts.map +1 -1
  121. package/dist/core/package-manager-git.js.map +1 -1
  122. package/dist/core/package-manager-manifest.d.ts.map +1 -1
  123. package/dist/core/package-manager-manifest.js +9 -1
  124. package/dist/core/package-manager-manifest.js.map +1 -1
  125. package/dist/core/package-manager-npm.d.ts +2 -2
  126. package/dist/core/package-manager-npm.d.ts.map +1 -1
  127. package/dist/core/package-manager-npm.js.map +1 -1
  128. package/dist/core/package-manager-paths.d.ts +7 -7
  129. package/dist/core/package-manager-paths.d.ts.map +1 -1
  130. package/dist/core/package-manager-paths.js.map +1 -1
  131. package/dist/core/package-manager-storage-adapters.d.ts +7 -0
  132. package/dist/core/package-manager-storage-adapters.d.ts.map +1 -0
  133. package/dist/core/package-manager-storage-adapters.js +76 -0
  134. package/dist/core/package-manager-storage-adapters.js.map +1 -0
  135. package/dist/core/package-manager-types.d.ts +4 -0
  136. package/dist/core/package-manager-types.d.ts.map +1 -1
  137. package/dist/core/package-manager-types.js.map +1 -1
  138. package/dist/core/provider-rate-limits.d.ts +23 -0
  139. package/dist/core/provider-rate-limits.d.ts.map +1 -0
  140. package/dist/core/provider-rate-limits.js +113 -0
  141. package/dist/core/provider-rate-limits.js.map +1 -0
  142. package/dist/core/provider-request-admission.d.ts +7 -0
  143. package/dist/core/provider-request-admission.d.ts.map +1 -0
  144. package/dist/core/provider-request-admission.js +91 -0
  145. package/dist/core/provider-request-admission.js.map +1 -0
  146. package/dist/core/resource-admission-scope.d.ts +4 -0
  147. package/dist/core/resource-admission-scope.d.ts.map +1 -0
  148. package/dist/core/resource-admission-scope.js +9 -0
  149. package/dist/core/resource-admission-scope.js.map +1 -0
  150. package/dist/core/sdk-types.d.ts +16 -0
  151. package/dist/core/sdk-types.d.ts.map +1 -1
  152. package/dist/core/sdk-types.js.map +1 -1
  153. package/dist/core/sdk.d.ts +2 -1
  154. package/dist/core/sdk.d.ts.map +1 -1
  155. package/dist/core/sdk.js +18 -2
  156. package/dist/core/sdk.js.map +1 -1
  157. package/dist/core/session-lifecycle-work.d.ts +1 -0
  158. package/dist/core/session-lifecycle-work.d.ts.map +1 -1
  159. package/dist/core/session-lifecycle-work.js +1 -0
  160. package/dist/core/session-lifecycle-work.js.map +1 -1
  161. package/dist/core/settings-manager-basic-accessors.d.ts +3 -0
  162. package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
  163. package/dist/core/settings-manager-basic-accessors.js +6 -0
  164. package/dist/core/settings-manager-basic-accessors.js.map +1 -1
  165. package/dist/core/settings-schema.d.ts +22 -0
  166. package/dist/core/settings-schema.d.ts.map +1 -1
  167. package/dist/core/settings-schema.js +18 -1
  168. package/dist/core/settings-schema.js.map +1 -1
  169. package/dist/core/storage/index.d.ts +1 -0
  170. package/dist/core/storage/index.d.ts.map +1 -1
  171. package/dist/core/storage/index.js +1 -0
  172. package/dist/core/storage/index.js.map +1 -1
  173. package/dist/core/storage/node-sqlite.d.ts +2 -1
  174. package/dist/core/storage/node-sqlite.d.ts.map +1 -1
  175. package/dist/core/storage/node-sqlite.js +5 -4
  176. package/dist/core/storage/node-sqlite.js.map +1 -1
  177. package/dist/core/storage/provider.d.ts +6 -0
  178. package/dist/core/storage/provider.d.ts.map +1 -1
  179. package/dist/core/storage/provider.js +27 -7
  180. package/dist/core/storage/provider.js.map +1 -1
  181. package/dist/core/storage/sqlite-facade.d.ts +49 -0
  182. package/dist/core/storage/sqlite-facade.d.ts.map +1 -0
  183. package/dist/core/storage/sqlite-facade.js +180 -0
  184. package/dist/core/storage/sqlite-facade.js.map +1 -0
  185. package/dist/core/storage/storage-status.d.ts +81 -0
  186. package/dist/core/storage/storage-status.d.ts.map +1 -0
  187. package/dist/core/storage/storage-status.js +409 -0
  188. package/dist/core/storage/storage-status.js.map +1 -0
  189. package/dist/core/tasks/supervisor.d.ts +2 -1
  190. package/dist/core/tasks/supervisor.d.ts.map +1 -1
  191. package/dist/core/tasks/supervisor.js +105 -4
  192. package/dist/core/tasks/supervisor.js.map +1 -1
  193. package/dist/core/tools/bash-pty-native.d.ts.map +1 -1
  194. package/dist/core/tools/bash-pty-native.js +1 -1
  195. package/dist/core/tools/bash-pty-native.js.map +1 -1
  196. package/dist/index.d.ts +2 -1
  197. package/dist/index.d.ts.map +1 -1
  198. package/dist/index.js +2 -1
  199. package/dist/index.js.map +1 -1
  200. package/dist/main.d.ts.map +1 -1
  201. package/dist/main.js +28 -2
  202. package/dist/main.js.map +1 -1
  203. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  204. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  205. package/dist/modes/interactive/components/assistant-message.js +7 -3
  206. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  207. package/dist/modes/interactive/components/chat-message-renderer.d.ts +9 -1
  208. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
  209. package/dist/modes/interactive/components/chat-message-renderer.js +16 -8
  210. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
  211. package/dist/modes/interactive/components/login-dialog.d.ts +5 -0
  212. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  213. package/dist/modes/interactive/components/login-dialog.js +69 -2
  214. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  215. package/dist/modes/interactive/components/markdown-transform.d.ts +1 -1
  216. package/dist/modes/interactive/components/markdown-transform.d.ts.map +1 -1
  217. package/dist/modes/interactive/components/markdown-transform.js +2 -2
  218. package/dist/modes/interactive/components/markdown-transform.js.map +1 -1
  219. package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
  220. package/dist/modes/interactive/components/session-selector-search.js +80 -22
  221. package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
  222. package/dist/modes/interactive/components/user-message.d.ts +3 -1
  223. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  224. package/dist/modes/interactive/components/user-message.js +6 -2
  225. package/dist/modes/interactive/components/user-message.js.map +1 -1
  226. package/dist/modes/interactive/interactive-mode-deps.d.ts +1 -1
  227. package/dist/modes/interactive/interactive-mode-deps.d.ts.map +1 -1
  228. package/dist/modes/interactive/interactive-mode-deps.js.map +1 -1
  229. package/dist/modes/interactive/interactive-render-chat.js +6 -2
  230. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  231. package/dist/modes/interactive-engine/engine-health.d.ts +8 -4
  232. package/dist/modes/interactive-engine/engine-health.d.ts.map +1 -1
  233. package/dist/modes/interactive-engine/engine-health.js +41 -6
  234. package/dist/modes/interactive-engine/engine-health.js.map +1 -1
  235. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  236. package/dist/modes/interactive-engine/isolated-runtime.js +19 -17
  237. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  238. package/dist/modes/interactive-engine/remote-queue-pause.js +1 -1
  239. package/dist/modes/interactive-engine/remote-queue-pause.js.map +1 -1
  240. package/dist/modes/rpc/rpc-input-scheduler.d.ts +1 -0
  241. package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
  242. package/dist/modes/rpc/rpc-input-scheduler.js +4 -1
  243. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  244. package/dist/modes/rpc/rpc-input.d.ts.map +1 -1
  245. package/dist/modes/rpc/rpc-input.js +29 -7
  246. package/dist/modes/rpc/rpc-input.js.map +1 -1
  247. package/dist/utils/shell.d.ts.map +1 -1
  248. package/dist/utils/shell.js +14 -0
  249. package/dist/utils/shell.js.map +1 -1
  250. package/docs/custom-provider/oauth.md +8 -1
  251. package/docs/extensions/api-reference.md +2 -0
  252. package/docs/intercom/operations.md +17 -8
  253. package/docs/intercom/reference.md +1 -1
  254. package/docs/keybindings.md +1 -1
  255. package/docs/packages/authoring.md +15 -0
  256. package/docs/packages.md +3 -2
  257. package/docs/providers.md +2 -0
  258. package/docs/reference/cli.md +17 -0
  259. package/docs/sdk.md +37 -0
  260. package/docs/settings.md +33 -2
  261. package/docs/storage.md +191 -219
  262. package/docs/usage.md +1 -1
  263. package/docs/workflows/api-reference.md +50 -0
  264. package/docs/workflows/operations.md +44 -1
  265. package/docs/workflows.md +25 -0
  266. package/examples/extensions/gondolin/package-lock.json +14 -24
  267. package/examples/extensions/gondolin/package.json +1 -1
  268. package/npm-shrinkwrap.json +35 -35
  269. package/package.json +4 -4
  270. package/schemas/settings.schema.json +87 -1
@@ -1,12 +1,14 @@
1
1
  import { EventEmitter } from "events";
2
2
  import net from "net";
3
3
  import { randomUUID } from "crypto";
4
- import { writeMessage, createMessageReader } from "./framing.js";
4
+ import { writeMessage, createMessageReader, type JsonWireValue } from "./framing.js";
5
+ import { isBrokerStatusReport } from "./broker-status.js";
5
6
  import { getBrokerSocketPath } from "./paths.js";
6
7
  import type {
7
8
  SessionInfo,
8
9
  Message,
9
10
  Attachment,
11
+ BrokerStatusReport,
10
12
  GroupSummary,
11
13
  SupervisorRegistration,
12
14
  SessionDirectory,
@@ -16,7 +18,7 @@ import type {
16
18
  WorkflowPossibleStageAnnouncement,
17
19
  WorkflowRunParentAnnouncement,
18
20
  } from "../types.js";
19
- import { buildSendSignature, PendingSendRegistry } from "./pending-send-registry.js";
21
+ import { buildSendSignature, PendingSendRegistry, PendingSendTimeoutError } from "./pending-send-registry.js";
20
22
  import { readSubagentMessageSource } from "../source-ownership.js";
21
23
  import { isMessage, isSessionInfo } from "./client-message-validation.js";
22
24
  import { normalizeGroups } from "../group.js";
@@ -175,12 +177,14 @@ export class IntercomClient extends EventEmitter {
175
177
  private brokerAcknowledgesReceipt = false;
176
178
  /** The broker of the current registration answers a pending-stage route registration that carries a `requestId`. */
177
179
  private brokerAcknowledgesPendingStageRoutes = false;
180
+ private brokerAnswersStatus = false;
178
181
  private socket: net.Socket | null = null;
179
182
  private _sessionId: string | null = null;
180
183
  private _supervisorSessionId: string | null = null;
181
184
  /** Source identity is captured once at connect, never read from env per message. */
182
185
  private _messageSource: Message["source"] | undefined;
183
186
  private pendingSends = new PendingSendRegistry();
187
+ private readonly sendResults = new WeakMap<Promise<SendResult>, Promise<SendResult>>();
184
188
  private pendingReplyTargets = new Map<string, { messageId: string; bind(sessionId: string): void }>();
185
189
  private pendingGroupLists = new Map<string, { resolve: (groups: GroupSummary[]) => void; reject: (error: Error) => void }>();
186
190
  private pendingLists = new Map<string, { resolve: (directory: SessionDirectory) => void; reject: (e: Error) => void }>();
@@ -190,6 +194,7 @@ export class IntercomClient extends EventEmitter {
190
194
  groups?: string[];
191
195
  }>();
192
196
  private pendingMemberships = new Map<string, { resolve: (groups: string[]) => void; reject: (error: Error) => void }>();
197
+ private pendingBrokerStatus = new Map<string, { resolve: (report: BrokerStatusReport) => void; reject: (error: Error) => void }>();
193
198
  private _groups = normalizeGroups();
194
199
  private pendingLiveWorkflowStageRoutes = new Map<string, { resolve(): void; reject(error: Error): void }>();
195
200
  private pendingStageRouteRegistrations = new Map<string, { resolve(): void; reject(error: Error): void }>();
@@ -201,6 +206,43 @@ export class IntercomClient extends EventEmitter {
201
206
  private pendingRestoringRecipientRequests = new Map<string, { resolve(): void; reject(error: Error): void }>();
202
207
  private disconnecting = false;
203
208
  private disconnectError: Error | null = null;
209
+ private healthCheck: { socket: net.Socket; result: Promise<Error | undefined> } | undefined;
210
+
211
+ private async requestTimeout(socket: net.Socket, error: Error): Promise<Error> {
212
+ if (this.socket !== socket || socket.destroyed) return this.disconnectError ?? new IntercomClientDisconnectedError();
213
+ if (this.healthCheck?.socket !== socket) {
214
+ const requestId = randomUUID();
215
+ const result = new Promise<Error | undefined>((resolve) => {
216
+ const finish = (failure?: Error) => {
217
+ clearTimeout(timer);
218
+ this.pendingGroupLists.delete(requestId);
219
+ resolve(failure);
220
+ };
221
+ const timer = setTimeout(() => finish(new IntercomBrokerTimeoutError("Broker health check timed out")), GROUP_REQUEST_TIMEOUT_MS);
222
+ this.pendingGroupLists.set(requestId, { resolve: () => finish(), reject: finish });
223
+ try {
224
+ writeMessage(socket, { type: "list_groups", requestId });
225
+ } catch (cause) {
226
+ finish(new IntercomClientDisconnectedError({ cause }));
227
+ }
228
+ });
229
+ this.healthCheck = { socket, result };
230
+ }
231
+ const check = this.healthCheck;
232
+ const failure = await check.result;
233
+ if (this.healthCheck === check) this.healthCheck = undefined;
234
+ if (failure === undefined) return error;
235
+ const reported = failure instanceof IntercomBrokerTimeoutError
236
+ ? new IntercomBrokerTimeoutError(error.message, { cause: failure })
237
+ : failure;
238
+ if (this.socket === socket && !this.disconnecting) {
239
+ this.disconnectError ??= reported;
240
+ this.failPending(this.disconnectError);
241
+ socket.destroy();
242
+ return this.disconnectError;
243
+ }
244
+ return reported;
245
+ }
204
246
 
205
247
  constructor(returnAddress: string = randomUUID(), options: IntercomClientOptions = {}) {
206
248
  super();
@@ -221,6 +263,7 @@ export class IntercomClient extends EventEmitter {
221
263
  for (const pending of this.pendingMemberships.values()) pending.reject(error);
222
264
  for (const pending of this.pendingLiveWorkflowStageRoutes.values()) pending.reject(error);
223
265
  for (const pending of this.pendingStageRouteRegistrations.values()) pending.reject(error);
266
+ for (const pending of this.pendingBrokerStatus.values()) pending.reject(error);
224
267
  this.pendingLists.clear();
225
268
  this.pendingGroupLists.clear();
226
269
  this.pendingSupervisorAuthorizations.clear();
@@ -229,6 +272,7 @@ export class IntercomClient extends EventEmitter {
229
272
  this.pendingMemberships.clear();
230
273
  this.pendingLiveWorkflowStageRoutes.clear();
231
274
  this.pendingStageRouteRegistrations.clear();
275
+ this.pendingBrokerStatus.clear();
232
276
  }
233
277
 
234
278
 
@@ -332,6 +376,7 @@ export class IntercomClient extends EventEmitter {
332
376
  this._supervisorSessionId = null;
333
377
  this.brokerAcknowledgesReceipt = false;
334
378
  this.brokerAcknowledgesPendingStageRoutes = false;
379
+ this.brokerAnswersStatus = false;
335
380
  this._messageSource = undefined;
336
381
  this.disconnectError = null;
337
382
  if (connectionEstablished && !wasDisconnecting) {
@@ -440,7 +485,8 @@ export class IntercomClient extends EventEmitter {
440
485
  if (typeof brokerMessage.sessionId !== "string"
441
486
  || (brokerMessage.supervisorSessionId !== undefined && typeof brokerMessage.supervisorSessionId !== "string")
442
487
  || (brokerMessage.acknowledgesReceipt !== undefined && brokerMessage.acknowledgesReceipt !== true)
443
- || (brokerMessage.acknowledgesPendingStageRoutes !== undefined && brokerMessage.acknowledgesPendingStageRoutes !== true)) {
488
+ || (brokerMessage.acknowledgesPendingStageRoutes !== undefined && brokerMessage.acknowledgesPendingStageRoutes !== true)
489
+ || (brokerMessage.brokerStatus !== undefined && brokerMessage.brokerStatus !== true)) {
444
490
  throw new Error("Invalid registered message");
445
491
  }
446
492
 
@@ -454,6 +500,7 @@ export class IntercomClient extends EventEmitter {
454
500
  // #3761: a broker from before receipts never echoes the capability and drops a client that sends `received`.
455
501
  this.brokerAcknowledgesReceipt = this.acknowledgesReceipt && brokerMessage.acknowledgesReceipt === true;
456
502
  this.brokerAcknowledgesPendingStageRoutes = brokerMessage.acknowledgesPendingStageRoutes === true;
503
+ this.brokerAnswersStatus = brokerMessage.brokerStatus === true;
457
504
  this.emit("_registered", { type: "registered", sessionId: brokerMessage.sessionId });
458
505
  break;
459
506
  }
@@ -539,6 +586,16 @@ export class IntercomClient extends EventEmitter {
539
586
  pending.resolve(groups as GroupSummary[]);
540
587
  break;
541
588
  }
589
+ case "broker_status": {
590
+ const { requestId } = brokerMessage;
591
+ const report = brokerMessage.report as JsonWireValue | undefined;
592
+ if (typeof requestId !== "string" || !isBrokerStatusReport(report)) throw new Error("Invalid broker_status message");
593
+ const pending = this.pendingBrokerStatus.get(requestId);
594
+ if (!pending) return;
595
+ this.pendingBrokerStatus.delete(requestId);
596
+ pending.resolve(report);
597
+ break;
598
+ }
542
599
  case "supervisor_authorized": {
543
600
  const { requestId, capability, supervisorSessionId, childName } = brokerMessage;
544
601
  if (typeof requestId !== "string" || typeof capability !== "string"
@@ -861,7 +918,7 @@ export class IntercomClient extends EventEmitter {
861
918
  const requestId = randomUUID();
862
919
  const timeout = setTimeout(() => {
863
920
  if (!this.pendingLists.delete(requestId)) return;
864
- reject(new Error("List sessions timeout"));
921
+ void this.requestTimeout(socket, new Error("List sessions timeout")).then(reject);
865
922
  }, 5000);
866
923
  this.pendingLists.set(requestId, {
867
924
  resolve: (directory) => {
@@ -893,7 +950,7 @@ export class IntercomClient extends EventEmitter {
893
950
  return new Promise((resolve, reject) => {
894
951
  const timeout = setTimeout(() => {
895
952
  if (!this.pendingGroupLists.delete(requestId)) return;
896
- reject(new Error("List groups timeout"));
953
+ void this.requestTimeout(socket, new Error("List groups timeout")).then(reject);
897
954
  }, GROUP_REQUEST_TIMEOUT_MS);
898
955
  this.pendingGroupLists.set(requestId, {
899
956
  resolve: (groups) => { clearTimeout(timeout); resolve(groups); },
@@ -909,6 +966,35 @@ export class IntercomClient extends EventEmitter {
909
966
  });
910
967
  }
911
968
 
969
+ /** The broker's own status, including multi-node routing; `undefined` from a broker older than `broker_status`. */
970
+ brokerStatus(): Promise<BrokerStatusReport | undefined> {
971
+ let socket: net.Socket;
972
+ try {
973
+ socket = this.requireActiveSocket();
974
+ } catch (error) {
975
+ return Promise.reject(toError(error));
976
+ }
977
+ if (!this.brokerAnswersStatus) return Promise.resolve(undefined);
978
+ const requestId = randomUUID();
979
+ return new Promise((resolve, reject) => {
980
+ const timeout = setTimeout(() => {
981
+ if (!this.pendingBrokerStatus.delete(requestId)) return;
982
+ reject(new Error("Broker status timeout"));
983
+ }, GROUP_REQUEST_TIMEOUT_MS);
984
+ this.pendingBrokerStatus.set(requestId, {
985
+ resolve: (report) => { clearTimeout(timeout); resolve(report); },
986
+ reject: (error) => { clearTimeout(timeout); reject(error); },
987
+ });
988
+ try {
989
+ writeMessage(socket, { type: "broker_status", requestId });
990
+ } catch (error) {
991
+ clearTimeout(timeout);
992
+ this.pendingBrokerStatus.delete(requestId);
993
+ reject(toError(error));
994
+ }
995
+ });
996
+ }
997
+
912
998
  joinGroup(group: string): Promise<string[]> {
913
999
  return this.updateMembership("join_group", group);
914
1000
  }
@@ -928,7 +1014,7 @@ export class IntercomClient extends EventEmitter {
928
1014
  return new Promise((resolve, reject) => {
929
1015
  const timeout = setTimeout(() => {
930
1016
  if (!this.pendingMemberships.delete(requestId)) return;
931
- reject(new Error("Membership update timeout"));
1017
+ void this.requestTimeout(socket, new Error("Membership update timeout")).then(reject);
932
1018
  }, GROUP_REQUEST_TIMEOUT_MS);
933
1019
  this.pendingMemberships.set(requestId, {
934
1020
  resolve: (groups) => { clearTimeout(timeout); resolve(groups); },
@@ -958,7 +1044,7 @@ export class IntercomClient extends EventEmitter {
958
1044
  const requestId = randomUUID();
959
1045
  const timeout = setTimeout(() => {
960
1046
  if (!this.pendingSupervisorAuthorizations.delete(requestId)) return;
961
- reject(new Error("Supervisor authorization timeout"));
1047
+ void this.requestTimeout(socket, new Error("Supervisor authorization timeout")).then(reject);
962
1048
  }, 5000);
963
1049
  this.pendingSupervisorAuthorizations.set(requestId, {
964
1050
  resolve: (authorization) => { clearTimeout(timeout); resolve(authorization); },
@@ -1020,7 +1106,7 @@ export class IntercomClient extends EventEmitter {
1020
1106
  return new Promise((resolve, reject) => {
1021
1107
  const timeout = setTimeout(() => {
1022
1108
  if (!this.pendingRestoringRecipientRequests.delete(requestId)) return;
1023
- reject(new Error("Restoring-recipient request timeout"));
1109
+ void this.requestTimeout(socket, new Error("Restoring-recipient request timeout")).then(reject);
1024
1110
  }, RESTORING_RECIPIENT_REQUEST_TIMEOUT_MS);
1025
1111
  this.pendingRestoringRecipientRequests.set(requestId, {
1026
1112
  resolve: () => { clearTimeout(timeout); resolve(); },
@@ -1088,7 +1174,7 @@ export class IntercomClient extends EventEmitter {
1088
1174
  const requestId = randomUUID();
1089
1175
  const timeout = setTimeout(() => {
1090
1176
  if (!this.pendingStageRouteRegistrations.delete(requestId)) return;
1091
- reject(new IntercomBrokerTimeoutError("Pending-stage route registration timeout"));
1177
+ void this.requestTimeout(socket, new IntercomBrokerTimeoutError("Pending-stage route registration timeout")).then(reject);
1092
1178
  }, PENDING_STAGE_ROUTE_ACK_TIMEOUT_MS);
1093
1179
  this.pendingStageRouteRegistrations.set(requestId, {
1094
1180
  resolve: () => {
@@ -1239,7 +1325,21 @@ export class IntercomClient extends EventEmitter {
1239
1325
  } catch (error) {
1240
1326
  return Promise.reject(toError(error));
1241
1327
  }
1242
- if (!acquired.owner) return acquired.attempt.promise;
1328
+ let result = this.sendResults.get(acquired.attempt.promise);
1329
+ if (result === undefined) {
1330
+ result = acquired.attempt.promise.catch(async (error: Error) => {
1331
+ if (error instanceof PendingSendTimeoutError) {
1332
+ try {
1333
+ throw await this.requestTimeout(socket, error);
1334
+ } finally {
1335
+ this.pendingSends.release(acquired.attempt);
1336
+ }
1337
+ }
1338
+ throw error;
1339
+ });
1340
+ this.sendResults.set(acquired.attempt.promise, result);
1341
+ }
1342
+ if (!acquired.owner) return result;
1243
1343
  const message: Message = {
1244
1344
  id: messageId,
1245
1345
  timestamp: Date.now(),
@@ -1271,7 +1371,7 @@ export class IntercomClient extends EventEmitter {
1271
1371
  }
1272
1372
  const clearReplyTarget = () => { this.pendingReplyTargets.delete(acquired.attempt.attemptId); };
1273
1373
  void acquired.attempt.promise.then(clearReplyTarget, clearReplyTarget);
1274
- return acquired.attempt.promise;
1374
+ return result;
1275
1375
  }
1276
1376
  updatePresence(updates: PresenceUpdates): boolean {
1277
1377
  if (this.disconnecting) {
@@ -1306,7 +1406,7 @@ export class IntercomClient extends EventEmitter {
1306
1406
  };
1307
1407
  const timeout = setTimeout(() => {
1308
1408
  if (!this.pendingPresence.delete(requestId)) return;
1309
- reject(new Error("Presence update timeout"));
1409
+ void this.requestTimeout(socket, new Error("Presence update timeout")).then(reject);
1310
1410
  }, PRESENCE_ACK_TIMEOUT_MS);
1311
1411
  this.pendingPresence.set(requestId, {
1312
1412
  resolve: wrappedResolve,
@@ -0,0 +1,225 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import net from "node:net";
4
+ import type { SqliteExecutor } from "@bastani/atomic/storage";
5
+ import type { BrokerStatusReport } from "../types.js";
6
+ import { describeMultiNodeStatus, isBrokerStatusReport } from "./broker-status.js";
7
+ import { createMessageReader, type JsonWireValue, writeMessage } from "./framing.js";
8
+ import { BROKER_LEASE_STALE_AFTER_MS } from "./multinode-state-store.js";
9
+ import { getBrokerPidPath, getBrokerSocketPath } from "./paths.js";
10
+ import {
11
+ describeSharedStorage,
12
+ readSharedStorageSetting,
13
+ sharedStorageDigest,
14
+ sharedStorageSettingSource,
15
+ } from "./shared-storage-setting.js";
16
+ import { readLiveBrokerNodes } from "./sql-lease-store.js";
17
+
18
+ /**
19
+ * Broker control for the Atomic CLI (`atomic intercom restart`, `atomic storage status`): it asks the agent
20
+ * directory's running broker about itself, or to shut down, over the broker's own socket, without registering a
21
+ * session. The CLI loads this module from the bundled Intercom package.
22
+ */
23
+
24
+ export { describeMultiNodeStatus };
25
+
26
+ /** How long a control request waits for its answer; a status report may wait up to 2 s for the shared database. */
27
+ export const BROKER_CONTROL_TIMEOUT_MS = 5_000;
28
+ /** How long `restartIntercomBroker` waits for the old broker to exit; its multi-node close is bounded at 2 s. */
29
+ export const BROKER_STOP_TIMEOUT_MS = 10_000;
30
+ /** How long `restartIntercomBroker` waits for a reconnecting session to start a new broker (first retry after 1 s). */
31
+ export const BROKER_RECONNECT_TIMEOUT_MS = 15_000;
32
+ const POLL_INTERVAL_MS = 100;
33
+
34
+ export type BrokerQuery =
35
+ | { readonly state: "stopped" }
36
+ | { readonly state: "running"; readonly report: BrokerStatusReport }
37
+ /** A broker older than `broker_status`: it answers on its socket but drops a connection that asks. */
38
+ | { readonly state: "legacy"; readonly pid?: number };
39
+
40
+ /** The shared storage a broker started now from this environment would read: what `broker_status` compares with. */
41
+ export interface ExpectedBrokerStorage {
42
+ readonly digest: string;
43
+ readonly source: "env" | "settings";
44
+ readonly adapter: string;
45
+ readonly prefix?: string;
46
+ }
47
+
48
+ export type RestartOutcome =
49
+ | { readonly status: "not-running" }
50
+ /** Stopped, and no broker came back: no session was connected, or none reconnected in time. */
51
+ | { readonly status: "stopped"; readonly previous: { readonly pid: number; readonly sessions?: number } }
52
+ | {
53
+ readonly status: "restarted";
54
+ readonly previous: { readonly pid: number; readonly sessions?: number };
55
+ readonly current: Exclude<BrokerQuery, { state: "stopped" }>;
56
+ };
57
+
58
+ type ControlReply = { readonly connected: false } | { readonly connected: true; readonly reply?: JsonWireValue };
59
+
60
+ function sleep(ms: number): Promise<void> {
61
+ return new Promise((resolve) => setTimeout(resolve, ms));
62
+ }
63
+
64
+ /** Send one control frame and wait for the reply of `replyType`; a broker that closes without one yields no reply. */
65
+ function controlRequest(
66
+ socketPath: string,
67
+ type: "broker_status" | "shutdown",
68
+ replyType: "broker_status" | "shutdown_ack",
69
+ timeoutMs: number,
70
+ ): Promise<ControlReply> {
71
+ return new Promise((resolve) => {
72
+ const requestId = randomUUID();
73
+ const socket = net.connect(socketPath);
74
+ let connected = false;
75
+ let settled = false;
76
+ const finish = (result: ControlReply): void => {
77
+ if (settled) return;
78
+ settled = true;
79
+ clearTimeout(timer);
80
+ socket.destroy();
81
+ resolve(result);
82
+ };
83
+ const timer = setTimeout(() => finish(connected ? { connected } : { connected: false }), timeoutMs);
84
+ socket.on("connect", () => {
85
+ connected = true;
86
+ writeMessage(socket, { type, requestId });
87
+ });
88
+ socket.on(
89
+ "data",
90
+ createMessageReader(
91
+ (message) => {
92
+ if (
93
+ typeof message === "object" &&
94
+ message !== null &&
95
+ !Array.isArray(message) &&
96
+ message.type === replyType &&
97
+ message.requestId === requestId
98
+ )
99
+ finish({ connected: true, reply: message });
100
+ },
101
+ () => finish({ connected }),
102
+ ),
103
+ );
104
+ socket.on("error", () => finish(connected ? { connected } : { connected: false }));
105
+ socket.on("close", () => finish(connected ? { connected } : { connected: false }));
106
+ });
107
+ }
108
+
109
+ function readBrokerPid(agentDir: string): number | undefined {
110
+ try {
111
+ const pid = Number.parseInt(readFileSync(getBrokerPidPath(agentDir), "utf8").trim(), 10);
112
+ return Number.isFinite(pid) ? pid : undefined;
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ function isProcessAlive(pid: number): boolean {
119
+ try {
120
+ process.kill(pid, 0);
121
+ return true;
122
+ } catch (error) {
123
+ return (error as NodeJS.ErrnoException).code === "EPERM";
124
+ }
125
+ }
126
+
127
+ /** Ask the agent directory's broker for its status. */
128
+ export async function queryIntercomBroker(
129
+ agentDir: string,
130
+ timeoutMs: number = BROKER_CONTROL_TIMEOUT_MS,
131
+ ): Promise<BrokerQuery> {
132
+ const answer = await controlRequest(
133
+ getBrokerSocketPath(process.platform, agentDir),
134
+ "broker_status",
135
+ "broker_status",
136
+ timeoutMs,
137
+ );
138
+ if (!answer.connected) return { state: "stopped" };
139
+ const reply = answer.reply;
140
+ if (typeof reply === "object" && reply !== null && !Array.isArray(reply) && isBrokerStatusReport(reply.report)) {
141
+ return { state: "running", report: reply.report };
142
+ }
143
+ const pid = readBrokerPid(agentDir);
144
+ // Windows can briefly accept a named-pipe connection after its broker has terminated.
145
+ if (pid !== undefined && !isProcessAlive(pid)) return { state: "stopped" };
146
+ return pid === undefined ? { state: "legacy" } : { state: "legacy", pid };
147
+ }
148
+
149
+ /** The shared selection a broker started from this environment and agent directory would read, if any. */
150
+ export function expectedBrokerStorage(
151
+ agentDir: string,
152
+ env: NodeJS.ProcessEnv = process.env,
153
+ ): ExpectedBrokerStorage | undefined {
154
+ const setting = readSharedStorageSetting(agentDir, env);
155
+ if (setting === undefined) return undefined;
156
+ return { digest: sharedStorageDigest(setting), source: sharedStorageSettingSource(env), ...describeSharedStorage(setting) };
157
+ }
158
+
159
+ /** Host names of the brokers live in a shared database, without creating Intercom's tables. */
160
+ export function readLiveBrokers(database: SqliteExecutor, prefix: string | undefined): Promise<string[] | undefined> {
161
+ return readLiveBrokerNodes(database, prefix, BROKER_LEASE_STALE_AFTER_MS);
162
+ }
163
+
164
+ export interface RestartOptions {
165
+ readonly stopTimeoutMs?: number;
166
+ readonly reconnectTimeoutMs?: number;
167
+ /** Progress lines, for example `Stopping the Intercom broker (pid 123, 2 sessions)`. */
168
+ readonly onProgress?: (line: string) => void;
169
+ }
170
+
171
+ /**
172
+ * Restart the agent directory's broker so it reads the current storage selection: ask it to shut down (a broker
173
+ * older than this request gets SIGTERM, which it handles the same way), wait for it to exit, then wait for its
174
+ * sessions to reconnect, which starts a new broker. Without connected sessions the next session starts one.
175
+ */
176
+ export async function restartIntercomBroker(agentDir: string, options: RestartOptions = {}): Promise<RestartOutcome> {
177
+ const stopTimeoutMs = options.stopTimeoutMs ?? BROKER_STOP_TIMEOUT_MS;
178
+ const reconnectTimeoutMs = options.reconnectTimeoutMs ?? BROKER_RECONNECT_TIMEOUT_MS;
179
+ const before = await queryIntercomBroker(agentDir);
180
+ if (before.state === "stopped") return { status: "not-running" };
181
+ const pid = before.state === "running" ? before.report.pid : before.pid;
182
+ if (pid === undefined) {
183
+ throw new Error(`The Intercom broker answers, but ${getBrokerPidPath(agentDir)} does not name its process.`);
184
+ }
185
+ const sessions = before.state === "running" ? before.report.sessions : undefined;
186
+ const previous = sessions === undefined ? { pid } : { pid, sessions };
187
+ options.onProgress?.(
188
+ `Stopping the Intercom broker (pid ${pid}${sessions === undefined ? "" : `, ${sessions} session${sessions === 1 ? "" : "s"} connected`}).`,
189
+ );
190
+ const answer =
191
+ before.state === "running"
192
+ ? await controlRequest(
193
+ getBrokerSocketPath(process.platform, agentDir),
194
+ "shutdown",
195
+ "shutdown_ack",
196
+ BROKER_CONTROL_TIMEOUT_MS,
197
+ )
198
+ : undefined;
199
+ if (answer === undefined || !answer.connected || answer.reply === undefined) {
200
+ try {
201
+ process.kill(pid, "SIGTERM");
202
+ } catch (error) {
203
+ if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
204
+ }
205
+ }
206
+ const stopDeadline = Date.now() + stopTimeoutMs;
207
+ while (isProcessAlive(pid)) {
208
+ if (Date.now() >= stopDeadline) {
209
+ throw new Error(`The Intercom broker (pid ${pid}) did not exit within ${Math.round(stopTimeoutMs / 1000)} s.`);
210
+ }
211
+ await sleep(POLL_INTERVAL_MS);
212
+ }
213
+ if (sessions === 0) return { status: "stopped", previous };
214
+ options.onProgress?.("Stopped. Waiting for its sessions to reconnect and start a new broker.");
215
+ const reconnectDeadline = Date.now() + reconnectTimeoutMs;
216
+ let current: BrokerQuery = { state: "stopped" };
217
+ while (Date.now() < reconnectDeadline) {
218
+ current = await queryIntercomBroker(agentDir);
219
+ const settled =
220
+ current.state === "legacy" || (current.state === "running" && current.report.multiNode.status !== "starting");
221
+ if (settled) break;
222
+ await sleep(POLL_INTERVAL_MS * 2);
223
+ }
224
+ return current.state === "stopped" ? { status: "stopped", previous } : { status: "restarted", previous, current };
225
+ }
@@ -1,7 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { readFileSync } from "node:fs";
3
2
  import { hostname } from "node:os";
4
- import { join } from "node:path";
5
3
  import { normalizeGroup } from "../group.js";
6
4
  import { isAgentRecipient, NON_AGENT_RECIPIENT_REFUSAL, TERMINAL_CHILD_ASK_REFUSAL } from "../recipient-purpose.js";
7
5
  import { DELIVERED_MESSAGE_TTL_MS, RECIPIENT_RECEIPT_TIMEOUT_MS } from "../retry-policy.js";
@@ -34,7 +32,6 @@ import {
34
32
  type RestoringReservationLease,
35
33
  reservationKeyFor,
36
34
  } from "./multinode-state-store.js";
37
- import { BROKER_STORAGE_ENV } from "./paths.js";
38
35
  import type { PendingQuestionIndex } from "./pending-question-index.js";
39
36
  import { UNCERTAIN_RECEIPT_REASON } from "./recipient-receipts.js";
40
37
  import { RESTORING_RECIPIENT_HOLD_MS, restoringRecipientRetryReason } from "./restoring-recipients.js";
@@ -49,6 +46,8 @@ import type { BrokerOutboundFrame } from "./socket-writes.js";
49
46
  import type { SupervisorChannelCache } from "./supervisor-channel.js";
50
47
  import type { WakeUpBus, WakeUpEvent, WakeUpSubscription } from "./wake-up-bus.js";
51
48
 
49
+ export { readSharedStorageSetting } from "./shared-storage-setting.js";
50
+
52
51
  /**
53
52
  * Multi-node S5 (specs/2026-10-19-intercom-multi-node.md §4–§8, §14): routes sends to sessions hosted by other
54
53
  * brokers through the shared state, and delivers the shared outbox to this broker's own sessions. Local delivery
@@ -91,39 +90,6 @@ const MAX_SUPERSEDED_HOPS = 8;
91
90
  export const KNOWN_SESSION_REFRESH_INTERVAL_MS = 60 * 60 * 1000;
92
91
  const TARGET_GROUP_REFUSAL = "Target session is in a different intercom group";
93
92
 
94
- /**
95
- * The raw shared `storage` selection, read without the host package so the broker stays self-contained: the
96
- * selection the launching process handed this broker in `ATOMIC_STORAGE` (its `--storage`, `--storage-file` or own
97
- * `ATOMIC_STORAGE`), otherwise the top-level `storage` value of the agent directory's settings. Relative paths in
98
- * either resolve against the agent directory's parent, as for sessions and the journal. `undefined` leaves
99
- * multi-node routing off: no selection, a per-process selection that is not a JSON object (the launching process
100
- * validated it; settings are never consulted instead), and the selections the storage layer reads as in-memory
101
- * (`memory: true` without a path, or `path: ":memory:"`), which no other broker can share. `path: ""` is not one of
102
- * them: it selects the default storage file.
103
- */
104
- export function readSharedStorageSetting(
105
- agentDir: string,
106
- env: NodeJS.ProcessEnv = process.env,
107
- ): Record<string, unknown> | undefined {
108
- const processSelection = env[BROKER_STORAGE_ENV];
109
- let parsed: unknown;
110
- try {
111
- parsed =
112
- processSelection === undefined || processSelection.length === 0
113
- ? (JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf8")) as Record<string, unknown>).storage
114
- : JSON.parse(processSelection);
115
- } catch {
116
- return undefined;
117
- }
118
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
119
- const selection = parsed as Record<string, unknown>;
120
- if (selection.adapter === undefined) {
121
- if (selection.memory === true && !selection.path) return undefined;
122
- if (selection.path === ":memory:") return undefined;
123
- }
124
- return selection;
125
- }
126
-
127
93
  /** What the broker uses of `open-broker-state.ts`, which it loads lazily (replaceable in tests). */
128
94
  export interface BrokerStateOpener<Source> {
129
95
  resolveBrokerStateSource(
@@ -139,9 +105,9 @@ export type MultiNodeStartup<Source> =
139
105
 
140
106
  /**
141
107
  * Decide whether a top-level `storage` value gives this broker a shared database. Multi-node routing is off, and
142
- * Intercom behaves exactly as a single-node broker, unless it does: also when the host storage module cannot be
143
- * loaded (a standalone binary) or the selection cannot be resolved to a database. Only a selection that resolved to a
144
- * database whose connection then fails makes cross-node sends fail explicitly.
108
+ * Intercom behaves exactly as a single-node broker, unless it does: also when the host storage module or the adapter
109
+ * cannot be loaded, or the selection cannot be resolved to a database. Only a selection that resolved to a database
110
+ * whose connection then fails makes cross-node sends fail explicitly.
145
111
  */
146
112
  export async function resolveMultiNodeStartup<Source>(
147
113
  setting: Record<string, unknown>,
@@ -490,6 +456,33 @@ export class MultiNodeRouter {
490
456
  });
491
457
  }
492
458
 
459
+ /**
460
+ * The host names of the other brokers whose membership lease is live, for status reports. `undefined` when the
461
+ * database fails or does not answer within `REMOTE_SESSIONS_BOUND_MS`.
462
+ */
463
+ peerBrokers(): Promise<string[] | undefined> {
464
+ if (this.closed) return Promise.resolve(undefined);
465
+ return new Promise((resolve) => {
466
+ const bound = setTimeout(() => resolve(undefined), REMOTE_SESSIONS_BOUND_MS);
467
+ bound.unref?.();
468
+ void this.state.leases.listLive("broker", BROKER_LEASE_STALE_AFTER_MS).then(
469
+ (live) => {
470
+ clearTimeout(bound);
471
+ resolve(
472
+ live.flatMap((lease) =>
473
+ lease.subject.kind === "broker" && lease.holderId !== this.brokerId ? [lease.subject.nodeId] : [],
474
+ ),
475
+ );
476
+ },
477
+ (error: Error) => {
478
+ clearTimeout(bound);
479
+ this.report(error);
480
+ resolve(undefined);
481
+ },
482
+ );
483
+ });
484
+ }
485
+
493
486
  // ---------------------------------------------------------------------------------------------------------
494
487
  // Restoring reservations (§5.7): mirror this broker's reservations so senders on any host hold their sends.
495
488
 
@@ -75,6 +75,10 @@ export function getBrokerLogPath(agentDir: string = getAgentDir()): string {
75
75
  return join(getIntercomDirPath(agentDir), "broker.log");
76
76
  }
77
77
 
78
+ export function getBrokerWatchdogLogPath(agentDir: string = getAgentDir()): string {
79
+ return join(getIntercomDirPath(agentDir), "broker-watchdog.log");
80
+ }
81
+
78
82
  export function getBrokerSocketPath(
79
83
  platform: NodeJS.Platform = process.platform,
80
84
  agentDir: string = getAgentDir(),