@bastani/atomic 0.9.39-alpha.2 → 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 +42 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +20 -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 +37 -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 +32 -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
@@ -8,8 +8,13 @@ import {
8
8
  } from "../workflow-stage-target.js";
9
9
  import { normalizeGroup } from "../group.js";
10
10
  import { isMessage } from "./client-message-validation.js";
11
- import { resolveSessionTarget, sessionTargetFailureReason } from "../session-target.js";
11
+ import {
12
+ resolveSessionTarget,
13
+ resolveSessionTargetByHostSessionId,
14
+ sessionTargetFailureReason,
15
+ } from "../session-target.js";
12
16
  import { DeliveredMessageCache } from "./delivered-message-cache.js";
17
+ import { type ReceiptSender, type RecipientReceipts, UNCERTAIN_RECEIPT_REASON } from "./recipient-receipts.js";
13
18
  import { buildMessageSendSignature } from "./send-signature.js";
14
19
  import { SupervisorChannelCache } from "./supervisor-channel.js";
15
20
  import { isVerticalBypass, sameGroup } from "./group-isolation.js";
@@ -34,6 +39,8 @@ export interface BrokerConnectedSession {
34
39
  supervisorId?: string;
35
40
  /** Private issuer identity used to restore child capabilities after reconnects. */
36
41
  supervisorOwnerToken?: string;
42
+ /** #3761: the recipient confirms each message once it has saved it; `delivered` waits for that confirmation. */
43
+ readonly acknowledgesReceipt?: true;
37
44
  }
38
45
 
39
46
  /** Immutable registration authority plus normalized memberships used when an accepted ask is rebound. */
