@bastani/atomic 0.9.39 → 0.9.40-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +14 -0
  3. package/dist/builtin/intercom/broker/broker.ts +523 -72
  4. package/dist/builtin/intercom/broker/client.ts +136 -5
  5. package/dist/builtin/intercom/broker/database-wake-up-bus.ts +274 -0
  6. package/dist/builtin/intercom/broker/group-membership.ts +2 -2
  7. package/dist/builtin/intercom/broker/multinode-router.ts +1500 -0
  8. package/dist/builtin/intercom/broker/multinode-state-store.ts +569 -0
  9. package/dist/builtin/intercom/broker/open-broker-state.ts +160 -0
  10. package/dist/builtin/intercom/broker/paths.ts +6 -0
  11. package/dist/builtin/intercom/broker/pending-send-registry.ts +14 -0
  12. package/dist/builtin/intercom/broker/recipient-receipts.ts +93 -0
  13. package/dist/builtin/intercom/broker/restoring-recipients.ts +263 -0
  14. package/dist/builtin/intercom/broker/send-handler.ts +241 -25
  15. package/dist/builtin/intercom/broker/session-id-reclaim.ts +41 -0
  16. package/dist/builtin/intercom/broker/socket-writes.ts +47 -0
  17. package/dist/builtin/intercom/broker/spawn.ts +10 -2
  18. package/dist/builtin/intercom/broker/sql-held-send-store.ts +96 -0
  19. package/dist/builtin/intercom/broker/sql-known-session-store.ts +129 -0
  20. package/dist/builtin/intercom/broker/sql-lease-store.ts +194 -0
  21. package/dist/builtin/intercom/broker/sql-outbox-store.ts +249 -0
  22. package/dist/builtin/intercom/broker/sql-pending-ask-store.ts +105 -0
  23. package/dist/builtin/intercom/broker/sql-restoring-reservation-store.ts +295 -0
  24. package/dist/builtin/intercom/broker/sql-sent-message-authority.ts +126 -0
  25. package/dist/builtin/intercom/broker/supervisor-channel.ts +10 -0
  26. package/dist/builtin/intercom/broker/wake-up-bus.ts +60 -0
  27. package/dist/builtin/intercom/index.bundle.mjs +793 -91
  28. package/dist/builtin/intercom/package.json +1 -1
  29. package/dist/builtin/intercom/retry-policy.ts +4 -0
  30. package/dist/builtin/intercom/session-target.ts +24 -0
  31. package/dist/builtin/intercom/types.ts +57 -3
  32. package/dist/builtin/mcp/package.json +1 -1
  33. package/dist/builtin/subagents/CHANGELOG.md +31 -0
  34. package/dist/builtin/subagents/README.md +18 -12
  35. package/dist/builtin/subagents/package.json +1 -1
  36. package/dist/builtin/subagents/skills/agent-browser/SKILL.md +1 -1
  37. package/dist/builtin/subagents/skills/cua-driver/BROWSER.md +55 -8
  38. package/dist/builtin/subagents/skills/cua-driver/EMBEDDING.md +7 -2
  39. package/dist/builtin/subagents/skills/cua-driver/LINUX.md +7 -1
  40. package/dist/builtin/subagents/skills/cua-driver/MACOS.md +151 -27
  41. package/dist/builtin/subagents/skills/cua-driver/RUNTIME.md +1 -1
  42. package/dist/builtin/subagents/skills/cua-driver/SKILL.md +33 -17
  43. package/dist/builtin/subagents/skills/cua-driver/WINDOWS.md +5 -0
  44. package/dist/builtin/subagents/skills/cua-driver/WORKFLOW.md +46 -8
  45. package/dist/builtin/subagents/skills/subagent/SKILL.md +18 -20
  46. package/dist/builtin/subagents/src/extension/index.bundle.mjs +8032 -6034
  47. package/dist/builtin/web-access/package.json +1 -1
  48. package/dist/builtin/workflows/CHANGELOG.md +18 -0
  49. package/dist/builtin/workflows/README.md +1 -1
  50. package/dist/builtin/workflows/package.json +1 -1
  51. package/dist/builtin/workflows/skills/impeccable/SKILL.md +8 -8
  52. package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +3 -3
  53. package/dist/builtin/workflows/skills/impeccable/reference/adapt.native.md +1 -1
  54. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +4 -2
  55. package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
  56. package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +1 -1
  57. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +22 -9
  58. package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +2 -2
  59. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +3 -3
  60. package/dist/builtin/workflows/skills/impeccable/reference/extract.md +3 -3
  61. package/dist/builtin/workflows/skills/impeccable/reference/harden.md +2 -4
  62. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +4 -4
  63. package/dist/builtin/workflows/skills/impeccable/reference/live.md +1 -1
  64. package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +6 -6
  65. package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +3 -3
  66. package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +3 -3
  67. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +3 -3
  68. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
  69. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +10 -0
  70. package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +1 -1
  71. package/dist/builtin/workflows/skills/impeccable/scripts/VERSION +1 -1
  72. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable +6 -0
  73. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable.cmd +47 -22
  74. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +34 -1
  75. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-session.js +25 -1
  76. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +33 -8
  77. package/dist/builtin/workflows/src/durable/journal/resolution.d.ts +7 -7
  78. package/dist/builtin/workflows/src/extension/index.bundle.mjs +22855 -22308
  79. package/dist/builtin/workflows/src/index.js +106 -35
  80. package/dist/cli/args.d.ts +2 -0
  81. package/dist/cli/args.d.ts.map +1 -1
  82. package/dist/cli/args.js +16 -0
  83. package/dist/cli/args.js.map +1 -1
  84. package/dist/core/agent-session-extension-bindings.d.ts +2 -0
  85. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  86. package/dist/core/agent-session-extension-bindings.js +6 -6
  87. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  88. package/dist/core/agent-session-methods.d.ts +6 -3
  89. package/dist/core/agent-session-methods.d.ts.map +1 -1
  90. package/dist/core/agent-session-methods.js.map +1 -1
  91. package/dist/core/agent-session-persistent-custom-messages.d.ts +7 -2
  92. package/dist/core/agent-session-persistent-custom-messages.d.ts.map +1 -1
  93. package/dist/core/agent-session-persistent-custom-messages.js +29 -12
  94. package/dist/core/agent-session-persistent-custom-messages.js.map +1 -1
  95. package/dist/core/agent-session-recovery.d.ts +14 -0
  96. package/dist/core/agent-session-recovery.d.ts.map +1 -0
  97. package/dist/core/agent-session-recovery.js +83 -0
  98. package/dist/core/agent-session-recovery.js.map +1 -0
  99. package/dist/core/agent-session-tasks.d.ts.map +1 -1
  100. package/dist/core/agent-session-tasks.js +4 -6
  101. package/dist/core/agent-session-tasks.js.map +1 -1
  102. package/dist/core/agent-session-types.d.ts +13 -0
  103. package/dist/core/agent-session-types.d.ts.map +1 -1
  104. package/dist/core/agent-session-types.js.map +1 -1
  105. package/dist/core/agent-session.d.ts +2 -1
  106. package/dist/core/agent-session.d.ts.map +1 -1
  107. package/dist/core/agent-session.js +4 -1
  108. package/dist/core/agent-session.js.map +1 -1
  109. package/dist/core/extensions/api-types.d.ts +6 -2
  110. package/dist/core/extensions/api-types.d.ts.map +1 -1
  111. package/dist/core/extensions/api-types.js.map +1 -1
  112. package/dist/core/extensions/event-results.d.ts +21 -0
  113. package/dist/core/extensions/event-results.d.ts.map +1 -1
  114. package/dist/core/extensions/event-results.js.map +1 -1
  115. package/dist/core/extensions/index.d.ts +1 -1
  116. package/dist/core/extensions/index.d.ts.map +1 -1
  117. package/dist/core/extensions/index.js.map +1 -1
  118. package/dist/core/extensions/loader-api.d.ts.map +1 -1
  119. package/dist/core/extensions/loader-api.js +4 -0
  120. package/dist/core/extensions/loader-api.js.map +1 -1
  121. package/dist/core/extensions/runner-events.d.ts +9 -2
  122. package/dist/core/extensions/runner-events.d.ts.map +1 -1
  123. package/dist/core/extensions/runner-events.js +29 -0
  124. package/dist/core/extensions/runner-events.js.map +1 -1
  125. package/dist/core/extensions/runner.d.ts +5 -3
  126. package/dist/core/extensions/runner.d.ts.map +1 -1
  127. package/dist/core/extensions/runner.js +7 -2
  128. package/dist/core/extensions/runner.js.map +1 -1
  129. package/dist/core/extensions/runtime-types.d.ts +1 -0
  130. package/dist/core/extensions/runtime-types.d.ts.map +1 -1
  131. package/dist/core/extensions/runtime-types.js.map +1 -1
  132. package/dist/core/extensions/session-events.d.ts +30 -2
  133. package/dist/core/extensions/session-events.d.ts.map +1 -1
  134. package/dist/core/extensions/session-events.js.map +1 -1
  135. package/dist/core/sdk-types.d.ts +3 -2
  136. package/dist/core/sdk-types.d.ts.map +1 -1
  137. package/dist/core/sdk-types.js.map +1 -1
  138. package/dist/core/sdk.d.ts.map +1 -1
  139. package/dist/core/sdk.js +3 -0
  140. package/dist/core/sdk.js.map +1 -1
  141. package/dist/core/session-storage.d.ts.map +1 -1
  142. package/dist/core/session-storage.js +15 -9
  143. package/dist/core/session-storage.js.map +1 -1
  144. package/dist/core/storage/process-selection.d.ts +39 -0
  145. package/dist/core/storage/process-selection.d.ts.map +1 -0
  146. package/dist/core/storage/process-selection.js +154 -0
  147. package/dist/core/storage/process-selection.js.map +1 -0
  148. package/dist/core/storage/provider.d.ts +6 -1
  149. package/dist/core/storage/provider.d.ts.map +1 -1
  150. package/dist/core/storage/provider.js +3 -2
  151. package/dist/core/storage/provider.js.map +1 -1
  152. package/dist/core/storage/selection.d.ts +1 -1
  153. package/dist/core/storage/selection.d.ts.map +1 -1
  154. package/dist/core/storage/selection.js.map +1 -1
  155. package/dist/core/storage/session/jsonl-session-store.d.ts +2 -0
  156. package/dist/core/storage/session/jsonl-session-store.d.ts.map +1 -1
  157. package/dist/core/storage/session/jsonl-session-store.js +38 -2
  158. package/dist/core/storage/session/jsonl-session-store.js.map +1 -1
  159. package/dist/core/tasks/completion.d.ts +11 -2
  160. package/dist/core/tasks/completion.d.ts.map +1 -1
  161. package/dist/core/tasks/completion.js +40 -16
  162. package/dist/core/tasks/completion.js.map +1 -1
  163. package/dist/core/tasks/pause.d.ts +4 -1
  164. package/dist/core/tasks/pause.d.ts.map +1 -1
  165. package/dist/core/tasks/pause.js +8 -5
  166. package/dist/core/tasks/pause.js.map +1 -1
  167. package/dist/index-extensions.d.ts +1 -1
  168. package/dist/index-extensions.d.ts.map +1 -1
  169. package/dist/index-extensions.js.map +1 -1
  170. package/dist/index.d.ts +2 -1
  171. package/dist/index.d.ts.map +1 -1
  172. package/dist/index.js +2 -0
  173. package/dist/index.js.map +1 -1
  174. package/dist/main.d.ts.map +1 -1
  175. package/dist/main.js +7 -1
  176. package/dist/main.js.map +1 -1
  177. package/dist/modes/interactive/components/chat-session-host-rendering.d.ts.map +1 -1
  178. package/dist/modes/interactive/components/chat-session-host-rendering.js +11 -1
  179. package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
  180. package/dist/modes/interactive/components/chat-session-host-state.d.ts +2 -0
  181. package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
  182. package/dist/modes/interactive/components/chat-session-host-state.js +2 -0
  183. package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
  184. package/dist/modes/interactive/interactive-deferred-startup.d.ts.map +1 -1
  185. package/dist/modes/interactive/interactive-deferred-startup.js +4 -0
  186. package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
  187. package/dist/modes/interactive/interactive-mode-surface.d.ts +2 -1
  188. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  189. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  190. package/dist/modes/interactive/interactive-render-chat.js +3 -0
  191. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  192. package/dist/modes/interactive/interactive-session-routing.js +2 -0
  193. package/dist/modes/interactive/interactive-session-routing.js.map +1 -1
  194. package/dist/modes/interactive/interactive-session-runtime.d.ts +2 -1
  195. package/dist/modes/interactive/interactive-session-runtime.d.ts.map +1 -1
  196. package/dist/modes/interactive/interactive-session-runtime.js +29 -0
  197. package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
  198. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  199. package/dist/modes/interactive/interactive-startup.js +4 -1
  200. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  201. package/dist/modes/interactive-engine/engine-args.d.ts.map +1 -1
  202. package/dist/modes/interactive-engine/engine-args.js +4 -0
  203. package/dist/modes/interactive-engine/engine-args.js.map +1 -1
  204. package/dist/modes/interactive-engine/engine-render-service.d.ts.map +1 -1
  205. package/dist/modes/interactive-engine/engine-render-service.js +4 -4
  206. package/dist/modes/interactive-engine/engine-render-service.js.map +1 -1
  207. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  208. package/dist/modes/interactive-engine/isolated-runtime.js +4 -0
  209. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  210. package/dist/modes/interactive-engine/remote-renderer.js +1 -1
  211. package/dist/modes/interactive-engine/remote-renderer.js.map +1 -1
  212. package/dist/modes/rpc/rpc-client-api.d.ts +4 -1
  213. package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
  214. package/dist/modes/rpc/rpc-client-api.js +4 -0
  215. package/dist/modes/rpc/rpc-client-api.js.map +1 -1
  216. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  217. package/dist/modes/rpc/rpc-command-handler.js +3 -0
  218. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  219. package/dist/modes/rpc/rpc-command-timeouts.d.ts.map +1 -1
  220. package/dist/modes/rpc/rpc-command-timeouts.js +1 -0
  221. package/dist/modes/rpc/rpc-command-timeouts.js.map +1 -1
  222. package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
  223. package/dist/modes/rpc/rpc-input-scheduler.js +6 -1
  224. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  225. package/dist/modes/rpc/rpc-types.d.ts +12 -1
  226. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  227. package/dist/modes/rpc/rpc-types.js.map +1 -1
  228. package/dist/utils/shell.d.ts.map +1 -1
  229. package/dist/utils/shell.js +44 -11
  230. package/dist/utils/shell.js.map +1 -1
  231. package/dist/utils/tools-manager.js +1 -1
  232. package/dist/utils/tools-manager.js.map +1 -1
  233. package/docs/background-tasks.md +5 -3
  234. package/docs/computer-use.md +2 -2
  235. package/docs/environment-variables.md +4 -1
  236. package/docs/extensions/events.md +29 -2
  237. package/docs/getting-started/first-session.md +1 -1
  238. package/docs/guides/intercom.md +1 -1
  239. package/docs/intercom/operations.md +103 -1
  240. package/docs/intercom/reference.md +10 -3
  241. package/docs/intercom.md +8 -6
  242. package/docs/keybindings.md +1 -1
  243. package/docs/models.md +1 -1
  244. package/docs/providers.md +4 -2
  245. package/docs/reference/cli.md +5 -0
  246. package/docs/rpc/protocol.md +19 -0
  247. package/docs/rpc.md +3 -0
  248. package/docs/sdk/web-server.md +1 -1
  249. package/docs/sdk.md +5 -2
  250. package/docs/sessions.md +17 -1
  251. package/docs/settings.md +5 -3
  252. package/docs/skills.md +1 -1
  253. package/docs/storage.md +54 -3
  254. package/docs/subagents.md +49 -9
  255. package/docs/usage.md +1 -1
  256. package/docs/windows.md +14 -4
  257. package/docs/workflows/api-reference.md +1 -1
  258. package/docs/workflows/authoring.md +1 -1
  259. package/docs/workflows/operations.md +10 -6
  260. package/npm-shrinkwrap.json +37 -37
  261. package/package.json +4 -4
