@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,129 @@
1
+ import { type SqliteRow, validateStoragePrefix } from "@bastani/atomic/storage";
2
+ import type {
3
+ IntercomBrokerStateOptions,
4
+ KnownSessionLookupResult,
5
+ KnownSessionRecordResult,
6
+ KnownSessionState,
7
+ KnownSessionStore,
8
+ } from "./multinode-state-store.js";
9
+
10
+ const STORE_NOW = "CAST(unixepoch('subsec') * 1000 AS INTEGER)";
11
+
12
+ type KnownRow = SqliteRow & {
13
+ target_identity: string;
14
+ intercom_session_id: string;
15
+ name_json: string | null;
16
+ atomic_session_id: string | null;
17
+ group_name: string;
18
+ groups_json: string;
19
+ recipient_purpose: string | null;
20
+ reply_capability: string | null;
21
+ age_ms: number | bigint;
22
+ };
23
+
24
+ function stateFromRow(row: KnownRow): KnownSessionState {
25
+ return {
26
+ targetIdentity: row.target_identity,
27
+ intercomSessionId: row.intercom_session_id,
28
+ ...(row.name_json === null ? {} : { name: JSON.parse(row.name_json) as string }),
29
+ ...(row.atomic_session_id === null ? {} : { atomicSessionId: row.atomic_session_id }),
30
+ group: row.group_name,
31
+ groups: JSON.parse(row.groups_json) as string[],
32
+ ...(row.recipient_purpose === null
33
+ ? {}
34
+ : { recipientPurpose: row.recipient_purpose as NonNullable<KnownSessionState["recipientPurpose"]> }),
35
+ ...(row.reply_capability === null
36
+ ? {}
37
+ : { replyCapability: row.reply_capability as NonNullable<KnownSessionState["replyCapability"]> }),
38
+ ageMs: Number(row.age_ms),
39
+ };
40
+ }
41
+
42
+ /**
43
+ * S5 amendment (spec §14): `KnownSessionStore` on the shared storage adapter's database, table
44
+ * `<prefix>intercom_known_sessions`. Names are stored as JSON so any text survives; `name_key` holds the
45
+ * `toLowerCase` form the lookup compares.
46
+ */
47
+ export async function openSqlKnownSessionStore({
48
+ database,
49
+ prefix,
50
+ }: IntercomBrokerStateOptions): Promise<KnownSessionStore> {
51
+ const table = `${validateStoragePrefix(prefix)}intercom_known_sessions`;
52
+ await database.transaction((tx) =>
53
+ tx.exec(`
54
+ CREATE TABLE IF NOT EXISTS ${table}(
55
+ record_order INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ target_identity TEXT NOT NULL UNIQUE,
57
+ intercom_session_id TEXT NOT NULL,
58
+ name_json TEXT,
59
+ name_key TEXT,
60
+ atomic_session_id TEXT,
61
+ group_name TEXT NOT NULL,
62
+ groups_json TEXT NOT NULL,
63
+ recipient_purpose TEXT,
64
+ reply_capability TEXT,
65
+ recorded_at INTEGER NOT NULL
66
+ );
67
+ CREATE INDEX IF NOT EXISTS ${table}_intercom_id ON ${table}(intercom_session_id);
68
+ CREATE INDEX IF NOT EXISTS ${table}_atomic_id ON ${table}(atomic_session_id);
69
+ CREATE INDEX IF NOT EXISTS ${table}_name ON ${table}(name_key);
70
+ CREATE INDEX IF NOT EXISTS ${table}_recorded ON ${table}(recorded_at);
71
+ `),
72
+ );
73
+
74
+ return {
75
+ async record(session): Promise<KnownSessionRecordResult> {
76
+ if (session.targetIdentity.length === 0) return { status: "invalid", reason: "empty-target-identity" };
77
+ if (session.intercomSessionId.length === 0) return { status: "invalid", reason: "empty-session-id" };
78
+ if (session.group.length === 0) return { status: "invalid", reason: "empty-group" };
79
+ await database.run(
80
+ `INSERT INTO ${table}(target_identity, intercom_session_id, name_json, name_key, atomic_session_id,
81
+ group_name, groups_json, recipient_purpose, reply_capability, recorded_at)
82
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ${STORE_NOW})
83
+ ON CONFLICT(target_identity) DO UPDATE SET
84
+ intercom_session_id = excluded.intercom_session_id,
85
+ name_json = excluded.name_json,
86
+ name_key = excluded.name_key,
87
+ atomic_session_id = excluded.atomic_session_id,
88
+ group_name = excluded.group_name,
89
+ groups_json = excluded.groups_json,
90
+ recipient_purpose = excluded.recipient_purpose,
91
+ reply_capability = excluded.reply_capability,
92
+ recorded_at = excluded.recorded_at`,
93
+ session.targetIdentity,
94
+ session.intercomSessionId,
95
+ session.name === undefined ? null : JSON.stringify(session.name),
96
+ session.name === undefined ? null : JSON.stringify(session.name.toLowerCase()),
97
+ session.atomicSessionId ?? null,
98
+ session.group,
99
+ JSON.stringify([...session.groups]),
100
+ session.recipientPurpose ?? null,
101
+ session.replyCapability ?? null,
102
+ );
103
+ return { status: "recorded" };
104
+ },
105
+ async lookup(target, retentionMs): Promise<KnownSessionLookupResult> {
106
+ if (target.length === 0) return { status: "invalid", reason: "empty-target" };
107
+ const rows = await database.all<KnownRow>(
108
+ `SELECT target_identity, intercom_session_id, name_json, atomic_session_id, group_name, groups_json,
109
+ recipient_purpose, reply_capability, MAX(0, ${STORE_NOW} - recorded_at) AS age_ms
110
+ FROM ${table}
111
+ WHERE (intercom_session_id = ? OR atomic_session_id = ? OR name_key = ?)
112
+ AND MAX(0, ${STORE_NOW} - recorded_at) <= ?
113
+ ORDER BY record_order`,
114
+ target,
115
+ target,
116
+ JSON.stringify(target.toLowerCase()),
117
+ retentionMs,
118
+ );
119
+ return { status: "found", sessions: rows.map(stateFromRow) };
120
+ },
121
+ async prune(olderThanMs): Promise<number> {
122
+ const { changes } = await database.run(
123
+ `DELETE FROM ${table} WHERE ${STORE_NOW} - recorded_at > ?`,
124
+ olderThanMs,
125
+ );
126
+ return changes;
127
+ },
128
+ };
129
+ }
@@ -0,0 +1,194 @@
1
+ import { type SqliteExecutor, type SqliteRow, validateStoragePrefix } from "@bastani/atomic/storage";
2
+ import type {
3
+ BrokerLeaseStore,
4
+ IntercomBrokerStateOptions,
5
+ InvalidStateInput,
6
+ Lease,
7
+ LeaseKey,
8
+ LeaseKind,
9
+ LeaseState,
10
+ LeaseSubject,
11
+ StateStoreInvalidReason,
12
+ } from "./multinode-state-store.js";
13
+
14
+ const STORE_NOW = "CAST(unixepoch('subsec') * 1000 AS INTEGER)";
15
+ const AGE = `MAX(0, ${STORE_NOW} - renewed_at)`;
16
+
17
+ type HolderRow = SqliteRow & {
18
+ holder_id: string | null;
19
+ fencing_token: number | bigint | null;
20
+ last_token: number | bigint;
21
+ age_ms: number | bigint | null;
22
+ };
23
+
24
+ type StateRow = SqliteRow & {
25
+ kind: string;
26
+ key: string;
27
+ holder_id: string;
28
+ fencing_token: number | bigint;
29
+ subject_json: string;
30
+ age_ms: number | bigint;
31
+ };
32
+
33
+ const invalid = (reason: StateStoreInvalidReason): InvalidStateInput => ({ status: "invalid", reason });
34
+
35
+ function keyOf(subject: LeaseSubject): LeaseKey {
36
+ return subject.kind === "broker"
37
+ ? { kind: "broker", brokerId: subject.brokerId }
38
+ : { kind: "presence", targetIdentity: subject.targetIdentity };
39
+ }
40
+
41
+ function keyId(key: LeaseKey): string {
42
+ return key.kind === "broker" ? key.brokerId : key.targetIdentity;
43
+ }
44
+
45
+ function keyFor(kind: LeaseKind, id: string): LeaseKey {
46
+ return kind === "broker" ? { kind, brokerId: id } : { kind, targetIdentity: id };
47
+ }
48
+
49
+ function invalidKey(key: LeaseKey): InvalidStateInput | undefined {
50
+ if (keyId(key).length > 0) return undefined;
51
+ return invalid(key.kind === "broker" ? "empty-broker-id" : "empty-target-identity");
52
+ }
53
+
54
+ function stateFromRow(row: StateRow): LeaseState {
55
+ const subject: LeaseSubject = JSON.parse(row.subject_json);
56
+ return {
57
+ key: keyFor(row.kind === "broker" ? "broker" : "presence", row.key),
58
+ holderId: row.holder_id,
59
+ fencingToken: Number(row.fencing_token),
60
+ subject,
61
+ ageMs: Number(row.age_ms),
62
+ };
63
+ }
64
+
65
+ /** Opens the S1 lease store on the shared database's `<prefix>intercom_leases` table; the database stays open. */
66
+ export async function openSqlBrokerLeaseStore(options: IntercomBrokerStateOptions): Promise<BrokerLeaseStore> {
67
+ const { database } = options;
68
+ const table = `${validateStoragePrefix(options.prefix)}intercom_leases`;
69
+ await database.exec(`
70
+ CREATE TABLE IF NOT EXISTS ${table}(
71
+ kind TEXT NOT NULL, key TEXT NOT NULL, holder_id TEXT, fencing_token INTEGER,
72
+ last_token INTEGER NOT NULL, subject_json TEXT, renewed_at INTEGER,
73
+ first_acquired_order INTEGER NOT NULL, PRIMARY KEY(kind, key)
74
+ )
75
+ `);
76
+
77
+ const holderOf = (executor: SqliteExecutor, key: LeaseKey) =>
78
+ executor.get<HolderRow>(
79
+ `SELECT holder_id, fencing_token, last_token, ${AGE} AS age_ms FROM ${table} WHERE kind = ? AND key = ?`,
80
+ key.kind,
81
+ keyId(key),
82
+ );
83
+ const stale = (row: HolderRow | undefined) =>
84
+ row?.holder_id === null || row?.holder_id === undefined
85
+ ? ({ status: "stale" } as const)
86
+ : ({ status: "stale", currentFencingToken: Number(row.fencing_token) } as const);
87
+ const validLease = (lease: Lease): InvalidStateInput | undefined =>
88
+ invalidKey(lease.key) ?? (lease.holderId.length === 0 ? invalid("empty-holder-id") : undefined);
89
+ const owned = `kind = ? AND key = ? AND holder_id = ? AND fencing_token = ?`;
90
+ const ownedParams = (lease: Lease) => [lease.key.kind, keyId(lease.key), lease.holderId, lease.fencingToken];
91
+
92
+ return {
93
+ async acquire(subject, holderId, staleAfterMs) {
94
+ const key = keyOf(subject);
95
+ const bad = invalidKey(key);
96
+ if (bad !== undefined) return bad;
97
+ if (holderId.length === 0) return invalid("empty-holder-id");
98
+ const subjectJson = JSON.stringify(subject);
99
+ return database.transaction(async (tx) => {
100
+ const row = await holderOf(tx, key);
101
+ if (row === undefined) {
102
+ await tx.run(
103
+ `INSERT INTO ${table}(kind, key, holder_id, fencing_token, last_token, subject_json, renewed_at,
104
+ first_acquired_order)
105
+ VALUES (?, ?, ?, 1, 1, ?, ${STORE_NOW},
106
+ (SELECT COALESCE(MAX(first_acquired_order), 0) + 1 FROM ${table}))`,
107
+ key.kind,
108
+ keyId(key),
109
+ holderId,
110
+ subjectJson,
111
+ );
112
+ return { status: "acquired", lease: { key, holderId, fencingToken: 1 } } as const;
113
+ }
114
+ if (row.holder_id === holderId) {
115
+ await tx.run(
116
+ `UPDATE ${table} SET subject_json = ?, renewed_at = ${STORE_NOW} WHERE kind = ? AND key = ?`,
117
+ subjectJson,
118
+ key.kind,
119
+ keyId(key),
120
+ );
121
+ return {
122
+ status: "acquired",
123
+ lease: { key, holderId, fencingToken: Number(row.fencing_token) },
124
+ } as const;
125
+ }
126
+ if (row.holder_id !== null && Number(row.age_ms) <= staleAfterMs) {
127
+ return { status: "held", holderId: row.holder_id, fencingToken: Number(row.fencing_token) } as const;
128
+ }
129
+ const fencingToken = Number(row.last_token) + 1;
130
+ await tx.run(
131
+ `UPDATE ${table} SET holder_id = ?, fencing_token = ?, last_token = ?, subject_json = ?,
132
+ renewed_at = ${STORE_NOW} WHERE kind = ? AND key = ?`,
133
+ holderId,
134
+ fencingToken,
135
+ fencingToken,
136
+ subjectJson,
137
+ key.kind,
138
+ keyId(key),
139
+ );
140
+ return { status: "acquired", lease: { key, holderId, fencingToken } } as const;
141
+ });
142
+ },
143
+ async renew(lease, subject) {
144
+ const bad = validLease(lease);
145
+ if (bad !== undefined) return bad;
146
+ if (subject !== undefined && (subject.kind !== lease.key.kind || keyId(keyOf(subject)) !== keyId(lease.key))) {
147
+ return invalid("subject-key-mismatch");
148
+ }
149
+ return database.transaction(async (tx) => {
150
+ const { changes } =
151
+ subject === undefined
152
+ ? await tx.run(`UPDATE ${table} SET renewed_at = ${STORE_NOW} WHERE ${owned}`, ...ownedParams(lease))
153
+ : await tx.run(
154
+ `UPDATE ${table} SET renewed_at = ${STORE_NOW}, subject_json = ? WHERE ${owned}`,
155
+ JSON.stringify(subject),
156
+ ...ownedParams(lease),
157
+ );
158
+ return changes === 1 ? ({ status: "renewed" } as const) : stale(await holderOf(tx, lease.key));
159
+ });
160
+ },
161
+ async release(lease) {
162
+ const bad = validLease(lease);
163
+ if (bad !== undefined) return bad;
164
+ return database.transaction(async (tx) => {
165
+ const { changes } = await tx.run(
166
+ `UPDATE ${table} SET holder_id = NULL, fencing_token = NULL, subject_json = NULL, renewed_at = NULL
167
+ WHERE ${owned}`,
168
+ ...ownedParams(lease),
169
+ );
170
+ return changes === 1 ? ({ status: "released" } as const) : stale(await holderOf(tx, lease.key));
171
+ });
172
+ },
173
+ async read(key) {
174
+ const bad = invalidKey(key);
175
+ if (bad !== undefined) return bad;
176
+ const row = await database.get<StateRow>(
177
+ `SELECT kind, key, holder_id, fencing_token, subject_json, ${AGE} AS age_ms FROM ${table}
178
+ WHERE kind = ? AND key = ? AND holder_id IS NOT NULL`,
179
+ key.kind,
180
+ keyId(key),
181
+ );
182
+ return row === undefined ? { status: "missing" } : { status: "found", lease: stateFromRow(row) };
183
+ },
184
+ async listLive(kind, staleAfterMs) {
185
+ const rows = await database.all<StateRow>(
186
+ `SELECT kind, key, holder_id, fencing_token, subject_json, ${AGE} AS age_ms FROM ${table}
187
+ WHERE kind = ? AND holder_id IS NOT NULL AND ${AGE} <= ? ORDER BY first_acquired_order`,
188
+ kind,
189
+ staleAfterMs,
190
+ );
191
+ return rows.map(stateFromRow);
192
+ },
193
+ };
194
+ }
@@ -0,0 +1,249 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ type SqliteExecutor,
4
+ type SqliteRow,
5
+ type SqliteValue,
6
+ validateStoragePrefix,
7
+ } from "@bastani/atomic/storage";
8
+ import type {
9
+ ClaimedOutboxRow,
10
+ InvalidStateInput,
11
+ IntercomBrokerStateOptions,
12
+ IntercomDeliveryFrame,
13
+ OutboxClaim,
14
+ OutboxClaimWriteRejection,
15
+ OutboxRow,
16
+ OutboxRowState,
17
+ OutboxStore,
18
+ StateStoreInvalidReason,
19
+ } from "./multinode-state-store.js";
20
+
21
+ const STORE_NOW = "CAST(unixepoch('subsec') * 1000 AS INTEGER)";
22
+ const UNDELIVERED = "state IN ('queued', 'claimed')";
23
+
24
+ interface OutboxRecord extends SqliteRow {
25
+ readonly sequence: SqliteValue;
26
+ readonly message_id: string;
27
+ readonly target_identity: string;
28
+ readonly origin_broker_id: string;
29
+ readonly digest: string;
30
+ readonly frame_json: string;
31
+ readonly state: OutboxRowState;
32
+ readonly claim_attempts: SqliteValue;
33
+ readonly claimant_id: string | null;
34
+ readonly claim_token: string | null;
35
+ /** Store-clock milliseconds since the claim was taken or renewed; null when unclaimed. */
36
+ readonly claim_age: SqliteValue;
37
+ readonly failure_reason: string | null;
38
+ }
39
+
40
+ const invalid = (reason: StateStoreInvalidReason): InvalidStateInput => ({ status: "invalid", reason });
41
+
42
+ function toRow(record: OutboxRecord): OutboxRow {
43
+ const failureReason = record.failure_reason;
44
+ return {
45
+ sequence: Number(record.sequence),
46
+ messageId: record.message_id,
47
+ targetIdentity: record.target_identity,
48
+ originBrokerId: record.origin_broker_id,
49
+ digest: record.digest,
50
+ frame: JSON.parse(record.frame_json) as IntercomDeliveryFrame,
51
+ state: record.state,
52
+ claimAttempts: Number(record.claim_attempts),
53
+ ...(failureReason === null ? {} : { failureReason: JSON.parse(failureReason) as string }),
54
+ };
55
+ }
56
+
57
+ const isSettled = (record: OutboxRecord): boolean => record.state === "delivered" || record.state === "failed";
58
+
59
+ /**
60
+ * S2 outbox on the shared storage adapter's database, in `<prefix>intercom_outbox`. Every read-modify-write runs in
61
+ * one `BEGIN IMMEDIATE` transaction, and every age is measured on the database clock.
62
+ */
63
+ export async function openSqlOutboxStore(options: IntercomBrokerStateOptions): Promise<OutboxStore> {
64
+ const { database } = options;
65
+ const table = `${validateStoragePrefix(options.prefix)}intercom_outbox`;
66
+ await database.exec(`
67
+ CREATE TABLE IF NOT EXISTS ${table}(
68
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
69
+ message_id TEXT NOT NULL UNIQUE,
70
+ target_identity TEXT NOT NULL,
71
+ origin_broker_id TEXT NOT NULL,
72
+ digest TEXT NOT NULL,
73
+ frame_json TEXT NOT NULL,
74
+ state TEXT NOT NULL CHECK (state IN ('queued', 'claimed', 'delivered', 'failed')),
75
+ claim_attempts INTEGER NOT NULL DEFAULT 0,
76
+ claimant_id TEXT,
77
+ claim_token TEXT,
78
+ claim_renewed_at INTEGER,
79
+ failure_reason TEXT,
80
+ settled_at INTEGER
81
+ );
82
+ CREATE INDEX IF NOT EXISTS ${table}_target ON ${table}(target_identity, state, sequence);
83
+ `);
84
+
85
+ const columns = `sequence, message_id, target_identity, origin_broker_id, digest, frame_json, state,
86
+ claim_attempts, claimant_id, claim_token, failure_reason,
87
+ CASE WHEN claim_renewed_at IS NULL THEN NULL ELSE MAX(0, ${STORE_NOW} - claim_renewed_at) END AS claim_age`;
88
+ const select = (executor: SqliteExecutor, messageId: string) =>
89
+ executor.get<OutboxRecord>(`SELECT ${columns} FROM ${table} WHERE message_id = ?`, messageId);
90
+ const undelivered = (executor: SqliteExecutor, targetIdentity: string) =>
91
+ executor.all<OutboxRecord>(
92
+ `SELECT ${columns} FROM ${table} WHERE target_identity = ? AND ${UNDELIVERED} ORDER BY sequence`,
93
+ targetIdentity,
94
+ );
95
+ const isLive = (record: OutboxRecord, staleAfterMs: number): boolean =>
96
+ record.claim_age !== null && Number(record.claim_age) <= staleAfterMs;
97
+
98
+ /** Runs `write` only for the row's current claim; every other case is reported as the contract requires. */
99
+ const withClaim = <Result>(
100
+ claim: OutboxClaim,
101
+ write: (executor: SqliteExecutor) => Promise<Result>,
102
+ ): Promise<Result | OutboxClaimWriteRejection> => {
103
+ if (claim.messageId.length === 0) return Promise.resolve(invalid("empty-message-id"));
104
+ if (claim.claimantId.length === 0) return Promise.resolve(invalid("empty-claimant-id"));
105
+ return database.transaction(async (executor) => {
106
+ const record = await select(executor, claim.messageId);
107
+ if (record === undefined) return { status: "unknown" } as const;
108
+ if (isSettled(record)) return { status: "settled", row: toRow(record) } as const;
109
+ if (record.claim_token !== claim.token || record.claimant_id !== claim.claimantId) {
110
+ return { status: "stale" } as const;
111
+ }
112
+ return write(executor);
113
+ });
114
+ };
115
+
116
+ const settle = async (
117
+ executor: SqliteExecutor,
118
+ messageId: string,
119
+ state: "delivered" | "failed",
120
+ reason: string | null,
121
+ ): Promise<OutboxRow> => {
122
+ await executor.run(
123
+ `UPDATE ${table} SET state = ?, failure_reason = ?, settled_at = ${STORE_NOW},
124
+ claimant_id = NULL, claim_token = NULL, claim_renewed_at = NULL
125
+ WHERE message_id = ?`,
126
+ state,
127
+ reason === null ? null : JSON.stringify(reason),
128
+ messageId,
129
+ );
130
+ return toRow((await select(executor, messageId))!);
131
+ };
132
+
133
+ return {
134
+ async enqueue(request) {
135
+ if (request.targetIdentity.length === 0) return invalid("empty-target-identity");
136
+ if (request.originBrokerId.length === 0) return invalid("empty-broker-id");
137
+ const messageId = request.frame.message.id;
138
+ if (messageId.length === 0) return invalid("empty-message-id");
139
+ const frameJson = JSON.stringify(request.frame);
140
+ return database.transaction(async (executor) => {
141
+ const existing = await select(executor, messageId);
142
+ if (existing !== undefined) {
143
+ return existing.digest === request.digest
144
+ ? ({ status: "duplicate", row: toRow(existing) } as const)
145
+ : ({ status: "conflict", messageId } as const);
146
+ }
147
+ await executor.run(
148
+ `INSERT INTO ${table}(message_id, target_identity, origin_broker_id, digest, frame_json, state)
149
+ VALUES (?, ?, ?, ?, ?, 'queued')`,
150
+ messageId,
151
+ request.targetIdentity,
152
+ request.originBrokerId,
153
+ request.digest,
154
+ frameJson,
155
+ );
156
+ return { status: "inserted", row: toRow((await select(executor, messageId))!) } as const;
157
+ });
158
+ },
159
+
160
+ async claim(request) {
161
+ if (request.claimantId.length === 0) return invalid("empty-claimant-id");
162
+ if (request.targetIdentity.length === 0) return invalid("empty-target-identity");
163
+ return database.transaction(async (executor) => {
164
+ const records = await undelivered(executor, request.targetIdentity);
165
+ const other = records.find(
166
+ (record) =>
167
+ record.state === "claimed" &&
168
+ record.claimant_id !== request.claimantId &&
169
+ isLive(record, request.staleAfterMs),
170
+ );
171
+ if (other?.claimant_id) return { status: "held", claimantId: other.claimant_id } as const;
172
+ const rows: ClaimedOutboxRow[] = [];
173
+ for (const record of records) {
174
+ if (record.state === "claimed" && isLive(record, request.staleAfterMs)) continue;
175
+ const token = randomUUID();
176
+ await executor.run(
177
+ `UPDATE ${table} SET state = 'claimed', claim_attempts = claim_attempts + 1, claimant_id = ?,
178
+ claim_token = ?, claim_renewed_at = ${STORE_NOW}
179
+ WHERE message_id = ?`,
180
+ request.claimantId,
181
+ token,
182
+ record.message_id,
183
+ );
184
+ const row = toRow((await select(executor, record.message_id))!);
185
+ rows.push({
186
+ row,
187
+ claim: { messageId: row.messageId, claimantId: request.claimantId, token, attempt: row.claimAttempts },
188
+ });
189
+ }
190
+ return { status: "claimed", rows } as const;
191
+ });
192
+ },
193
+
194
+ renewClaim(claim) {
195
+ return withClaim(claim, async (executor) => {
196
+ await executor.run(
197
+ `UPDATE ${table} SET claim_renewed_at = ${STORE_NOW} WHERE message_id = ?`,
198
+ claim.messageId,
199
+ );
200
+ return { status: "renewed" } as const;
201
+ });
202
+ },
203
+
204
+ releaseClaim(claim) {
205
+ return withClaim(claim, async (executor) => {
206
+ await executor.run(
207
+ `UPDATE ${table} SET state = 'queued', claimant_id = NULL, claim_token = NULL, claim_renewed_at = NULL
208
+ WHERE message_id = ?`,
209
+ claim.messageId,
210
+ );
211
+ return { status: "released" } as const;
212
+ });
213
+ },
214
+
215
+ markDelivered(claim) {
216
+ return withClaim(claim, async (executor) => ({
217
+ status: "delivered" as const,
218
+ row: await settle(executor, claim.messageId, "delivered", null),
219
+ }));
220
+ },
221
+
222
+ markFailed(claim, reason) {
223
+ return withClaim(claim, async (executor) => ({
224
+ status: "failed" as const,
225
+ row: await settle(executor, claim.messageId, "failed", reason),
226
+ }));
227
+ },
228
+
229
+ async read(messageId) {
230
+ if (messageId.length === 0) return invalid("empty-message-id");
231
+ const record = await select(database, messageId);
232
+ return record === undefined ? { status: "missing" } : { status: "found", row: toRow(record) };
233
+ },
234
+
235
+ async listUndelivered(targetIdentity) {
236
+ if (targetIdentity.length === 0) return invalid("empty-target-identity");
237
+ return { status: "listed", rows: (await undelivered(database, targetIdentity)).map(toRow) };
238
+ },
239
+
240
+ async prune(settledOlderThanMs) {
241
+ const result = await database.run(
242
+ `DELETE FROM ${table}
243
+ WHERE state IN ('delivered', 'failed') AND MAX(0, ${STORE_NOW} - settled_at) > ?`,
244
+ settledOlderThanMs,
245
+ );
246
+ return result.changes;
247
+ },
248
+ };
249
+ }
@@ -0,0 +1,105 @@
1
+ import { type SqliteRow, validateStoragePrefix } from "@bastani/atomic/storage";
2
+ import type {
3
+ IntercomBrokerStateOptions,
4
+ InvalidStateInput,
5
+ PendingAsk,
6
+ PendingAskClearResult,
7
+ PendingAskMatchResult,
8
+ PendingAskPruneResult,
9
+ PendingAskRecordResult,
10
+ PendingAskStore,
11
+ PendingAskTakeResult,
12
+ ReplyRoute,
13
+ StateStoreInvalidReason,
14
+ } from "./multinode-state-store.js";
15
+
16
+ type AskRow = SqliteRow & { sender_session_id: string; target_session_id: string; message_id: string };
17
+
18
+ const invalid = (reason: StateStoreInvalidReason): InvalidStateInput => ({ status: "invalid", reason });
19
+
20
+ function invalidAsk(ask: PendingAsk): InvalidStateInput | undefined {
21
+ if (ask.senderSessionId.length === 0 || ask.targetSessionId.length === 0) return invalid("empty-session-id");
22
+ return ask.messageId.length === 0 ? invalid("empty-message-id") : undefined;
23
+ }
24
+
25
+ function invalidRoute(route: ReplyRoute): InvalidStateInput | undefined {
26
+ if (route.replySenderSessionId.length === 0 || route.replyTargetSessionId.length === 0) {
27
+ return invalid("empty-session-id");
28
+ }
29
+ return route.replyTo.length === 0 ? invalid("empty-message-id") : undefined;
30
+ }
31
+
32
+ /** S3: `PendingAskStore` on the shared storage adapter's database, table `<prefix>intercom_pending_asks`. */
33
+ export async function openSqlPendingAskStore({
34
+ database,
35
+ prefix,
36
+ }: IntercomBrokerStateOptions): Promise<PendingAskStore> {
37
+ const table = `${validateStoragePrefix(prefix)}intercom_pending_asks`;
38
+ await database.transaction((tx) =>
39
+ tx.exec(`
40
+ CREATE TABLE IF NOT EXISTS ${table}(
41
+ "order" INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ sender_session_id TEXT NOT NULL,
43
+ target_session_id TEXT NOT NULL,
44
+ message_id TEXT NOT NULL,
45
+ UNIQUE(sender_session_id, target_session_id, message_id)
46
+ );
47
+ CREATE INDEX IF NOT EXISTS ${table}_target ON ${table}(target_session_id, "order");
48
+ `),
49
+ );
50
+ const reverseRoute = "sender_session_id = ? AND target_session_id = ? AND message_id = ?";
51
+ const reverseParams = (route: ReplyRoute) =>
52
+ [route.replyTargetSessionId, route.replySenderSessionId, route.replyTo] as const;
53
+
54
+ return {
55
+ async record(ask): Promise<PendingAskRecordResult> {
56
+ const bad = invalidAsk(ask);
57
+ if (bad !== undefined) return bad;
58
+ const { changes } = await database.run(
59
+ `INSERT INTO ${table}(sender_session_id, target_session_id, message_id) VALUES (?, ?, ?)
60
+ ON CONFLICT DO NOTHING`,
61
+ ask.senderSessionId,
62
+ ask.targetSessionId,
63
+ ask.messageId,
64
+ );
65
+ return changes === 0 ? { status: "duplicate" } : { status: "recorded" };
66
+ },
67
+ async matchesReply(route): Promise<PendingAskMatchResult> {
68
+ const bad = invalidRoute(route);
69
+ if (bad !== undefined) return bad;
70
+ const row = await database.get(`SELECT 1 AS found FROM ${table} WHERE ${reverseRoute}`, ...reverseParams(route));
71
+ return row === undefined ? { status: "no-match" } : { status: "match" };
72
+ },
73
+ async clearReply(route): Promise<PendingAskClearResult> {
74
+ const bad = invalidRoute(route);
75
+ if (bad !== undefined) return bad;
76
+ const { changes } = await database.run(`DELETE FROM ${table} WHERE ${reverseRoute}`, ...reverseParams(route));
77
+ return changes === 0 ? { status: "no-match" } : { status: "cleared" };
78
+ },
79
+ async pruneSender(senderSessionId): Promise<PendingAskPruneResult> {
80
+ if (senderSessionId.length === 0) return invalid("empty-session-id");
81
+ const { changes } = await database.run(`DELETE FROM ${table} WHERE sender_session_id = ?`, senderSessionId);
82
+ return { status: "pruned", removed: changes };
83
+ },
84
+ async takeForTarget(targetSessionId): Promise<PendingAskTakeResult> {
85
+ if (targetSessionId.length === 0) return invalid("empty-session-id");
86
+ const rows = await database.transaction(async (tx) => {
87
+ const taken = await tx.all<AskRow>(
88
+ `SELECT sender_session_id, target_session_id, message_id FROM ${table}
89
+ WHERE target_session_id = ? ORDER BY "order"`,
90
+ targetSessionId,
91
+ );
92
+ await tx.run(`DELETE FROM ${table} WHERE target_session_id = ?`, targetSessionId);
93
+ return taken;
94
+ });
95
+ return {
96
+ status: "taken",
97
+ asks: rows.map((row) => ({
98
+ senderSessionId: row.sender_session_id,
99
+ targetSessionId: row.target_session_id,
100
+ messageId: row.message_id,
101
+ })),
102
+ };
103
+ },
104
+ };
105
+ }