@@ -80,12 +87,56 @@ export type LegacyWorkflowStageTargetResolver = (runId: string, stageKey: string
80
87
  export const PENDING_STAGE_ASK_REFUSAL =
81
88
  "Cannot ask a workflow stage whose session has not initialized. Use send; Atomic will queue the message until the stage session initializes.";
82
89
 
90
+ /**
91
+ * #3760: offered every send or ask that found no live recipient, before it fails with `Session not found`.
92
+ * Returns true when it answered or held the send (a subagent that its owner is restoring).
93
+ */
94
+ export type RestoringRecipientHold = (send: {
95
+ readonly socket: net.Socket;
96
+ readonly from: BrokerConnectedSession;
97
+ readonly clientMessage: SendClientMessage & { readonly to: string };
98
+ readonly target: string;
99
+ readonly messageId: string;
100
+ readonly attemptId?: string;
101
+ }) => boolean;
83
102
 
84
-
85
-
86
- interface SendClientMessage extends Record<string, unknown> {
103
+ export interface SendClientMessage extends Record<string, unknown> {
87
104
  type: string;
88
105
  }
106
+
107
+ /**
108
+ * Multi-node S5: a validated send whose target no local session, local hold or workflow route resolved. The
109
+ * router answers it through the shared state (another broker's recipient, a shared hold or an offline session),
110
+ * or calls `notFound`, which answers exactly as a single-node broker would.
111
+ */
112
+ export interface RemoteSendRequest {
113
+ readonly socket: net.Socket;
114
+ readonly from: BrokerConnectedSession;
115
+ readonly clientMessage: SendClientMessage & { readonly to: string };
116
+ readonly message: Message;
117
+ /** The trimmed `to`. */
118
+ readonly target: string;
119
+ /** The canonical send whose unkeyed SHA-256 is the cross-node digest. */
120
+ readonly signature: string;
121
+ readonly attemptId?: string;
122
+ readonly supervisorSend: boolean;
123
+ readonly requirePendingReply: boolean;
124
+ readonly expectedRecipientId?: string;
125
+ readonly resolveReplyTarget: boolean;
126
+ readonly awaitRestoringRecipient: boolean;
127
+ /**
128
+ * Set when the target resolved to a session on this broker: the router answers a same-id retry of a send that
129
+ * already went through the shared outbox from that row, and calls `notFound` to deliver anything else locally.
130
+ */
131
+ readonly localTarget?: BrokerConnectedSession;
132
+ /** Without `localTarget`, nothing on any node: answer exactly as a single-node broker does. */
133
+ readonly notFound: () => void;
134
+ }
135
+
136
+ /** Local sends the multi-node router has already checked against the shared outbox. */
137
+ const checkedAgainstSharedOutbox = new WeakSet<SendClientMessage>();
138
+
139
+ export type RemoteSendRouter = (request: RemoteSendRequest) => void;
89
140
  function wireMessageId(value: unknown): string {
90
141
  if (typeof value !== "object" || value === null) return "unknown";
91
142
  const id = (value as Record<string, unknown>).id;
@@ -115,6 +166,9 @@ export function handleBrokerSend(
115
166
  resolveLegacyWorkflowStageTarget?: LegacyWorkflowStageTargetResolver,
116
167
  writeConfirmed?: ConfirmedMessageWriter,
117
168
  isNonAgentWorkflowTarget?: (target: string) => boolean,
169
+ holdForRestoringRecipient?: RestoringRecipientHold,
170
+ receipts?: RecipientReceipts<BrokerConnectedSession>,
171
+ routeRemote?: RemoteSendRouter,
118
172
  ): void {
119
173
  const message = clientMessage.message;
120
174
  const messageId = wireMessageId(message);
@@ -286,9 +340,76 @@ export function handleBrokerSend(
286
340
  return;
287
341
  }
288
342
  }
289
- const resolution = exactIdTarget
343
+ const nameOrIdResolution = exactIdTarget
290
344
  ? ({ kind: "resolved", session: exactIdTarget.info } as const)
291
345
  : resolveSessionTarget(candidates, trimmedTo);
346
+ // #3760: a host session id (a resumed session's or a restored subagent's own session id) reaches the one
347
+ // visible session it registered, under the same group visibility as names.
348
+ const resolution = nameOrIdResolution.kind === "not_found"
349
+ ? resolveSessionTargetByHostSessionId(
350
+ candidates,
351
+ trimmedTo,
352
+ (info) => sessions.get(info.id)?.registrationReturnAddress,
353
+ )
354
+ : nameOrIdResolution;
355
+ if (
356
+ routeRemote !== undefined &&
357
+ resolution.kind === "not_found" &&
358
+ workflowTarget === undefined &&
359
+ !visibleCandidates.some((info) => !isAgentRecipient(info) && info.name?.toLowerCase() === trimmedTo.toLowerCase())
360
+ ) {
361
+ const send = clientMessage as SendClientMessage & { readonly to: string };
362
+ if (
363
+ !supervisorSend &&
364
+ !requirePendingReply &&
365
+ expectedRecipientId === undefined &&
366
+ holdForRestoringRecipient?.({
367
+ socket,
368
+ from: fromSession,
369
+ clientMessage: send,
370
+ target: trimmedTo,
371
+ messageId: message.id,
372
+ ...(attemptId ? { attemptId } : {}),
373
+ })
374
+ ) {
375
+ return;
376
+ }
377
+ routeRemote({
378
+ socket,
379
+ from: fromSession,
380
+ clientMessage: send,
381
+ message,
382
+ target: trimmedTo,
383
+ signature,
384
+ ...(attemptId === undefined ? {} : { attemptId }),
385
+ supervisorSend,
386
+ requirePendingReply,
387
+ ...(expectedRecipientId === undefined ? {} : { expectedRecipientId }),
388
+ resolveReplyTarget: clientMessage.resolveReplyTarget === true,
389
+ awaitRestoringRecipient: clientMessage.awaitRestoringRecipient === true,
390
+ // Nothing on any node: answer exactly as a single-node broker does.
391
+ notFound: () =>
392
+ handleBrokerSend(
393
+ socket,
394
+ clientMessage,
395
+ currentId,
396
+ sessions,
397
+ deliveredMessages,
398
+ write,
399
+ supervisorCache,
400
+ pendingQuestions,
401
+ routePendingStage,
402
+ resolveLiveWorkflowStage,
403
+ canControlLiveWorkflowStage,
404
+ resolveLegacyWorkflowStageTarget,
405
+ writeConfirmed,
406
+ isNonAgentWorkflowTarget,
407
+ holdForRestoringRecipient,
408
+ receipts,
409
+ ),
410
+ });
411
+ return;
412
+ }
292
413
  if (expectedRecipientId !== undefined && (resolution.kind !== "resolved" || resolution.session.id !== expectedRecipientId)) {
293
414
  write(socket, { type: "delivery_failed", messageId, attemptId,
294
415
  reason: resolution.kind === "resolved" ? "Reply target does not match the expected recipient" : sessionTargetFailureReason(clientMessage.to, resolution),
@@ -315,8 +436,52 @@ export function handleBrokerSend(
315
436
  write(socket, { type: "delivery_failed", messageId: message.id, attemptId, reason: "Cannot message the current session" });
316
437
  return;
317
438
  }
439
+ if (routeRemote !== undefined && !checkedAgainstSharedOutbox.delete(clientMessage)) {
440
+ // Multi-node: a retry whose first attempt reached this recipient through another broker is answered from the
441
+ // shared outbox row; `DeliveredMessageCache` never saw that delivery.
442
+ routeRemote({
443
+ socket,
444
+ from: fromSession,
445
+ clientMessage: clientMessage as SendClientMessage & { readonly to: string },
446
+ message,
447
+ target: trimmedTo,
448
+ signature,
449
+ ...(attemptId === undefined ? {} : { attemptId }),
450
+ supervisorSend,
451
+ requirePendingReply,
452
+ ...(expectedRecipientId === undefined ? {} : { expectedRecipientId }),
453
+ resolveReplyTarget: clientMessage.resolveReplyTarget === true,
454
+ awaitRestoringRecipient: clientMessage.awaitRestoringRecipient === true,
455
+ localTarget: target,
456
+ notFound: () => {
457
+ checkedAgainstSharedOutbox.add(clientMessage);
458
+ handleBrokerSend(
459
+ socket,
460
+ clientMessage,
461
+ currentId,
462
+ sessions,
463
+ deliveredMessages,
464
+ write,
465
+ supervisorCache,
466
+ pendingQuestions,
467
+ routePendingStage,
468
+ resolveLiveWorkflowStage,
469
+ canControlLiveWorkflowStage,
470
+ resolveLegacyWorkflowStageTarget,
471
+ writeConfirmed,
472
+ isNonAgentWorkflowTarget,
473
+ holdForRestoringRecipient,
474
+ receipts,
475
+ routeRemote,
476
+ );
477
+ },
478
+ });
479
+ return;
480
+ }
318
481
  const targetIdentity = deliveryTargetIdentity(target);
319
482
  const deliveredMatch = deliveredMessages.lookupForTarget(message.id, signature, targetIdentity);
483
+ // #3761: a recipient that confirms saves ignores a copy it already saved, so a reserved message is sent again.
484
+ const confirmsSaves = target.acknowledgesReceipt === true && receipts !== undefined;
320
485
  if (requirePendingReply && deliveredMatch === "match") {
321
486
  write(socket, { type: "delivered", messageId: message.id, attemptId });
322
487
  return;
@@ -340,7 +505,7 @@ export function handleBrokerSend(
340
505
  });
341
506
  return;
342
507
  }
343
- if (requirePendingReply && deliveredMatch === "uncertain") {
508
+ if (requirePendingReply && deliveredMatch === "uncertain" && !confirmsSaves) {
344
509
  write(socket, {
345
510
  type: "delivery_failed",
346
511
  messageId: message.id,
@@ -427,7 +592,7 @@ export function handleBrokerSend(
427
592
  });
428
593
  return;
429
594
  }
430
- if (deliveredMatch === "uncertain") {
595
+ if (deliveredMatch === "uncertain" && !confirmsSaves) {
431
596
  write(socket, {
432
597
  type: "delivery_failed",
433
598
  messageId: message.id,
@@ -501,7 +666,7 @@ export function handleBrokerSend(
501
666
  write(socket, { type: "delivered", messageId: message.id, attemptId });
502
667
  return;
503
668
  }
504
- if (reservation !== "recorded") {
669
+ if (reservation !== "recorded" && !(reservation === "uncertain" && confirmsSaves)) {
505
670
  write(socket, {
506
671
  type: "delivery_failed",
507
672
  messageId: message.id,
@@ -517,24 +682,28 @@ export function handleBrokerSend(
517
682
  }
518
683
  if (message.expectsReply === true && clientMessage.resolveReplyTarget === true && attemptId !== undefined)
519
684
  write(socket, { type: "question_target", messageId: message.id, attemptId, sessionId: target.info.id });
520
- const finishDelivery = (): void => {
685
+ const finishDelivery = (recipient: BrokerConnectedSession, sender: ReceiptSender | undefined): void => {
686
+ const notify = (frame: BrokerMessage): void => {
687
+ if (sender !== undefined) write(sender.socket, frame);
688
+ };
689
+ const senderAttemptId = sender?.attemptId;
521
690
  let accepted: ReturnType<DeliveredMessageCache["accept"]>;
522
691
  try {
523
692
  accepted = deliveredMessages.accept(message.id, signature, targetIdentity);
524
693
  } catch {
525
- write(socket, {
694
+ notify({
526
695
  type: "delivery_failed",
527
696
  messageId: message.id,
528
- attemptId,
697
+ attemptId: senderAttemptId,
529
698
  reason: "Intercom could not durably confirm the forwarded operation; refusing a possibly unsafe retry",
530
699
  });
531
700
  return;
532
701
  }
533
702
  if (accepted !== "accepted") {
534
- write(socket, {
703
+ notify({
535
704
  type: "delivery_failed",
536
705
  messageId: message.id,
537
- attemptId,
706
+ attemptId: senderAttemptId,
538
707
  reason: "Intercom could not durably confirm the forwarded operation; refusing a possibly unsafe retry",
539
708
  ...(accepted === "conflict" ? { reasonCode: "message_id_conflict" as const } : {}),
540
709
  });
@@ -542,27 +711,58 @@ export function handleBrokerSend(
542
711
  }
543
712
  // A terminal presence frame can arrive while the socket write callback is pending.
544
713
  // Retain accepted-delivery authority, but never open a reply route after termination.
545
- if (message.expectsReply === true && target.info.replyCapability === "terminal") {
546
- write(socket, { type: "delivery_failed", messageId: message.id, attemptId, reason: TERMINAL_CHILD_ASK_REFUSAL });
714
+ if (message.expectsReply === true && recipient.info.replyCapability === "terminal") {
715
+ notify({ type: "delivery_failed", messageId: message.id, attemptId: senderAttemptId, reason: TERMINAL_CHILD_ASK_REFUSAL });
547
716
  return;
548
717
  }
549
- if (message.expectsReply === true && sessions.get(target.info.id) !== target) {
550
- write(socket, { type: "delivery_failed", messageId: message.id, attemptId,
551
- reason: `Session "${target.info.name ?? target.info.id}" disconnected before replying` });
718
+ if (message.expectsReply === true && sessions.get(recipient.info.id) !== recipient) {
719
+ notify({ type: "delivery_failed", messageId: message.id, attemptId: senderAttemptId,
720
+ reason: `Session "${recipient.info.name ?? recipient.info.id}" disconnected before replying` });
552
721
  return;
553
722
  }
554
- if (message.expectsReply === true) {
555
- pendingQuestions.record(fromSession.info.id, target.info.id, message.id);
723
+ // A sender whose wait already ended was told the outcome is uncertain; its retry opens the reply route.
724
+ if (message.expectsReply === true && sender !== undefined) {
725
+ pendingQuestions.record(fromSession.info.id, recipient.info.id, message.id);
726
+ // #3761: a recipient that reconnected before confirming answers under its new session id.
727
+ if (recipient.info.id !== target.info.id && clientMessage.resolveReplyTarget === true && senderAttemptId !== undefined)
728
+ notify({ type: "question_target", messageId: message.id, attemptId: senderAttemptId, sessionId: recipient.info.id });
556
729
  }
557
- if (message.replyTo !== undefined && pendingQuestions.clearReply(fromSession.info.id, target.info.id, message.replyTo)) {
730
+ if (message.replyTo !== undefined && pendingQuestions.clearReply(fromSession.info.id, recipient.info.id, message.replyTo)) {
558
731
  deliveredMessages.settleQuestion(message.replyTo);
559
732
  }
560
- if (supervisorSend) supervisorCache.record(message.id, fromSession.info.id, target.info.id);
561
- write(socket, { type: "delivered", messageId: message.id, attemptId });
733
+ if (supervisorSend) supervisorCache.record(message.id, fromSession.info.id, recipient.info.id);
734
+ notify({ type: "delivered", messageId: message.id, attemptId: senderAttemptId });
562
735
  };
736
+ const senderWaiting: ReceiptSender = { socket, ...(attemptId === undefined ? {} : { attemptId }) };
737
+ if (confirmsSaves && receipts !== undefined) {
738
+ receipts.await(
739
+ {
740
+ messageId: message.id,
741
+ targetIdentity,
742
+ frame: outbound,
743
+ complete: finishDelivery,
744
+ expire: (waiting) => {
745
+ write(waiting.socket, { type: "delivery_failed", messageId: message.id, attemptId: waiting.attemptId, reason: UNCERTAIN_RECEIPT_REASON });
746
+ },
747
+ },
748
+ senderWaiting,
749
+ );
750
+ const unwritten = (): void => {
751
+ receipts.drop(message.id);
752
+ failDelivery();
753
+ };
754
+ if (writeConfirmed !== undefined) {
755
+ writeConfirmed(target.socket, outbound, (written) => {
756
+ if (!written) unwritten();
757
+ });
758
+ return;
759
+ }
760
+ if (!write(target.socket, outbound)) unwritten();
761
+ return;
762
+ }
563
763
  if (writeConfirmed !== undefined) {
564
764
  writeConfirmed(target.socket, outbound, (written) => {
565
- if (written) finishDelivery();
765
+ if (written) finishDelivery(target, senderWaiting);
566
766
  else failDelivery();
567
767
  });
568
768
  return;
@@ -573,13 +773,29 @@ export function handleBrokerSend(
573
773
  failDelivery();
574
774
  return;
575
775
  }
576
- finishDelivery();
776
+ finishDelivery(target, senderWaiting);
577
777
  return;
578
778
  }
579
779
  if (resolution.kind === "not_found" && visibleCandidates.some((info) => !isAgentRecipient(info) && info.name?.toLowerCase() === trimmedTo.toLowerCase())) {
580
780
  write(socket, { type: "delivery_failed", messageId: message.id, attemptId, reason: NON_AGENT_RECIPIENT_REFUSAL });
581
781
  return;
582
782
  }
783
+ if (
784
+ resolution.kind === "not_found" &&
785
+ !supervisorSend &&
786
+ !requirePendingReply &&
787
+ workflowTarget === undefined &&
788
+ holdForRestoringRecipient?.({
789
+ socket,
790
+ from: fromSession,
791
+ clientMessage: clientMessage as SendClientMessage & { readonly to: string },
792
+ target: trimmedTo,
793
+ messageId: message.id,
794
+ ...(attemptId ? { attemptId } : {}),
795
+ })
796
+ ) {
797
+ return;
798
+ }
583
799
  if (
584
800
  resolution.kind === "not_found" &&
585
801
  !supervisorSend &&
@@ -0,0 +1,41 @@
1
+ /** Broker session ids are lowercase canonical UUIDs; only that form may be requested back. */
2
+ const CANONICAL_SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
3
+ const DEFAULT_MAX_ENTRIES = 10_000;
4
+
5
+ /**
6
+ * Lets a restarted or reconnected session register under the Intercom id peers already know (#3760).
7
+ *
8
+ * A requested id is granted only when no live session holds it, so a registration never displaces a
9
+ * live session. While this broker remembers who last held an id, only a registration with that same
10
+ * return address (the host's session identity) gets it back. A broker that restarted remembers
11
+ * nothing; its sessions and their routing state are gone too, so a free id carries no authority.
12
+ */
13
+ export class SessionIdReclaim {
14
+ private readonly retired = new Map<string, string | undefined>();
15
+
16
+ constructor(private readonly maxEntries = DEFAULT_MAX_ENTRIES) {}
17
+
18
+ /** Remember who held a departed session id. */
19
+ retire(sessionId: string, returnAddress: string | undefined): void {
20
+ this.retired.delete(sessionId);
21
+ this.retired.set(sessionId, returnAddress);
22
+ while (this.retired.size > this.maxEntries) {
23
+ const oldest = this.retired.keys().next().value as string | undefined;
24
+ if (oldest === undefined) break;
25
+ this.retired.delete(oldest);
26
+ }
27
+ }
28
+
29
+ /** The requested id when it can be granted, otherwise undefined (the caller assigns a fresh id). */
30
+ grant(
31
+ preferredSessionId: string | undefined,
32
+ returnAddress: string | undefined,
33
+ isLive: (sessionId: string) => boolean,
34
+ ): string | undefined {
35
+ if (preferredSessionId === undefined || returnAddress === undefined) return undefined;
36
+ if (!CANONICAL_SESSION_ID.test(preferredSessionId) || isLive(preferredSessionId)) return undefined;
37
+ if (this.retired.has(preferredSessionId) && this.retired.get(preferredSessionId) !== returnAddress) return undefined;
38
+ this.retired.delete(preferredSessionId);
39
+ return preferredSessionId;
40
+ }
41
+ }
@@ -58,6 +58,15 @@ export type BrokerOutboundFrame =
58
58
  notInKnownSet?: true;
59
59
  };
60
60
 
61
+ /** A frame written while its socket's writes were held, with the outcome callback of a confirmed write. */
62
+ export interface HeldWrite {
63
+ readonly message: BrokerOutboundFrame;
64
+ readonly onSettled?: (written: boolean) => void;
65
+ }
66
+
67
+ /** Multi-node S5: sockets whose frames are held until their deferred `registered` is written. */
68
+ const heldWrites = new WeakMap<net.Socket, HeldWrite[]>();
69
+
61
70
  /**
62
71
  * Write one broker frame unless the socket's writable side is already gone.
63
72
  *
@@ -69,6 +78,11 @@ export type BrokerOutboundFrame =
69
78
  */
70
79
  export function writeMessageIfOpen(socket: net.Socket, message: BrokerOutboundFrame): boolean {
71
80
  if (!isSocketOpenForWrite(socket)) return false;
81
+ const held = heldWrites.get(socket);
82
+ if (held !== undefined) {
83
+ held.push({ message });
84
+ return true;
85
+ }
72
86
  writeMessage(socket, message);
73
87
  return true;
74
88
  }
@@ -89,6 +103,11 @@ export function writeMessageWithOutcome(
89
103
  onSettled(false);
90
104
  return;
91
105
  }
106
+ const held = heldWrites.get(socket);
107
+ if (held !== undefined) {
108
+ held.push({ message, onSettled });
109
+ return;
110
+ }
92
111
  let settled = false;
93
112
  const finish = (written: boolean): void => {
94
113
  if (settled) return;
@@ -101,3 +120,31 @@ export function writeMessageWithOutcome(
101
120
  finish(false);
102
121
  }
103
122
  }
123
+
124
+ /**
125
+ * Multi-node S5: hold every frame written to `socket` until `releaseHeldWrites`. A session whose registration
126
+ * completes asynchronously is already routable, but its client accepts no frame before `registered`.
127
+ */
128
+ export function holdWrites(socket: net.Socket): void {
129
+ if (!heldWrites.has(socket)) heldWrites.set(socket, []);
130
+ }
131
+
132
+ /** Stop holding frames for `socket`; returns the frames held, in the order they were written. */
133
+ export function releaseHeldWrites(socket: net.Socket): HeldWrite[] {
134
+ const held = heldWrites.get(socket) ?? [];
135
+ heldWrites.delete(socket);
136
+ return held;
137
+ }
138
+
139
+ /** Write released frames in order; a confirmed write to a closed socket reports `false`, as any write does. */
140
+ export function flushHeldWrites(socket: net.Socket, held: readonly HeldWrite[]): void {
141
+ for (const { message, onSettled } of held) {
142
+ if (onSettled === undefined) writeMessageIfOpen(socket, message);
143
+ else writeMessageWithOutcome(socket, message, onSettled);
144
+ }
145
+ }
146
+
147
+ /** Drop released frames that will never be written; each confirmed write reports `false`. */
148
+ export function discardHeldWrites(held: readonly HeldWrite[]): void {
149
+ for (const { onSettled } of held) onSettled?.(false);
150
+ }
@@ -4,8 +4,9 @@ import { join, dirname } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import { createRequire } from "module";
6
6
  import { isSocketAnswering } from "./socket-liveness.js";
7
- import { createChildProcessEnvironment, isBunBinary } from "@bastani/atomic";
7
+ import { createChildProcessEnvironment, isBunBinary, processSharedStorageSelection } from "@bastani/atomic";
8
8
  import {
9
+ BROKER_STORAGE_ENV,
9
10
  getBrokerLogPath,
10
11
  getBrokerPidPath,
11
12
  getBrokerSocketPath,
@@ -243,6 +244,9 @@ export function getBrokerLaunchSpec(
243
244
  * The broker is detached and outlives this process, so its stderr goes to an already-open file
244
245
  * descriptor rather than a pipe: a long-lived pipe would keep the parent's event loop attached
245
246
  * and break once the parent exits.
247
+ *
248
+ * The broker routes through the shared storage this process was launched with (`--storage`,
249
+ * `--storage-file` or `ATOMIC_STORAGE`), passed as `ATOMIC_STORAGE`; without one it reads settings.
246
250
  */
247
251
  export function getBrokerSpawnOptions(
248
252
  extensionDir: string = EXTENSION_DIR,
@@ -254,11 +258,15 @@ export function getBrokerSpawnOptions(
254
258
  env: NodeJS.ProcessEnv;
255
259
  windowsHide: true;
256
260
  } {
261
+ const storage = processSharedStorageSelection();
257
262
  return {
258
263
  detached: true,
259
264
  stdio: ["ignore", "ignore", stderr],
260
265
  cwd: extensionDir,
261
- env: createChildProcessEnvironment({ NODE_NO_WARNINGS: "1" }),
266
+ env: createChildProcessEnvironment({
267
+ NODE_NO_WARNINGS: "1",
268
+ ...(storage === undefined ? {} : { [BROKER_STORAGE_ENV]: storage }),
269
+ }),
262
270
  windowsHide: true,
263
271
  };
264
272
  }
@@ -0,0 +1,96 @@
1
+ import { type SqliteRow, validateStoragePrefix } from "@bastani/atomic/storage";
2
+ import type {
3
+ HeldSend,
4
+ HeldSendDropResult,
5
+ HeldSendHoldResult,
6
+ HeldSendListResult,
7
+ HeldSendRequest,
8
+ HeldSendStore,
9
+ HeldSendTakeResult,
10
+ IntercomBrokerStateOptions,
11
+ InvalidStateInput,
12
+ StateStoreInvalidReason,
13
+ } from "./multinode-state-store.js";
14
+
15
+ type HeldRow = SqliteRow & {
16
+ reservation_key: string;
17
+ origin_broker_id: string;
18
+ sender_session_id: string;
19
+ request_json: string;
20
+ };
21
+
22
+ const STORE_NOW = "CAST(unixepoch('subsec') * 1000 AS INTEGER)";
23
+
24
+ const invalid = (reason: StateStoreInvalidReason): InvalidStateInput => ({ status: "invalid", reason });
25
+
26
+ function heldSendOf(row: HeldRow): HeldSend {
27
+ return {
28
+ reservationKey: row.reservation_key,
29
+ originBrokerId: row.origin_broker_id,
30
+ senderSessionId: row.sender_session_id,
31
+ request: JSON.parse(row.request_json) as HeldSendRequest,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * S3: `HeldSendStore` on the shared storage adapter's database, table `<prefix>intercom_held_sends`. Sends are keyed
37
+ * by `reservationKeyFor(capability)`; the capability itself is never stored.
38
+ */
39
+ export async function openSqlHeldSendStore({ database, prefix }: IntercomBrokerStateOptions): Promise<HeldSendStore> {
40
+ const table = `${validateStoragePrefix(prefix)}intercom_held_sends`;
41
+ await database.transaction((tx) =>
42
+ tx.exec(`
43
+ CREATE TABLE IF NOT EXISTS ${table}(
44
+ "order" INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ message_id TEXT NOT NULL UNIQUE,
46
+ reservation_key TEXT NOT NULL,
47
+ origin_broker_id TEXT NOT NULL,
48
+ sender_session_id TEXT NOT NULL,
49
+ request_json TEXT NOT NULL,
50
+ held_at INTEGER NOT NULL
51
+ );
52
+ CREATE INDEX IF NOT EXISTS ${table}_reservation ON ${table}(reservation_key, "order");
53
+ `),
54
+ );
55
+ const selectForReservation = `SELECT reservation_key, origin_broker_id, sender_session_id, request_json FROM ${table}
56
+ WHERE reservation_key = ? ORDER BY "order"`;
57
+
58
+ return {
59
+ async hold(send): Promise<HeldSendHoldResult> {
60
+ if (send.reservationKey.length === 0) return invalid("empty-reservation-key");
61
+ if (send.originBrokerId.length === 0) return invalid("empty-broker-id");
62
+ if (send.senderSessionId.length === 0) return invalid("empty-session-id");
63
+ const messageId = send.request.message.id;
64
+ if (messageId.length === 0) return invalid("empty-message-id");
65
+ const { changes } = await database.run(
66
+ `INSERT INTO ${table}(message_id, reservation_key, origin_broker_id, sender_session_id, request_json, held_at)
67
+ VALUES (?, ?, ?, ?, ?, ${STORE_NOW}) ON CONFLICT(message_id) DO NOTHING`,
68
+ messageId,
69
+ send.reservationKey,
70
+ send.originBrokerId,
71
+ send.senderSessionId,
72
+ JSON.stringify(send.request),
73
+ );
74
+ return changes === 0 ? { status: "duplicate" } : { status: "held" };
75
+ },
76
+ async list(reservationKey): Promise<HeldSendListResult> {
77
+ if (reservationKey.length === 0) return invalid("empty-reservation-key");
78
+ const rows = await database.all<HeldRow>(selectForReservation, reservationKey);
79
+ return { status: "listed", sends: rows.map(heldSendOf) };
80
+ },
81
+ async take(reservationKey): Promise<HeldSendTakeResult> {
82
+ if (reservationKey.length === 0) return invalid("empty-reservation-key");
83
+ const rows = await database.transaction(async (tx) => {
84
+ const taken = await tx.all<HeldRow>(selectForReservation, reservationKey);
85
+ await tx.run(`DELETE FROM ${table} WHERE reservation_key = ?`, reservationKey);
86
+ return taken;
87
+ });
88
+ return { status: "taken", sends: rows.map(heldSendOf) };
89
+ },
90
+ async drop(messageId): Promise<HeldSendDropResult> {
91
+ if (messageId.length === 0) return invalid("empty-message-id");
92
+ const { changes } = await database.run(`DELETE FROM ${table} WHERE message_id = ?`, messageId);
93
+ return changes === 0 ? { status: "missing" } : { status: "dropped" };
94
+ },
95
+ };
96
+ }