@@ -0,0 +1,295 @@
1
+ import { type SqliteExecutor, type SqliteRow, validateStoragePrefix } from "@bastani/atomic/storage";
2
+ import type {
3
+ IntercomBrokerStateOptions,
4
+ InvalidStateInput,
5
+ LeaseStaleResult,
6
+ RestoringReservation,
7
+ RestoringReservationConsumeResult,
8
+ RestoringReservationEnd,
9
+ RestoringReservationLease,
10
+ RestoringReservationLookupResult,
11
+ RestoringReservationReadResult,
12
+ RestoringReservationReleaseResult,
13
+ RestoringReservationRenewResult,
14
+ RestoringReservationReserveResult,
15
+ RestoringReservationStore,
16
+ StateStoreInvalidReason,
17
+ } from "./multinode-state-store.js";
18
+
19
+ type ReservationRow = SqliteRow & {
20
+ reservation_key: string;
21
+ owner_session_id: string;
22
+ owner_broker_id: string;
23
+ child_name_json: string;
24
+ child_target: string;
25
+ targets_json: string;
26
+ reservation_group: string;
27
+ dormant: number;
28
+ fencing_token: number;
29
+ gone_json: string | null;
30
+ end_json: string | null;
31
+ age_ms: number;
32
+ };
33
+
34
+ type Counter = "order" | "token";
35
+
36
+ const STORE_NOW = "CAST(unixepoch('subsec') * 1000 AS INTEGER)";
37
+
38
+ const invalid = (reason: StateStoreInvalidReason): InvalidStateInput => ({ status: "invalid", reason });
39
+
40
+ function reservationOf(row: ReservationRow): RestoringReservation {
41
+ return {
42
+ reservationKey: row.reservation_key,
43
+ ownerSessionId: row.owner_session_id,
44
+ ownerBrokerId: row.owner_broker_id,
45
+ childName: JSON.parse(row.child_name_json) as string,
46
+ childTarget: row.child_target,
47
+ targets: JSON.parse(row.targets_json) as string[],
48
+ group: row.reservation_group,
49
+ dormant: row.dormant === 1,
50
+ fencingToken: row.fencing_token,
51
+ ...(row.gone_json === null ? {} : { gone: JSON.parse(row.gone_json) as string }),
52
+ };
53
+ }
54
+
55
+ function endOf(endJson: string): RestoringReservationEnd {
56
+ const end = JSON.parse(endJson) as RestoringReservationEnd;
57
+ if (end.outcome === "consumed") return { outcome: "consumed" };
58
+ if (end.outcome === "superseded") return { outcome: "superseded", by: end.by };
59
+ return end.refusal === undefined ? { outcome: "released" } : { outcome: "released", refusal: end.refusal };
60
+ }
61
+
62
+ function mergeTargets(targets: string[], added: readonly string[]): string[] {
63
+ for (const target of added) if (!targets.includes(target)) targets.push(target);
64
+ return targets;
65
+ }
66
+
67
+ function invalidLease(lease: RestoringReservationLease): InvalidStateInput | undefined {
68
+ if (lease.reservationKey.length === 0) return invalid("empty-reservation-key");
69
+ return lease.ownerSessionId.length === 0 ? invalid("empty-session-id") : undefined;
70
+ }
71
+
72
+ /**
73
+ * S3: `RestoringReservationStore` on the shared storage adapter's database, table
74
+ * `<prefix>intercom_restoring_reservations`. Insertion order and fencing tokens come from
75
+ * `<prefix>intercom_restoring_reservation_counters`, so both keep increasing after `prune` deletes rows.
76
+ */
77
+ export async function openSqlRestoringReservationStore({
78
+ database,
79
+ prefix,
80
+ }: IntercomBrokerStateOptions): Promise<RestoringReservationStore> {
81
+ const validPrefix = validateStoragePrefix(prefix);
82
+ const table = `${validPrefix}intercom_restoring_reservations`;
83
+ const counters = `${validPrefix}intercom_restoring_reservation_counters`;
84
+ await database.transaction((tx) =>
85
+ tx.exec(`
86
+ CREATE TABLE IF NOT EXISTS ${table}(
87
+ reservation_key TEXT PRIMARY KEY,
88
+ "order" INTEGER NOT NULL,
89
+ owner_session_id TEXT NOT NULL,
90
+ owner_broker_id TEXT NOT NULL,
91
+ child_name TEXT NOT NULL,
92
+ child_target TEXT NOT NULL,
93
+ targets_json TEXT NOT NULL,
94
+ "group" TEXT NOT NULL,
95
+ dormant INTEGER NOT NULL,
96
+ fencing_token INTEGER NOT NULL,
97
+ gone TEXT,
98
+ renewed_at INTEGER NOT NULL,
99
+ end_json TEXT,
100
+ ended_at INTEGER
101
+ );
102
+ CREATE INDEX IF NOT EXISTS ${table}_active ON ${table}(end_json, "order");
103
+ CREATE TABLE IF NOT EXISTS ${counters}(name TEXT PRIMARY KEY, value INTEGER NOT NULL);
104
+ INSERT INTO ${counters}(name, value) VALUES ('order', 0), ('token', 0) ON CONFLICT(name) DO NOTHING;
105
+ `),
106
+ );
107
+ const columns = `reservation_key, owner_session_id, owner_broker_id, child_name AS child_name_json, child_target,
108
+ targets_json, "group" AS reservation_group, dormant, fencing_token, gone AS gone_json, end_json,
109
+ MAX(0, ${STORE_NOW} - renewed_at) AS age_ms`;
110
+ const readRow = (executor: SqliteExecutor, reservationKey: string) =>
111
+ executor.get<ReservationRow>(`SELECT ${columns} FROM ${table} WHERE reservation_key = ?`, reservationKey);
112
+ const activeRows = (executor: SqliteExecutor) =>
113
+ executor.all<ReservationRow>(`SELECT ${columns} FROM ${table} WHERE end_json IS NULL ORDER BY "order"`);
114
+ const next = async (executor: SqliteExecutor, counter: Counter): Promise<number> => {
115
+ const row = await executor.get<SqliteRow & { value: number }>(
116
+ `UPDATE ${counters} SET value = value + 1 WHERE name = ? RETURNING value`,
117
+ counter,
118
+ );
119
+ if (row === undefined) throw new Error(`Missing ${counters} row ${counter}`);
120
+ return row.value;
121
+ };
122
+ const end = (executor: SqliteExecutor, reservationKey: string, outcome: RestoringReservationEnd) =>
123
+ executor.run(
124
+ `UPDATE ${table} SET end_json = ?, ended_at = ${STORE_NOW} WHERE reservation_key = ?`,
125
+ JSON.stringify(outcome),
126
+ reservationKey,
127
+ );
128
+ const owned = async (
129
+ executor: SqliteExecutor,
130
+ lease: RestoringReservationLease,
131
+ ): Promise<LeaseStaleResult | undefined> => {
132
+ const row = await readRow(executor, lease.reservationKey);
133
+ if (row === undefined || row.end_json !== null) return { status: "stale" };
134
+ if (row.owner_session_id !== lease.ownerSessionId || row.fencing_token !== lease.fencingToken) {
135
+ return { status: "stale", currentFencingToken: row.fencing_token };
136
+ }
137
+ return undefined;
138
+ };
139
+
140
+ return {
141
+ async reserve(request): Promise<RestoringReservationReserveResult> {
142
+ if (request.reservationKey.length === 0) return invalid("empty-reservation-key");
143
+ if (request.ownerSessionId.length === 0) return invalid("empty-session-id");
144
+ if (request.ownerBrokerId.length === 0) return invalid("empty-broker-id");
145
+ if (request.childName.length === 0) return invalid("empty-child-name");
146
+ if (request.childTarget.length === 0 || request.aliases.some((alias) => alias.length === 0)) {
147
+ return invalid("empty-target");
148
+ }
149
+ if (request.group.length === 0) return invalid("empty-group");
150
+ return database.transaction(async (tx) => {
151
+ const existing = await readRow(tx, request.reservationKey);
152
+ const superseded: string[] = [];
153
+ let fencingToken: number;
154
+ if (existing !== undefined && existing.end_json === null) {
155
+ fencingToken =
156
+ existing.owner_session_id === request.ownerSessionId
157
+ ? existing.fencing_token
158
+ : await next(tx, "token");
159
+ await tx.run(
160
+ `UPDATE ${table} SET owner_session_id = ?, owner_broker_id = ?, "group" = ?, dormant = ?,
161
+ fencing_token = ?, targets_json = ?, renewed_at = ${STORE_NOW} WHERE reservation_key = ?`,
162
+ request.ownerSessionId,
163
+ request.ownerBrokerId,
164
+ request.group,
165
+ request.dormant ? 1 : 0,
166
+ fencingToken,
167
+ JSON.stringify(mergeTargets(JSON.parse(existing.targets_json) as string[], request.aliases)),
168
+ request.reservationKey,
169
+ );
170
+ } else {
171
+ const order = await next(tx, "order");
172
+ fencingToken = await next(tx, "token");
173
+ const targets = [request.childTarget];
174
+ for (const previous of await activeRows(tx)) {
175
+ if (previous.child_target !== request.childTarget) continue;
176
+ const sameOwner = previous.owner_session_id === request.ownerSessionId;
177
+ const dormantInGroup = previous.dormant === 1 && previous.reservation_group === request.group;
178
+ if (!sameOwner && !dormantInGroup) continue;
179
+ await end(tx, previous.reservation_key, { outcome: "superseded", by: request.reservationKey });
180
+ mergeTargets(targets, JSON.parse(previous.targets_json) as string[]);
181
+ superseded.push(previous.reservation_key);
182
+ }
183
+ await tx.run(
184
+ `INSERT OR REPLACE INTO ${table}(reservation_key, "order", owner_session_id, owner_broker_id,
185
+ child_name, child_target, targets_json, "group", dormant, fencing_token, gone, renewed_at,
186
+ end_json, ended_at)
187
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ${STORE_NOW}, NULL, NULL)`,
188
+ request.reservationKey,
189
+ order,
190
+ request.ownerSessionId,
191
+ request.ownerBrokerId,
192
+ JSON.stringify(request.childName),
193
+ request.childTarget,
194
+ JSON.stringify(mergeTargets(targets, request.aliases)),
195
+ request.group,
196
+ request.dormant ? 1 : 0,
197
+ fencingToken,
198
+ );
199
+ }
200
+ return {
201
+ status: "reserved",
202
+ lease: {
203
+ reservationKey: request.reservationKey,
204
+ ownerSessionId: request.ownerSessionId,
205
+ fencingToken,
206
+ },
207
+ superseded,
208
+ };
209
+ });
210
+ },
211
+ async lookup(target, sender, staleAfterMs): Promise<RestoringReservationLookupResult> {
212
+ if (target.length === 0) return invalid("empty-target");
213
+ if (sender.sessionId.length === 0) return invalid("empty-session-id");
214
+ const found = (await activeRows(database)).find(
215
+ (row) =>
216
+ row.age_ms <= staleAfterMs &&
217
+ (JSON.parse(row.targets_json) as string[]).includes(target) &&
218
+ (row.owner_session_id === sender.sessionId || sender.groups.includes(row.reservation_group)),
219
+ );
220
+ if (found === undefined) return { status: "none" };
221
+ return { status: found.gone_json === null ? "reserved" : "gone", reservation: reservationOf(found) };
222
+ },
223
+ async renew(lease): Promise<RestoringReservationRenewResult> {
224
+ const bad = invalidLease(lease);
225
+ if (bad !== undefined) return bad;
226
+ return database.transaction(async (tx) => {
227
+ const stale = await owned(tx, lease);
228
+ if (stale !== undefined) return stale;
229
+ await tx.run(
230
+ `UPDATE ${table} SET renewed_at = ${STORE_NOW} WHERE reservation_key = ?`,
231
+ lease.reservationKey,
232
+ );
233
+ return { status: "renewed" };
234
+ });
235
+ },
236
+ async release(lease, outcome): Promise<RestoringReservationReleaseResult> {
237
+ const bad = invalidLease(lease);
238
+ if (bad !== undefined) return bad;
239
+ return database.transaction(async (tx) => {
240
+ const stale = await owned(tx, lease);
241
+ if (stale !== undefined) return stale;
242
+ if (outcome?.gone !== undefined) {
243
+ await tx.run(
244
+ `UPDATE ${table} SET gone = ? WHERE reservation_key = ?`,
245
+ JSON.stringify(outcome.gone),
246
+ lease.reservationKey,
247
+ );
248
+ return { status: "gone" };
249
+ }
250
+ await end(
251
+ tx,
252
+ lease.reservationKey,
253
+ outcome?.refusal === undefined
254
+ ? { outcome: "released" }
255
+ : { outcome: "released", refusal: outcome.refusal },
256
+ );
257
+ return { status: "released" };
258
+ });
259
+ },
260
+ async consume(reservationKey, registered): Promise<RestoringReservationConsumeResult> {
261
+ if (reservationKey.length === 0) return invalid("empty-reservation-key");
262
+ if (registered !== undefined && registered.childTarget.length === 0) return invalid("empty-target");
263
+ return database.transaction(async (tx) => {
264
+ const ended: string[] = [];
265
+ for (const row of await activeRows(tx)) {
266
+ if (row.gone_json !== null) continue;
267
+ const own = row.reservation_key === reservationKey;
268
+ const dormantChild =
269
+ registered !== undefined &&
270
+ row.dormant === 1 &&
271
+ row.child_target === registered.childTarget &&
272
+ registered.groups.includes(row.reservation_group);
273
+ if (!own && !dormantChild) continue;
274
+ await end(tx, row.reservation_key, { outcome: "consumed" });
275
+ ended.push(row.reservation_key);
276
+ }
277
+ return { status: "consumed", reservationKeys: ended };
278
+ });
279
+ },
280
+ async read(reservationKey): Promise<RestoringReservationReadResult> {
281
+ if (reservationKey.length === 0) return invalid("empty-reservation-key");
282
+ const row = await readRow(database, reservationKey);
283
+ if (row === undefined) return { status: "missing" };
284
+ if (row.end_json !== null) return { status: "ended", end: endOf(row.end_json) };
285
+ return { status: "active", reservation: reservationOf(row), ageMs: row.age_ms };
286
+ },
287
+ async prune(olderThanMs): Promise<number> {
288
+ const { changes } = await database.run(
289
+ `DELETE FROM ${table} WHERE ${STORE_NOW} - COALESCE(ended_at, renewed_at) > ?`,
290
+ olderThanMs,
291
+ );
292
+ return changes;
293
+ },
294
+ };
295
+ }
@@ -0,0 +1,126 @@
1
+ import { type SqliteExecutor, type SqliteRow, validateStoragePrefix } from "@bastani/atomic/storage";
2
+ import type {
3
+ IntercomBrokerStateOptions,
4
+ InvalidStateInput,
5
+ SentMessageAcceptResult,
6
+ SentMessageAuthority,
7
+ SentMessageForgetResult,
8
+ SentMessageKey,
9
+ SentMessageLookupResult,
10
+ SentMessageReserveResult,
11
+ SentMessageState,
12
+ SentQuestionRoute,
13
+ } from "./multinode-state-store.js";
14
+
15
+ type SentRow = SqliteRow & {
16
+ digest: string;
17
+ target_identity: string;
18
+ question_route_json: string | null;
19
+ state: SentMessageState;
20
+ };
21
+
22
+ const STORE_NOW = "CAST(unixepoch('subsec') * 1000 AS INTEGER)";
23
+
24
+ const emptyMessageId: InvalidStateInput = { status: "invalid", reason: "empty-message-id" };
25
+
26
+ function questionRouteOf(row: SentRow): { questionRoute?: SentQuestionRoute } {
27
+ if (row.question_route_json === null) return {};
28
+ const { targetSessionId, senderGroupIdentity } = JSON.parse(row.question_route_json) as SentQuestionRoute;
29
+ return { questionRoute: { targetSessionId, senderGroupIdentity } };
30
+ }
31
+
32
+ /**
33
+ * S3: `SentMessageAuthority` on the shared storage adapter's database, table `<prefix>intercom_sent_messages`.
34
+ * Digests are opaque strings computed by the caller.
35
+ */
36
+ export async function openSqlSentMessageAuthority({
37
+ database,
38
+ prefix,
39
+ }: IntercomBrokerStateOptions): Promise<SentMessageAuthority> {
40
+ const table = `${validateStoragePrefix(prefix)}intercom_sent_messages`;
41
+ await database.transaction((tx) =>
42
+ tx.exec(`
43
+ CREATE TABLE IF NOT EXISTS ${table}(
44
+ message_id TEXT PRIMARY KEY,
45
+ digest TEXT NOT NULL,
46
+ target_identity TEXT NOT NULL,
47
+ question_route_json TEXT,
48
+ state TEXT NOT NULL CHECK (state IN ('reserved', 'accepted')),
49
+ recorded_at INTEGER NOT NULL
50
+ );
51
+ CREATE INDEX IF NOT EXISTS ${table}_recorded ON ${table}(recorded_at);
52
+ `),
53
+ );
54
+ const read = (executor: SqliteExecutor, messageId: string) =>
55
+ executor.get<SentRow>(
56
+ `SELECT digest, target_identity, question_route_json, state FROM ${table} WHERE message_id = ?`,
57
+ messageId,
58
+ );
59
+
60
+ return {
61
+ async reserve(reservation): Promise<SentMessageReserveResult> {
62
+ if (reservation.messageId.length === 0) return emptyMessageId;
63
+ if (reservation.targetIdentity.length === 0) return { status: "invalid", reason: "empty-target-identity" };
64
+ const route =
65
+ reservation.questionRoute === undefined
66
+ ? null
67
+ : JSON.stringify({
68
+ targetSessionId: reservation.questionRoute.targetSessionId,
69
+ senderGroupIdentity: reservation.questionRoute.senderGroupIdentity,
70
+ });
71
+ return database.transaction(async (tx) => {
72
+ const { changes } = await tx.run(
73
+ `INSERT INTO ${table}(message_id, digest, target_identity, question_route_json, state, recorded_at)
74
+ VALUES (?, ?, ?, ?, 'reserved', ${STORE_NOW}) ON CONFLICT(message_id) DO NOTHING`,
75
+ reservation.messageId,
76
+ reservation.digest,
77
+ reservation.targetIdentity,
78
+ route,
79
+ );
80
+ if (changes === 1) return { status: "reserved" };
81
+ const existing = await read(tx, reservation.messageId);
82
+ if (existing === undefined || existing.digest !== reservation.digest) return { status: "conflict" };
83
+ return { status: "match", state: existing.state };
84
+ });
85
+ },
86
+ async accept(key: SentMessageKey): Promise<SentMessageAcceptResult> {
87
+ if (key.messageId.length === 0) return emptyMessageId;
88
+ return database.transaction(async (tx) => {
89
+ const existing = await read(tx, key.messageId);
90
+ if (existing === undefined) return { status: "missing" };
91
+ if (existing.digest !== key.digest) return { status: "conflict" };
92
+ await tx.run(`UPDATE ${table} SET state = 'accepted' WHERE message_id = ?`, key.messageId);
93
+ return { status: "accepted" };
94
+ });
95
+ },
96
+ async lookup(key: SentMessageKey): Promise<SentMessageLookupResult> {
97
+ if (key.messageId.length === 0) return emptyMessageId;
98
+ const existing = await read(database, key.messageId);
99
+ if (existing === undefined) return { status: "miss" };
100
+ if (existing.digest !== key.digest) return { status: "conflict" };
101
+ return {
102
+ status: "match",
103
+ state: existing.state,
104
+ targetIdentity: existing.target_identity,
105
+ ...questionRouteOf(existing),
106
+ };
107
+ },
108
+ async forget(key: SentMessageKey): Promise<SentMessageForgetResult> {
109
+ if (key.messageId.length === 0) return emptyMessageId;
110
+ return database.transaction(async (tx) => {
111
+ const existing = await read(tx, key.messageId);
112
+ if (existing === undefined) return { status: "missing" };
113
+ if (existing.digest !== key.digest) return { status: "conflict" };
114
+ await tx.run(`DELETE FROM ${table} WHERE message_id = ?`, key.messageId);
115
+ return { status: "forgotten" };
116
+ });
117
+ },
118
+ async prune(olderThanMs: number): Promise<number> {
119
+ const { changes } = await database.run(
120
+ `DELETE FROM ${table} WHERE ${STORE_NOW} - recorded_at > ?`,
121
+ olderThanMs,
122
+ );
123
+ return changes;
124
+ },
125
+ };
126
+ }
@@ -89,6 +89,16 @@ export class SupervisorChannelCache {
89
89
  return authorization.supervisorId;
90
90
  }
