@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,55 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.42-alpha.1] - 2026-10-23
6
+
7
+ Sessions, workflow runs, and Intercom can now share one libSQL / Turso database across machines, and provider rate limits are respected across every request in a process.
8
+
9
+ - **Shared storage across machines:** install `@bastani/atomic-storage-libsql`, join a machine with `atomic storage join <url> --token-file <path>`, and check the result with `atomic storage status`. Atomic packages can ship storage adapters, and `createSqliteDatabase()` handles the facade rules when you write your own ([#3856](https://github.com/bastani-inc/atomic/issues/3856), [#3857](https://github.com/bastani-inc/atomic/issues/3857))
10
+ - **Intercom you can inspect and restart:** `atomic intercom status` and `atomic intercom restart` show and refresh the broker's storage selection and multi-node routing, and a broker that stops answering is replaced after about a minute instead of hanging every session ([#3857](https://github.com/bastani-inc/atomic/issues/3857), [#3862](https://github.com/bastani-inc/atomic/issues/3862))
11
+ - **Rate limits handled across requests:** `configureProviderRequestBudget()` sets process-wide request budgets, 429 cooldowns are shared between model requests, and workflow model chains wait for the earliest eligible account instead of exhausting it ([#3864](https://github.com/bastani-inc/atomic/issues/3864))
12
+ - **Admission limits and local workflow pools:** SDK hosts can share session and command limits with `resourceAdmission`, and workflows can run child subtrees in local worker processes ([#3869](https://github.com/bastani-inc/atomic/issues/3869))
13
+ - **More responsive sessions and workflows:** Ctrl+C and queue clearing act without waiting for a busy engine, workflow stages reload resources four at a time, and standalone `run()` gives stages Intercom without an interactive host ([#3861](https://github.com/bastani-inc/atomic/issues/3861), [#3863](https://github.com/bastani-inc/atomic/issues/3863), [#3865](https://github.com/bastani-inc/atomic/issues/3865))
14
+ - **Login, export, and search fixes:** device-code `/login` waits for Enter before opening the browser, HTML exports show billed costs and the session's own system prompt, and `/resume` search ranks name matches first ([pi #9282](https://github.com/earendil-works/pi/issues/9282), [pi #9980](https://github.com/earendil-works/pi/issues/9980), [pi #11261](https://github.com/earendil-works/pi/issues/11261), [pi #11220](https://github.com/earendil-works/pi/issues/11220))
15
+
16
+ ### Added
17
+
18
+ - Markdown transformers registered with `pi.registerMarkdownTransformer()` now receive `messageId` and `timestamp` in their context when a message is rendered from the saved session, so an extension can tie its output to a session entry. Both are `undefined` for live messages. See [registerMarkdownTransformer](docs/extensions/api-reference.md#pi-registermarkdowntransformer-transformer)
19
+ - Added `configureProviderRequestBudget(provider, { maxRequests, intervalMs })` for process-wide rolling request budgets shared across SDK model runtimes. Model requests also share provider cooldowns after 429 responses. Separate configured provider IDs remain independent ([#3864](https://github.com/bastani-inc/atomic/issues/3864)).
20
+ - Storage adapter modules can import `@bastani/atomic/storage` and `@bastani/atomic` without an installed or linked copy of Atomic. They get the running Atomic's own modules, as extensions do, so Atomic recognizes the storage errors they throw ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
21
+ - `createSqliteDatabase()` in `@bastani/atomic/storage` turns a SQLite-compatible driver's calls into a `SqliteDatabase` that applies the facade rules for you: call-order queueing, transaction handle lifetime, NUL rejection, integer and blob normalization, and typed errors with credentials redacted. `redactSecrets()` builds the credential redactor. The libSQL/Turso example in [Storage](docs/storage.md#bring-your-own-driver) now uses it and is a complete adapter ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
22
+ - Atomic packages can provide storage adapters with a `storageAdapters` manifest entry, and `storage.adapter` can name one by package (`<package>` or `<package>/<name>`), so `atomic install npm:<adapter-package>` installs the adapter together with its driver. See [Adapters from Atomic packages](docs/storage.md#adapters-from-atomic-packages) ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
23
+ - `@bastani/atomic-storage-libsql` is a first-party libSQL / Turso storage adapter package. Install it with `atomic install npm:@bastani/atomic-storage-libsql` and select it with `"storage": { "adapter": "@bastani/atomic-storage-libsql" }` to share sessions, workflow runs, and Intercom routing across machines. See [Turso and libSQL](docs/storage.md#turso-and-libsql) ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
24
+ - `atomic storage status` shows, for sessions, workflow runs, and Intercom, the selected storage and where the selection comes from (environment variable, project or global settings, or default), the outranked selections, whether the adapter loads, and whether the database answers a `SELECT 1`, without creating anything. It also shows whether the running Intercom broker uses the current selection, its multi-node routing state, and the brokers live in the shared database. `--json` prints the same report. See [Checking the selected storage](docs/storage.md#checking-the-selected-storage) ([#3856](https://github.com/bastani-inc/atomic/issues/3856), [#3857](https://github.com/bastani-inc/atomic/issues/3857))
25
+ - `atomic storage join <url> --token-file <path>` joins this machine to a shared libSQL / Turso database in one step: it checks the token and the connection, stores the trimmed token in the agent directory with mode 600, writes only the global `storage` setting, creates no tables, and lists the environment variables and settings that override the new selection. It refuses a URL that carries the token itself, so a credential never reaches settings or output. When `@bastani/atomic-storage-libsql` is not installed, it prints the install command. See [Joining a machine to a shared database](docs/storage.md#joining-a-machine-to-a-shared-database) ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
26
+ - `atomic intercom restart` replaces the running Intercom broker so it reads a changed storage selection; connected sessions reconnect to the new broker. `atomic intercom status` shows the broker, whether it uses the current selection, and its multi-node routing state ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
27
+ - Interactive and print sessions warn at startup when the shared `storage` selection names an adapter that the running Atomic cannot load, naming the adapter and the cause, instead of Intercom multi-node routing staying off silently ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
28
+ - SDK hosts can share admission limits through `resourceAdmission` and `withResourceAdmission()`. Session and managed-command permits remain held until cleanup, including background commands. Delegated sessions inherit the same service and request immediate admission or rejection rather than waiting on a permit held by their parent. See [Shared resource admission](docs/sdk.md#shared-resource-admission) ([#3869](https://github.com/bastani-inc/atomic/issues/3869)).
29
+
30
+ ### Changed
31
+
32
+ - The standalone `atomic` binaries are now built with Bun 1.4.4, which includes upstream security fixes in the embedded runtime
33
+ - Device-code `/login` flows (Kimi Code, GitHub Copilot, Meta, and extension providers) no longer open the browser on their own. Press Enter to open the verification URL and copy the code, or log in manually. Providers can turn off either action with `openBrowserOnConfirm: false` or `copyCodeOnConfirm: false`. See [Provider OAuth](docs/custom-provider/oauth.md#oauth-support) ([pi #9282](https://github.com/earendil-works/pi/issues/9282))
34
+ - A `{ "file": ... }` storage secret now drops one trailing line ending (`\n` or `\r\n`), so a token saved with `turso db tokens create <db> > token` works without trimming it first. Other whitespace is still kept ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
35
+
36
+ ### Fixed
37
+
38
+ - HTML session exports now show the recorded total cost of each response, such as OpenRouter's billed cost, instead of the sum of its catalog-estimated components. Sessions recorded before totals were stored still use the component sum ([pi #9980](https://github.com/earendil-works/pi/issues/9980))
39
+ - On macOS, cancelling a running bash command no longer lets the next command in a `a; b` sequence start while the shell is being killed: the process group is stopped before it is killed ([pi #11252](https://github.com/earendil-works/pi/issues/11252))
40
+ - HTML session exports, including `atomic --export`, now show the system prompt and available tools from the session's first system message, and later system messages as named prompt-section and tool updates. Sessions without system entries still show the prompt and tools from the agent state ([pi #11261](https://github.com/earendil-works/pi/issues/11261))
41
+ - HTML session exports of very long sessions no longer fail to render the session tree with a call-stack overflow ([pi #11225](https://github.com/earendil-works/pi/issues/11225))
42
+ - `pi.exec()` now reports why a command could not be started (for example `ENOENT` for a missing executable) in `stderr` instead of returning an empty `stderr` with exit code 1 ([pi #11085](https://github.com/earendil-works/pi/issues/11085))
43
+ - `/resume` search matches the session name, summary, id, messages and working directory separately. A name match ranks first, then text found in the id, summary, messages or directory, then characters scattered through the name, summary or directory. Matches deep in a long transcript are no longer ranked last, and scattered characters in session ids or message text no longer produce noise matches ([pi #11220](https://github.com/earendil-works/pi/issues/11220))
44
+ - Updated the bundled `ip-address` dependency to 10.7.4, which fixes a quadratic-time zone-identifier match in IPv6 address parsing that a single long string could use to stall the process ([GHSA-rxp7-6hxx-x8q5](https://github.com/advisories/GHSA-rxp7-6hxx-x8q5))
45
+ - Provider request-budget waits no longer consume OpenAI, Anthropic, Mistral, or explicit Codex SSE HTTP timeouts, and successive Anthropic federation-authenticated requests keep reusing their cached token. Cancelling a queued model request reports an aborted result without dispatching it ([#3864](https://github.com/bastani-inc/atomic/issues/3864)).
46
+ - Cancelling or clearing queued messages no longer waits for a busy interactive engine to finish draining its output before acting. Cancelled prompts and shell commands waiting to start do not run later when output resumes. If queue clearing or cancellation of chat, shell commands, compaction, or retries stalls or fails, Ctrl+C can still replace the engine even while it continues sending heartbeats, including controls sent while a replacement initializes; an earlier failed queue pause no longer prevents an abort from being sent ([#3865](https://github.com/bastani-inc/atomic/issues/3865))
47
+ - The standalone binary's Intercom broker now runs multi-node routing with a shared `storage` selection and loads storage adapters, as the npm installation does ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
48
+ - `atomic storage join` keeps previous credentials intact when saving settings fails, rejects relative `file:` URLs that would select different databases from different directories, and rejects credentials embedded in `file:` URLs before printing or saving them. New joins use separate private token files, preserving existing sessions' credentials. A successful join no longer reports failure when malformed project settings prevent the final override report: it warns that storage was saved and asks you to fix the settings before running status ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
49
+ - `atomic storage status` reports settings load failures, opens local SQLite files (including adapter `file:` URLs) read-only without creating missing databases or changing journal mode, and bounds connection cleanup even when a query stalls ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
50
+ - `createSqliteDatabase()` preserves callback and rollback failures when transaction cleanup also fails ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
51
+ - Package-name storage adapters now honor packages listed in legacy global settings, while explicit agent directories remain isolated ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
52
+ - On macOS, pausing a managed command no longer causes it to be reported as completed and terminated ([#3869](https://github.com/bastani-inc/atomic/issues/3869)).
53
+
5
54
  ## [0.9.41] - 2026-10-22
6
55
 
7
56
  Workflows stay contained when they fail, classifiers read images, and usage totals say where their cost came from.
@@ -4,6 +4,19 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.42-alpha.1] - 2026-10-23
8
+
9
+ ### Added
10
+
11
+ - `intercom({ action: "status" })` reports the broker's multi-node routing state, for example `Multi-node routing: on (adapter @bastani/atomic-storage-libsql, prefix none), 1 peer broker live (build-host)`, or why it is off or unavailable ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
12
+ - The broker writes a positive line to `broker.log` when it starts: `Intercom multi-node routing is on (adapter <adapter>, prefix <prefix>)`, or `Intercom multi-node routing is off: <reason>`, also for an absent, in-memory, or domain-only selection, so a log without errors is no longer ambiguous ([#3856](https://github.com/bastani-inc/atomic/issues/3856), [#3857](https://github.com/bastani-inc/atomic/issues/3857))
13
+ - The broker answers status and shutdown requests from `atomic storage status`, `atomic intercom status`, and `atomic intercom restart`: its storage selection, sessions, multi-node state, and the other brokers live in the shared database. A restart lets connected sessions reconnect to a broker that reads the current storage selection, without closing them ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
14
+
15
+ ### Fixed
16
+
17
+ - Fixed Intercom remaining stuck on a broker that accepts connections but stops answering. New brokers monitor their own request handling and terminate after about a minute of failed health checks so clients can start one replacement. Timed-out client requests also check connection health before entering reconnect recovery, without restarting a responsive broker. Identical in-flight sends keep sharing one result through the health check. A bounded previous broker log and the latest watchdog diagnosis survive replacement ([#3862](https://github.com/bastani-inc/atomic/issues/3862)).
18
+ - Restarting an older broker on Windows no longer mistakes a lingering named pipe for a restarted broker after its process has exited ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
19
+
7
20
  ## [0.9.41] - 2026-10-22
8
21
 
9
22
  ### Changed
@@ -389,7 +389,7 @@ When multiple live agent stages share a name, `ask` refuses the ambiguous target
389
389
 
390
390
  `contact_supervisor` decisions and interviews are deliberately exclusive per child: one supervisor wait may coexist with ordinary peer asks, but a second concurrent supervisor wait receives `Already waiting for a supervisor reply`. Claimed foreground parent handoffs remain first-claim-wins and do not allocate a waiter. Multiple children can contact the same parent independently because inbound asks and handoffs are keyed by child/message identity. Mutual peer asks are supported, but each side must process inbound work to reply; the per-waiter timeout remains the deadlock backstop.
391
391
 
392
- **`status`** — Shows connection status, session ID, every current membership, and the total visible-session count. A `group` filter remains a read-only peek.
392
+ **`status`** — Shows connection status, session ID, every current membership, and the total visible-session count. It also reports the broker's multi-node routing state on a `Multi-node routing:` line: `on (adapter <adapter>, prefix <prefix>)` with the number of other brokers live in the shared database, or why routing is off or unavailable; `details.multiNode` carries the same fields. A broker that predates status reporting shows `unknown`, and `atomic intercom restart` replaces it. A `group` filter remains a read-only peek.
393
393
 
394
394
  ## Keyboard Shortcuts
395
395
 
@@ -463,6 +463,8 @@ graph TB
463
463
 
464
464
  The broker is a standalone TypeScript process that manages session registration and message routing. It auto-spawns when the first session that invokes Intercom needs it and exits after 5 seconds when it last has no registered sessions, including brokers that never received a connection and sockets that close before register. Clients reconnect automatically if the broker disappears and later comes back. A failed reconnect schedules the next attempt on a bounded backoff (1s, 2s, 5s, 10s, then 30s) once it releases reconnect ownership, so a transient failure never leaves a live session with nothing owning recovery; a failed explicit tool or overlay connection still returns its error to the caller and leaves the retry behind. A reconnect that fails after the broker already accepted it closes that connection first, so a session never appears twice in `intercom list`.
465
465
 
466
+ A broker-owned watchdog checks the local request path independently of the broker's main thread. After about a minute without a valid response from its own broker instance, it terminates its own process; clients reconnect through the existing spawn lock. It never signals a PID read from `broker.pid`. A timed-out client request shares one connection-health check with other simultaneous timeouts on that connection, and invalidates a silent connection. A responsive broker is left running. Brokers already running from an older version gain the watchdog only after a restart; an OS-suspended process cannot run it.
467
+
466
468
  Each `send`, `ask`, or `reply` invocation retries typed recoverable disconnects internally up to three times, after delays of 1, 2, and 5 seconds. Retries keep the same message ID, exact arguments, attachment order and presence, and reply route. The model does not supply or receive a retry token; existing integrations must remove `retryToken`, which is now refused without sending. Every new invocation remains a fresh operation, even with identical text. Confirmation happens once, and neither retries nor renewed reply waits extend the original 11-minute operation deadline.
467
469
 
468
470
  Lazy initialization before delivery has a separate limit of three reconnect retries on the same schedule. If that phase stops, the tool reports `outcome: "not_sent"`. The initialization wrapper never re-executes a delivery that has already started.
@@ -494,7 +496,9 @@ Runtime files live under the active agent directory. Atomic defaults to `~/.atom
494
496
  - `delivered-messages.key` — random owner-only HMAC key paired with the authority database
495
497
  - `broker.spawn.lock` — Lock used to avoid duplicate auto-spawns. It records the spawner and its broker, and is held until the broker answers or the processes it names are gone, with a 60 second backstop. On Windows, where the hidden launcher does not expose the broker process, it ends with the spawner's 5 second readiness wait, and a later spawn that finds `broker.log` still held by a starting broker waits for that broker instead of starting another
496
498
  - `broker.sock.replace.lock` — Short-lived lock a starting broker holds while it re-checks and replaces a stale socket file, so two brokers never both replace it
497
- - `broker.log` — Broker stderr, truncated on every spawn and capped at 8 KiB by the broker itself
499
+ - `broker.log` — Broker stderr, capped at 8 KiB by the broker itself. It records whether multi-node routing is on (`Intercom multi-node routing is on (adapter <adapter>, prefix <prefix>)`) or off and why
500
+ - `broker.log.previous` — The bounded tail of the previous nonempty broker log, preserved on spawn
501
+ - `broker-watchdog.log` — The latest watchdog termination's time, PID, instance, and reason; kept across spawns
498
502
  - `config.json` — User configuration
499
503
 
500
504
  The broker is a detached subprocess and never inherits the host's extension-loader module aliases, so every module reachable from `broker/broker.ts` resolves to Node built-ins or Intercom's own files. Its stderr is captured to `broker.log` through an already-open file descriptor rather than a pipe, because a pipe to a process that outlives its parent breaks once the parent exits. Startup failures — both an early exit and a readiness timeout — quote the log path and a bounded tail of that output.
@@ -505,7 +509,7 @@ The broker caps the file from the inside, since nothing on the parent side can b
505
509
 
506
510
  **Local IPC instead of TCP.** Same-machine only by design. `pi-intercom` uses Unix sockets on macOS/Linux and a named pipe on Windows, which keeps setup simple and avoids port management.
507
511
 
508
- **Auto-spawn with file lock.** The broker starts on first connection and exits after 5 seconds idle. There is no daemon to manage. A spawn lock file, keyed by PID and timestamp, prevents duplicate brokers when multiple sessions start at once. A broker that starts while another broker already answers on the socket exits instead of taking the socket over; it replaces only a stale socket file. A broker whose socket path comes to point at another broker shuts down, and its sessions reconnect to the broker that owns the path.
512
+ **Auto-spawn with file lock.** The broker starts on first connection and exits after 5 seconds idle. There is no daemon to manage. A spawn lock file, keyed by PID and timestamp, prevents duplicate brokers when multiple sessions start at once. A broker that starts while another broker already answers on the socket exits instead of taking the socket over; it replaces only a stale socket file. A broker whose socket path comes to point at another broker shuts down, and its sessions reconnect to the broker that owns the path. `atomic intercom restart` asks the broker to shut down over its socket (a `shutdown` request, accepted before registration like `broker_status`), so its sessions reconnect to a broker that reads the current storage selection.
509
513
 
510
514
  **`ask` stays client-side.** The broker still routes plain messages; it does not have a special request/response mode for `ask`. The client waits for a matching reply before it triggers a new turn, then returns that reply as the tool result. Reply hints make that flow practical by showing the recipient the exact `send` call to use. Separately, `list` / `sessions` now carry a `requestId` so a delayed session-list reply cannot be mistaken for a newer one.
511
515
 
@@ -0,0 +1,48 @@
1
+ import type { BrokerMultiNodeStatus, BrokerStatusReport } from "../types.js";
2
+ import type { JsonWireValue } from "./framing.js";
3
+
4
+ type WireObject = { [key: string]: JsonWireValue };
5
+
6
+ function isObject(value: JsonWireValue | undefined): value is WireObject {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+
10
+ function isOptionalString(value: JsonWireValue | undefined): boolean {
11
+ return value === undefined || typeof value === "string";
12
+ }
13
+
14
+ /** Whether a `broker_status` frame's `report` has the shape this version reads. */
15
+ export function isBrokerStatusReport(value: JsonWireValue | undefined): value is WireObject & BrokerStatusReport {
16
+ if (!isObject(value) || typeof value.pid !== "number" || typeof value.sessions !== "number") return false;
17
+ const { storage, multiNode } = value;
18
+ if (
19
+ storage !== undefined &&
20
+ (!isObject(storage) ||
21
+ typeof storage.digest !== "string" ||
22
+ (storage.source !== "env" && storage.source !== "settings"))
23
+ )
24
+ return false;
25
+ return (
26
+ isObject(multiNode) &&
27
+ (multiNode.status === "off" ||
28
+ multiNode.status === "starting" ||
29
+ multiNode.status === "on" ||
30
+ multiNode.status === "unavailable") &&
31
+ isOptionalString(multiNode.reason) &&
32
+ isOptionalString(multiNode.adapter) &&
33
+ isOptionalString(multiNode.prefix) &&
34
+ (multiNode.peers === undefined ||
35
+ (Array.isArray(multiNode.peers) && multiNode.peers.every((peer) => typeof peer === "string")))
36
+ );
37
+ }
38
+
39
+ /** One line for a broker's multi-node routing state, e.g. `on (adapter x, prefix none), 1 peer broker live (host-b)`. */
40
+ export function describeMultiNodeStatus(multiNode: BrokerMultiNodeStatus): string {
41
+ const storage = multiNode.adapter === undefined ? "" : ` (adapter ${multiNode.adapter}, prefix ${multiNode.prefix ?? "none"})`;
42
+ if (multiNode.status === "off" || multiNode.status === "unavailable") {
43
+ return `${multiNode.status}${storage}${multiNode.reason === undefined ? "" : `: ${multiNode.reason}`}`;
44
+ }
45
+ if (multiNode.status === "starting" || multiNode.peers === undefined) return `${multiNode.status}${storage}`;
46
+ const count = `${multiNode.peers.length} peer broker${multiNode.peers.length === 1 ? "" : "s"} live`;
47
+ return `on${storage}, ${count}${multiNode.peers.length === 0 ? "" : ` (${[...new Set(multiNode.peers)].join(", ")})`}`;
48
+ }
@@ -17,17 +17,21 @@ import {
17
17
  } from "./socket-writes.js";
18
18
  import { isSocketAnswering } from "./socket-liveness.js";
19
19
  import { acquireSocketReplacementLock } from "./socket-replacement-lock.js";
20
+ import { startBrokerWatchdog } from "./watchdog.js";
20
21
  import {
21
22
  getBrokerDeliveredMessagesPath,
22
23
  getBrokerPidPath,
24
+ getBrokerWatchdogLogPath,
23
25
  getBrokerSocketPath,
24
26
  getBrokerSocketReplacementLockPath,
25
27
  getIntercomDirPath,
26
28
  } from "./paths.js";
29
+ import { describeSharedStorage, sharedStorageDigest, sharedStorageSettingSource } from "./shared-storage-setting.js";
27
30
  import type {
28
31
  SessionInfo,
29
32
  Message,
30
33
  BrokerMessage,
34
+ BrokerStatusReport,
31
35
  SupervisorRegistration,
32
36
  WorkflowStageRosterAnnouncement,
33
37
  WorkflowStageRosterEntry,
@@ -86,6 +90,8 @@ import {
86
90
 
87
91
  const INTERCOM_DIR = getIntercomDirPath();
88
92
  const SOCKET_PATH = getBrokerSocketPath();
93
+ const BROKER_IDLE_TIMEOUT_MS = 5_000;
94
+ const UNCLASSIFIED_CONNECTION_RECHECK_MS = 100;
89
95
  const PID_PATH = getBrokerPidPath();
90
96
  const SOCKET_REPLACEMENT_LOCK_PATH = getBrokerSocketReplacementLockPath();
91
97
 
@@ -94,6 +100,13 @@ const LISTEN_ATTEMPTS = 5;
94
100
  const LISTEN_RETRY_DELAY_MS = 100;
95
101
  const SOCKET_OWNERSHIP_CHECK_MS = 1_000;
96
102
  const MULTI_NODE_CLOSE_BOUND_MS = 2_000;
103
+ const NO_SHARED_STORAGE_REASON = "no shared storage selection (top-level `storage` in global settings, or ATOMIC_STORAGE)";
104
+ /** The off reasons `resolveBrokerStateSource` reports without an error, as the log and `broker_status` state them. */
105
+ const MULTI_NODE_OFF_REASONS: Readonly<Record<string, string>> = {
106
+ "no-storage": "the shared storage selection names no database",
107
+ memory: "the shared storage selection is in memory",
108
+ "domain-only-adapter": "the storage adapter has no openDatabase hook",
109
+ };
97
110
 
98
111
  function exitBecauseLiveBrokerOwnsSocket(): never {
99
112
  console.error(`Intercom broker not started: a live broker already answers on ${SOCKET_PATH}`);
@@ -302,7 +315,9 @@ function isWorkflowPossibleStageAnnouncements(
302
315
 
303
316
  class IntercomBroker {
304
317
  private sessions = new Map<string, ConnectedSession>();
318
+ private unclassifiedConnections = new Map<net.Socket, number>();
305
319
  private server: net.Server;
320
+ private readonly instanceId = randomUUID();
306
321
  private shutdownTimer: NodeJS.Timeout | null = null;
307
322
  private boundSocketInode: number | undefined;
308
323
  private lastConnectionAt = performance.now();
@@ -328,6 +343,8 @@ class IntercomBroker {
328
343
  this.sharedStorageSetting === undefined ? undefined : (request) => this.routeRemoteSend(request);
329
344
  private multiNode: MultiNodeRouter | undefined;
330
345
  private multiNodeStatus: "starting" | "on" | "off" | "unavailable" = "off";
346
+ /** Why multi-node routing is off or unavailable, for `broker_status` and the log. */
347
+ private multiNodeReason: string | undefined = NO_SHARED_STORAGE_REASON;
331
348
  private multiNodeFirstAttempt: Promise<void> = Promise.resolve();
332
349
  private shuttingDown = false;
333
350
 
@@ -340,6 +357,7 @@ class IntercomBroker {
340
357
  start(): void {
341
358
  this.listen(1);
342
359
  if (this.sharedStorageSetting !== undefined) this.startMultiNode(this.sharedStorageSetting);
360
+ else console.error(`Intercom multi-node routing is off: ${NO_SHARED_STORAGE_REASON}`);
343
361
  process.on("SIGTERM", () => this.shutdown());
344
362
  process.on("SIGINT", () => this.shutdown());
345
363
  }
@@ -356,6 +374,7 @@ class IntercomBroker {
356
374
  releaseReplacementLock();
357
375
  writeFileSync(PID_PATH, String(process.pid));
358
376
  console.log(`Intercom broker started (pid: ${process.pid})`);
377
+ startBrokerWatchdog(SOCKET_PATH, this.instanceId, getBrokerWatchdogLogPath());
359
378
  this.lastConnectionAt = performance.now();
360
379
  this.scheduleShutdownCheck();
361
380
  if (this.boundSocketInode !== undefined) this.watchSocketOwnership();
@@ -415,11 +434,18 @@ class IntercomBroker {
415
434
 
416
435
  private handleConnection(socket: net.Socket): void {
417
436
  let sessionId: string | null = null;
418
- this.lastConnectionAt = performance.now();
419
- this.restartShutdownCheck();
437
+ this.unclassifiedConnections.set(socket, performance.now());
438
+ const recordActivity = () => {
439
+ this.lastConnectionAt = performance.now();
440
+ this.restartShutdownCheck();
441
+ };
420
442
 
421
443
  const reader = createMessageReader((msg) => {
422
444
  if (socket.destroyed || socket.writableEnded) return;
445
+ if (this.unclassifiedConnections.delete(socket)) {
446
+ const health = typeof msg === "object" && msg !== null && !Array.isArray(msg) && msg.type === "health";
447
+ if (!health) recordActivity();
448
+ }
423
449
  this.handleMessage(socket, msg, sessionId, (id) => {
424
450
  sessionId = id;
425
451
  });
@@ -441,6 +467,7 @@ class IntercomBroker {
441
467
  // is lost. disconnectSession is idempotent, so 'end', 'error' and 'close'
442
468
  // may each fire without duplicating the departure broadcast.
443
469
  const retire = () => {
470
+ if (this.unclassifiedConnections.delete(socket)) recordActivity();
444
471
  const departing = sessionId;
445
472
  if (departing === null) return;
446
473
  sessionId = null;
@@ -464,17 +491,24 @@ class IntercomBroker {
464
491
  });
465
492
  }
466
493
 
467
- private scheduleShutdownCheck(): void {
494
+ private scheduleShutdownCheck(delayMs = BROKER_IDLE_TIMEOUT_MS): void {
468
495
  if (this.shutdownTimer) return;
469
496
 
470
497
  this.shutdownTimer = setTimeout(() => {
471
498
  this.shutdownTimer = null;
472
499
  if (this.sessions.size === 0) {
500
+ const now = performance.now();
501
+ const awaitingFirstFrame = [...this.unclassifiedConnections.values()]
502
+ .some((acceptedAt) => now - acceptedAt < BROKER_IDLE_TIMEOUT_MS);
503
+ if (awaitingFirstFrame) {
504
+ this.scheduleShutdownCheck(UNCLASSIFIED_CONNECTION_RECHECK_MS);
505
+ return;
506
+ }
473
507
  const idleMs = Math.round(performance.now() - this.lastConnectionAt);
474
508
  console.log(`No sessions connected for ${idleMs}ms since the last connection, shutting down`);
475
509
  this.shutdown();
476
510
  }
477
- }, 5000);
511
+ }, delayMs);
478
512
  }
479
513
 
480
514
  private restartShutdownCheck(): void {
@@ -1473,6 +1507,27 @@ class IntercomBroker {
1473
1507
 
1474
1508
  const clientMessage = msg;
1475
1509
 
1510
+ if (clientMessage.type === "health") {
1511
+ if (typeof clientMessage.requestId !== "string" || clientMessage.requestId.length > 128) {
1512
+ throw new Error("Invalid health request");
1513
+ }
1514
+ writeMessageIfOpen(socket, { type: "health", requestId: clientMessage.requestId, instanceId: this.instanceId });
1515
+ return;
1516
+ }
1517
+
1518
+ // Control requests need no registration: `atomic intercom restart` and `atomic storage status` send them.
1519
+ if (clientMessage.type === "broker_status" || clientMessage.type === "shutdown") {
1520
+ if (typeof clientMessage.requestId !== "string") throw new Error(`Invalid ${clientMessage.type} message`);
1521
+ const requestId = clientMessage.requestId;
1522
+ if (clientMessage.type === "broker_status") {
1523
+ void this.statusReport().then((report) => writeMessageIfOpen(socket, { type: "broker_status", requestId, report }));
1524
+ return;
1525
+ }
1526
+ console.error("Intercom broker shutting down on request, so its sessions reconnect to a new broker");
1527
+ writeMessageWithOutcome(socket, { type: "shutdown_ack", requestId }, () => this.shutdown());
1528
+ return;
1529
+ }
1530
+
1476
1531
  if (currentId === null && clientMessage.type !== "register") {
1477
1532
  throw new Error(`Received ${clientMessage.type} before register`);
1478
1533
  }
@@ -1567,6 +1622,7 @@ class IntercomBroker {
1567
1622
  ...(supervisorId ? { supervisorSessionId: supervisorId } : {}),
1568
1623
  ...(clientMessage.acknowledgesReceipt === true ? { acknowledgesReceipt: true as const } : {}),
1569
1624
  acknowledgesPendingStageRoutes: true as const,
1625
+ brokerStatus: true as const,
1570
1626
  });
1571
1627
  if (isAgentRecipient(info)) this.broadcastToMemberships({ type: "session_joined", session: info }, sessionGroups(info), id);
1572
1628
  // #3761: messages written to this recipient's previous connection that it never confirmed saving go out again.
@@ -2097,12 +2153,32 @@ class IntercomBroker {
2097
2153
  });
2098
2154
  }
2099
2155
 
2156
+ /** This broker's `broker_status` report: its storage selection's digest, multi-node state and live peers. */
2157
+ private async statusReport(): Promise<BrokerStatusReport> {
2158
+ const setting = this.sharedStorageSetting;
2159
+ const peers = this.multiNodeStatus === "on" ? await this.multiNode?.peerBrokers() : undefined;
2160
+ return {
2161
+ pid: process.pid,
2162
+ sessions: this.sessions.size,
2163
+ ...(setting === undefined
2164
+ ? {}
2165
+ : { storage: { digest: sharedStorageDigest(setting), source: sharedStorageSettingSource() } }),
2166
+ multiNode: {
2167
+ status: this.multiNodeStatus,
2168
+ ...(this.multiNodeReason === undefined ? {} : { reason: this.multiNodeReason }),
2169
+ ...(setting === undefined ? {} : describeSharedStorage(setting)),
2170
+ ...(peers === undefined ? {} : { peers }),
2171
+ },
2172
+ };
2173
+ }
2174
+
2100
2175
  /**
2101
2176
  * The shared state is loaded lazily, so a single-node broker never loads the host storage layer. A database that
2102
2177
  * cannot be opened is retried with exponential backoff; meanwhile local delivery continues.
2103
2178
  */
2104
- private startMultiNode(setting: Record<string, unknown>): void {
2179
+ private startMultiNode(setting: Record<string, JsonWireValue>): void {
2105
2180
  this.multiNodeStatus = "starting";
2181
+ this.multiNodeReason = undefined;
2106
2182
  let settleFirstAttempt: () => void = () => {};
2107
2183
  this.multiNodeFirstAttempt = new Promise<void>((resolve) => {
2108
2184
  settleFirstAttempt = resolve;
@@ -2112,7 +2188,8 @@ class IntercomBroker {
2112
2188
  settleFirstAttempt();
2113
2189
  };
2114
2190
  const report = (error: unknown): void => {
2115
- console.error(`Intercom multi-node routing is unavailable: ${error instanceof Error ? error.message : String(error)}`);
2191
+ this.multiNodeReason = error instanceof Error ? error.message : String(error);
2192
+ console.error(`Intercom multi-node routing is unavailable: ${this.multiNodeReason}`);
2116
2193
  };
2117
2194
  void (async () => {
2118
2195
  const startup = await resolveMultiNodeStartup(setting, dirname(INTERCOM_DIR), async () => {
@@ -2126,9 +2203,10 @@ class IntercomBroker {
2126
2203
  return opener;
2127
2204
  });
2128
2205
  if (startup.status === "off") {
2129
- // A standalone binary has no host storage module, and a selection may not resolve to a database: both
2130
- // leave this broker single-node, exactly as without a shared `storage`.
2131
- if (startup.error !== undefined) console.error(`Intercom multi-node routing is off: ${startup.reason}`);
2206
+ // A selection that does not resolve to a database, or an adapter that cannot load, leaves this broker
2207
+ // single-node, exactly as without a shared `storage`.
2208
+ this.multiNodeReason = startup.error === undefined ? (MULTI_NODE_OFF_REASONS[startup.reason] ?? startup.reason) : startup.reason;
2209
+ console.error(`Intercom multi-node routing is off: ${this.multiNodeReason}`);
2132
2210
  settle("off");
2133
2211
  return;
2134
2212
  }
@@ -2171,6 +2249,9 @@ class IntercomBroker {
2171
2249
  }
2172
2250
  }
2173
2251
  if (this.shuttingDown) await this.multiNode.close().catch(() => {});
2252
+ this.multiNodeReason = undefined;
2253
+ const { adapter, prefix } = describeSharedStorage(setting);
2254
+ console.error(`Intercom multi-node routing is on (adapter ${adapter}, prefix ${prefix ?? "none"})`);
2174
2255
  settle("on");
2175
2256
  return;
2176
2257
  } catch (error) {