@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
@@ -5,6 +5,12 @@ export { buildSendSignature } from "./send-signature.js";
5
5
  const DEFAULT_GENERATION_TTL_MS = 10 * 60 * 1000;
6
6
  const DEFAULT_MAX_GENERATIONS = 10_000;
7
7
 
8
+ export class PendingSendTimeoutError extends Error {
9
+ constructor() {
10
+ super("Send timeout");
11
+ }
12
+ }
13
+
8
14
  export interface SendOptionsLike extends LogicalSendOptions {}
9
15
 
10
16
  export interface SendResultLike {
@@ -29,6 +35,7 @@ interface OwnedPendingSend extends PendingSendAttempt {
29
35
  resolve(result: SendResultLike): void;
30
36
  reject(error: Error): void;
31
37
  timer?: ReturnType<typeof setTimeout>;
38
+ timedOut?: boolean;
32
39
  }
33
40
 
34
41
  export interface PendingSendAcquisition {
@@ -106,7 +113,7 @@ export class PendingSendRegistry {
106
113
  generation,
107
114
  };
108
115
  attempt.timer = setTimeout(() => {
109
- this.reject(attempt, new Error("Send timeout"));
116
+ this.expire(attempt);
110
117
  }, timeoutMs);
111
118
  this.attempts.set(messageId, attempt);
112
119
  return { attempt, owner: true };
@@ -114,7 +121,7 @@ export class PendingSendRegistry {
114
121
 
115
122
  resolve(messageId: string, attemptId: string, result: SendResultLike): boolean {
116
123
  const attempt = this.attempts.get(messageId);
117
- if (!attempt || attempt.attemptId !== attemptId) return false;
124
+ if (!attempt || attempt.timedOut || attempt.attemptId !== attemptId) return false;
118
125
  this.attempts.delete(messageId);
119
126
  if (attempt.timer) clearTimeout(attempt.timer);
120
127
  this.touchGeneration(attempt);
@@ -128,10 +135,10 @@ export class PendingSendRegistry {
128
135
  */
129
136
  extend(messageId: string, attemptId: string | undefined, timeoutMs: number): boolean {
130
137
  const attempt = this.attempts.get(messageId);
131
- if (!attempt || (attemptId === undefined ? !attempt.legacyEligible : attempt.attemptId !== attemptId)) return false;
138
+ if (!attempt || attempt.timedOut || (attemptId === undefined ? !attempt.legacyEligible : attempt.attemptId !== attemptId)) return false;
132
139
  if (attempt.timer) clearTimeout(attempt.timer);
133
140
  attempt.timer = setTimeout(() => {
134
- this.reject(attempt, new Error("Send timeout"));
141
+ this.expire(attempt);
135
142
  }, timeoutMs);
136
143
  return true;
137
144
  }
@@ -139,10 +146,25 @@ export class PendingSendRegistry {
139
146
  /** Resolve a pre-attemptId response only for an ID's first active generation. */
140
147
  resolveLegacy(messageId: string, result: SendResultLike): boolean {
141
148
  const attempt = this.attempts.get(messageId);
142
- if (!attempt?.legacyEligible) return false;
149
+ if (!attempt?.legacyEligible || attempt.timedOut) return false;
143
150
  return this.resolve(messageId, attempt.attemptId, result);
144
151
  }
145
152
 
153
+ private expire(attempt: OwnedPendingSend): void {
154
+ if (this.attempts.get(attempt.messageId) !== attempt) return;
155
+ attempt.timedOut = true;
156
+ attempt.timer = undefined;
157
+ attempt.reject(new PendingSendTimeoutError());
158
+ }
159
+
160
+ release(attempt: PendingSendAttempt): boolean {
161
+ const current = this.attempts.get(attempt.messageId);
162
+ if (current !== attempt || !current.timedOut) return false;
163
+ this.attempts.delete(attempt.messageId);
164
+ this.touchGeneration(current);
165
+ return true;
166
+ }
167
+
146
168
  reject(attempt: PendingSendAttempt, error: Error): boolean {
147
169
  const current = this.attempts.get(attempt.messageId);
148
170
  if (current !== attempt) return false;
@@ -0,0 +1,60 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { JsonWireValue } from "./framing.js";
5
+ import { BROKER_STORAGE_ENV } from "./paths.js";
6
+
7
+ /**
8
+ * The raw shared `storage` selection, read without the host package so the broker stays self-contained: the
9
+ * selection the launching process handed this broker in `ATOMIC_STORAGE` (its `--storage`, `--storage-file` or own
10
+ * `ATOMIC_STORAGE`), otherwise the top-level `storage` value of the agent directory's settings. Relative paths in
11
+ * either resolve against the agent directory's parent, as for sessions and the journal. `undefined` leaves
12
+ * multi-node routing off: no selection, a per-process selection that is not a JSON object (the launching process
13
+ * validated it; settings are never consulted instead), and the selections the storage layer reads as in-memory
14
+ * (`memory: true` without a path, or `path: ":memory:"`), which no other broker can share. `path: ""` is not one of
15
+ * them: it selects the default storage file.
16
+ */
17
+ export function readSharedStorageSetting(
18
+ agentDir: string,
19
+ env: NodeJS.ProcessEnv = process.env,
20
+ ): Record<string, JsonWireValue> | undefined {
21
+ const processSelection = env[BROKER_STORAGE_ENV];
22
+ let parsed: JsonWireValue | undefined;
23
+ try {
24
+ parsed =
25
+ processSelection === undefined || processSelection.length === 0
26
+ ? (JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf8")) as { storage?: JsonWireValue }).storage
27
+ : JSON.parse(processSelection);
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
32
+ const selection = parsed;
33
+ if (selection.adapter === undefined) {
34
+ if (selection.memory === true && !selection.path) return undefined;
35
+ if (selection.path === ":memory:") return undefined;
36
+ }
37
+ return selection;
38
+ }
39
+
40
+ /** Where `readSharedStorageSetting` takes the selection from in this environment. */
41
+ export function sharedStorageSettingSource(env: NodeJS.ProcessEnv = process.env): "env" | "settings" {
42
+ const processSelection = env[BROKER_STORAGE_ENV];
43
+ return processSelection === undefined || processSelection.length === 0 ? "settings" : "env";
44
+ }
45
+
46
+ function sortedKeys(_key: string, value: JsonWireValue): JsonWireValue {
47
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
48
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)));
49
+ }
50
+
51
+ /** Identifies a shared selection independently of key order, without revealing it: two brokers agree when it matches. */
52
+ export function sharedStorageDigest(setting: Readonly<Record<string, JsonWireValue>>): string {
53
+ return createHash("sha256").update(JSON.stringify(setting, sortedKeys)).digest("hex");
54
+ }
55
+
56
+ /** The adapter (`sqlite` for a `path` selection) and table prefix a shared selection names, for logs and status. */
57
+ export function describeSharedStorage(setting: Readonly<Record<string, JsonWireValue>>): { adapter: string; prefix?: string } {
58
+ const adapter = typeof setting.adapter === "string" ? setting.adapter : "sqlite";
59
+ return typeof setting.prefix === "string" && setting.prefix.length > 0 ? { adapter, prefix: setting.prefix } : { adapter };
60
+ }
@@ -272,13 +272,21 @@ export function getBrokerSpawnOptions(
272
272
  }
273
273
 
274
274
  /**
275
- * Truncate (or create) the broker log so each spawn starts from a bounded, current file. Returns false when a
275
+ * Preserve a bounded previous log across truncation. Returns false when a
276
276
  * Windows broker that is still starting holds the log through its launcher's stderr redirect.
277
277
  */
278
278
  function resetBrokerLog(logPath: string = BROKER_LOG): boolean {
279
279
  ensurePrivateDirectory(dirname(logPath));
280
280
  try {
281
+ const previous = readBrokerLogTail(logPath);
281
282
  closeSync(openSync(logPath, "w"));
283
+ if (previous.length > 0) {
284
+ try {
285
+ const previousPath = `${logPath}.previous`;
286
+ writeFileSync(previousPath, Buffer.from(previous).subarray(-BROKER_LOG_TAIL_BYTES), { mode: 0o600 });
287
+ if (process.platform !== "win32") chmodSync(previousPath, 0o600);
288
+ } catch {}
289
+ }
282
290
  return true;
283
291
  } catch (error) {
284
292
  const code = (error as NodeJS.ErrnoException).code;
@@ -327,6 +335,7 @@ export async function spawnBrokerIfNeeded(
327
335
  brokerCommand: string,
328
336
  brokerArgs: string[],
329
337
  readyTimeoutMs: number = BROKER_READY_TIMEOUT_MS,
338
+ extensionDir: string = EXTENSION_DIR,
330
339
  ): Promise<void> {
331
340
  ensurePrivateDirectory(INTERCOM_DIR);
332
341
 
@@ -346,8 +355,8 @@ export async function spawnBrokerIfNeeded(
346
355
  return;
347
356
  }
348
357
 
349
- const brokerPath = resolveBrokerEntrypoint(EXTENSION_DIR);
350
- const launch = getBrokerLaunchSpec(brokerPath, brokerCommand, brokerArgs);
358
+ const brokerPath = resolveBrokerEntrypoint(extensionDir);
359
+ const launch = getBrokerLaunchSpec(brokerPath, brokerCommand, brokerArgs, extensionDir);
351
360
  if (launch.kind === "windows-launcher") {
352
361
  writeWindowsHiddenLauncher(launch.launcherCommandLine, launch.launcherPath);
353
362
  }
@@ -362,7 +371,7 @@ export async function spawnBrokerIfNeeded(
362
371
  const logFd = launch.kind === "direct" ? openSync(BROKER_LOG, "a") : undefined;
363
372
  let child: ReturnType<typeof spawn>;
364
373
  try {
365
- child = spawn(launch.command, launch.args, getBrokerSpawnOptions(EXTENSION_DIR, logFd ?? "ignore"));
374
+ child = spawn(launch.command, launch.args, getBrokerSpawnOptions(extensionDir, logFd ?? "ignore"));
366
375
  } finally {
367
376
  if (logFd !== undefined) closeSync(logFd);
368
377
  }
@@ -62,6 +62,29 @@ function stateFromRow(row: StateRow): LeaseState {
62
62
  };
63
63
  }
64
64
 
65
+ /**
66
+ * The host names of the brokers whose membership lease is live in `<prefix>intercom_leases`, read without creating
67
+ * the table: `undefined` when no broker has created it yet. For status reports; brokers use `listLive`.
68
+ */
69
+ export async function readLiveBrokerNodes(
70
+ database: SqliteExecutor,
71
+ prefix: string | undefined,
72
+ staleAfterMs: number,
73
+ ): Promise<string[] | undefined> {
74
+ const table = `${validateStoragePrefix(prefix)}intercom_leases`;
75
+ const exists = await database.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", table);
76
+ if (exists === undefined) return undefined;
77
+ const rows = await database.all<StateRow>(
78
+ `SELECT kind, key, holder_id, fencing_token, subject_json, ${AGE} AS age_ms FROM ${table}
79
+ WHERE kind = 'broker' AND holder_id IS NOT NULL AND ${AGE} <= ? ORDER BY first_acquired_order`,
80
+ staleAfterMs,
81
+ );
82
+ return rows.flatMap((row) => {
83
+ const { subject } = stateFromRow(row);
84
+ return subject.kind === "broker" ? [subject.nodeId] : [];
85
+ });
86
+ }
87
+
65
88
  /** Opens the S1 lease store on the shared database's `<prefix>intercom_leases` table; the database stays open. */
66
89
  export async function openSqlBrokerLeaseStore(options: IntercomBrokerStateOptions): Promise<BrokerLeaseStore> {
67
90
  const { database } = options;
@@ -0,0 +1,105 @@
1
+ import { Worker } from "node:worker_threads";
2
+
3
+ export const BROKER_WATCHDOG_INTERVAL_MS = 2_000;
4
+ export const BROKER_WATCHDOG_PROBE_TIMEOUT_MS = 10_000;
5
+ export const BROKER_WATCHDOG_SILENCE_MS = 60_000;
6
+
7
+ const WATCHDOG_SOURCE = `
8
+ const { workerData } = require("node:worker_threads");
9
+ const { connect } = require("node:net");
10
+ const { randomUUID } = require("node:crypto");
11
+ const { writeFileSync } = require("node:fs");
12
+ let lastHealthyAt = performance.now();
13
+ let consecutiveFailures = 0;
14
+
15
+ function probe() {
16
+ return new Promise((resolve) => {
17
+ const requestId = randomUUID();
18
+ const socket = connect(workerData.socketPath);
19
+ let buffer = Buffer.alloc(0);
20
+ let settled = false;
21
+ const finish = (failure) => {
22
+ if (settled) return;
23
+ settled = true;
24
+ clearTimeout(timer);
25
+ socket.destroy();
26
+ resolve(failure);
27
+ };
28
+ const timer = setTimeout(() => finish("response-timeout"), workerData.probeTimeoutMs);
29
+ socket.on("error", () => finish("connection-error"));
30
+ socket.on("close", () => finish("connection-closed"));
31
+ socket.on("connect", () => {
32
+ const payload = Buffer.from(JSON.stringify({ type: "health", requestId }));
33
+ const header = Buffer.alloc(4);
34
+ header.writeUInt32BE(payload.length);
35
+ try { socket.write(Buffer.concat([header, payload])); }
36
+ catch { finish("write-error"); }
37
+ });
38
+ socket.on("data", (data) => {
39
+ if (buffer.length + data.length > 1024) return finish("oversized-response");
40
+ buffer = Buffer.concat([buffer, data]);
41
+ if (buffer.length < 4) return;
42
+ const length = buffer.readUInt32BE(0);
43
+ if (length > 1020) return finish("oversized-response");
44
+ if (buffer.length < length + 4) return;
45
+ try {
46
+ const reply = JSON.parse(buffer.subarray(4, length + 4).toString("utf8"));
47
+ if (reply.type !== "health" || reply.requestId !== requestId) return finish("unexpected-response");
48
+ if (reply.instanceId !== workerData.instanceId) return finish("different-broker-instance");
49
+ finish();
50
+ } catch { finish("invalid-response"); }
51
+ });
52
+ });
53
+ }
54
+
55
+ async function check() {
56
+ const failure = await probe();
57
+ if (failure === undefined) {
58
+ lastHealthyAt = performance.now();
59
+ consecutiveFailures = 0;
60
+ } else {
61
+ consecutiveFailures += 1;
62
+ const silentForMs = Math.round(performance.now() - lastHealthyAt);
63
+ if (consecutiveFailures >= 3 && silentForMs >= workerData.silenceMs) {
64
+ try {
65
+ writeFileSync(workerData.logPath, JSON.stringify({
66
+ at: new Date().toISOString(), pid: process.pid, instanceId: workerData.instanceId,
67
+ reason: "Intercom broker stopped answering health requests; terminating for recovery",
68
+ failure, consecutiveFailures, silentForMs
69
+ }) + "\\n", { mode: 0o600 });
70
+ } catch {}
71
+ process.kill(process.pid, "SIGKILL");
72
+ return;
73
+ }
74
+ }
75
+ setTimeout(check, workerData.intervalMs);
76
+ }
77
+ setTimeout(check, workerData.intervalMs);
78
+ `;
79
+
80
+ export function startBrokerWatchdog(socketPath: string, instanceId: string, logPath: string): void {
81
+ try {
82
+ const worker = new Worker(WATCHDOG_SOURCE, {
83
+ eval: true,
84
+ workerData: {
85
+ socketPath,
86
+ instanceId,
87
+ logPath,
88
+ intervalMs: BROKER_WATCHDOG_INTERVAL_MS,
89
+ probeTimeoutMs: BROKER_WATCHDOG_PROBE_TIMEOUT_MS,
90
+ silenceMs: BROKER_WATCHDOG_SILENCE_MS,
91
+ },
92
+ });
93
+ let failed = false;
94
+ worker.on("error", (error) => {
95
+ failed = true;
96
+ console.error("Intercom broker watchdog unavailable:", error);
97
+ });
98
+ worker.on("exit", (code) => {
99
+ if (!failed) console.error(`Intercom broker watchdog exited unexpectedly with code ${code}`);
100
+ });
101
+ worker.unref();
102
+ } catch (error) {
103
+ console.error("Intercom broker watchdog unavailable:", error);
104
+ }
105
+ }