91
91
 
92
+ /**
93
+ * The exact child name this capability authorizes `supervisorId` to supervise, or undefined. Only that
94
+ * supervisor may act for the child (#3760); dynamic (`*`) slots name no child yet and never qualify.
95
+ */
96
+ supervisedChildName(capability: string, supervisorId: string): string | undefined {
97
+ const authorization = this.authorizations.get(capability);
98
+ if (!authorization || authorization.supervisorId !== supervisorId || authorization.childName === "*") return undefined;
99
+ return authorization.childName;
100
+ }
101
+
92
102
  /** Record a supervisor-channel crossing keyed by the outbound message id. */
93
103
  record(messageId: string, fromId: string, toId: string, now = Date.now()): void {
94
104
  this.prune(now);
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Wake-up bus seam (specs/2026-10-19-intercom-multi-node.md). A wake-up is only a hint that tells one broker to
3
+ * read the shared store; it never carries a message. Hints may be delayed, coalesced, duplicated or lost, so a
4
+ * broker never depends on them: it scans the store on every `resync` event and on its backup poll.
5
+ */
6
+
7
+ /** `outbox`: rows may be waiting for the broker's recipients. `outcome`: rows its senders wait on may have settled. */
8
+ export type WakeUpReason = "outbox" | "outcome";
9
+
10
+ export interface WakeUpHint {
11
+ /** The broker that should read the store. */
12
+ readonly brokerId: string;
13
+ readonly reason: WakeUpReason;
14
+ /**
15
+ * The outbox `sequence` that prompted the hint (0 when no row did, as for a held-send outcome); diagnostic only.
16
+ * It is not a high-water mark: rows settle out of insertion order, so a receiver scans on every hint and never
17
+ * skips one because of its version.
18
+ */
19
+ readonly version: number;
20
+ }
21
+
22
+ export type WakeUpEvent =
23
+ | { readonly kind: "hint"; readonly hint: WakeUpHint }
24
+ /** Hints may have been missed: on subscribe and after every reconnect, so the broker runs a catch-up scan. */
25
+ | { readonly kind: "resync"; readonly cause: "subscribed" | "reconnected" };
26
+
27
+ export type WakeUpListener = (event: WakeUpEvent) => void;
28
+
29
+ export interface InvalidWakeUpInput {
30
+ readonly status: "invalid";
31
+ readonly reason: "empty-broker-id";
32
+ }
33
+
34
+ export interface WakeUpSubscription {
35
+ readonly brokerId: string;
36
+ /** Stop delivering events to this listener; idempotent. */
37
+ close(): Promise<void>;
38
+ }
39
+
40
+ export type WakeUpPublishResult =
41
+ | { readonly status: "published" }
42
+ /** The bus is unreachable; the hint is dropped and the backup poll covers it. */
43
+ | { readonly status: "unavailable" }
44
+ | InvalidWakeUpInput;
45
+
46
+ export type WakeUpSubscribeResult =
47
+ | { readonly status: "subscribed"; readonly subscription: WakeUpSubscription }
48
+ /** This connection is closed; nothing is subscribed. */
49
+ | { readonly status: "unavailable" }
50
+ | InvalidWakeUpInput;
51
+
52
+ /** S4: one broker's connection to the wake-up bus shared by every broker using the same storage. */
53
+ export interface WakeUpBus {
54
+ /** Best effort; never throws for a transport failure. With no subscriber it is a no-op; after `close` it reports `unavailable`. */
55
+ publish(hint: WakeUpHint): Promise<WakeUpPublishResult>;
56
+ /** Deliver hints addressed to `brokerId`, preceded by a `subscribed` resync. After `close` it reports `unavailable`. */
57
+ subscribe(brokerId: string, listener: WakeUpListener): Promise<WakeUpSubscribeResult>;
58
+ /** Close this connection and its subscriptions; idempotent. */
59
+ close(): Promise<void>;
60
+ }