@astrosheep/square 0.3.106 → 0.4.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 (206) hide show
  1. package/dist/activity-feed.js +1 -1
  2. package/dist/artifact-store.d.ts +41 -0
  3. package/dist/artifact-store.js +272 -0
  4. package/dist/artifact.d.ts +6 -12
  5. package/dist/artifact.js +43 -344
  6. package/dist/attention-presentation.d.ts +13 -7
  7. package/dist/attention-presentation.js +29 -68
  8. package/dist/boundary-presentation.d.ts +3 -16
  9. package/dist/boundary-presentation.js +29 -109
  10. package/dist/card-presentation.d.ts +1 -0
  11. package/dist/card-presentation.js +2 -0
  12. package/dist/catch-decisions.d.ts +2 -2
  13. package/dist/catch-decisions.js +7 -3
  14. package/dist/catch-selection.d.ts +8 -0
  15. package/dist/catch-selection.js +5 -0
  16. package/dist/claude-delivery.d.ts +14 -12
  17. package/dist/claude-delivery.js +42 -142
  18. package/dist/claude-mcp-context.js +2 -2
  19. package/dist/claude-mod.js +28 -140
  20. package/dist/claude-transport.d.ts +5 -0
  21. package/dist/claude-transport.js +34 -0
  22. package/dist/cli/context.d.ts +2 -2
  23. package/dist/cli/context.js +7 -10
  24. package/dist/cli/maintenance-commands.d.ts +0 -4
  25. package/dist/cli/maintenance-commands.js +2 -29
  26. package/dist/cli/observation-commands.d.ts +2 -0
  27. package/dist/cli/observation-commands.js +12 -2
  28. package/dist/cli/program.js +1 -1
  29. package/dist/cli/registry.js +4 -3
  30. package/dist/codex-address.d.ts +2 -2
  31. package/dist/codex-boundary-state.d.ts +1 -5
  32. package/dist/codex-boundary-state.js +0 -4
  33. package/dist/codex-hook.d.ts +7 -10
  34. package/dist/codex-hook.js +69 -33
  35. package/dist/codex-queue.d.ts +10 -11
  36. package/dist/codex-queue.js +32 -34
  37. package/dist/codex-watch.d.ts +2 -0
  38. package/dist/codex-watch.js +100 -0
  39. package/dist/decisions.js +3 -3
  40. package/dist/delivery-deferral.d.ts +1 -19
  41. package/dist/delivery-deferral.js +1 -29
  42. package/dist/delivery-frame.d.ts +7 -0
  43. package/dist/delivery-frame.js +11 -0
  44. package/dist/delivery-ledger-sqlite.d.ts +97 -0
  45. package/dist/delivery-ledger-sqlite.js +496 -0
  46. package/dist/delivery-ledger.d.ts +196 -0
  47. package/dist/delivery-ledger.js +268 -0
  48. package/dist/delivery-lifetime.d.ts +6 -0
  49. package/dist/delivery-lifetime.js +45 -0
  50. package/dist/harness-links.js +61 -7
  51. package/dist/harness.js +12 -17
  52. package/dist/help.js +3 -6
  53. package/dist/http-delivery.d.ts +5 -9
  54. package/dist/http-delivery.js +20 -79
  55. package/dist/inbox.d.ts +6 -57
  56. package/dist/inbox.js +24 -380
  57. package/dist/index.d.ts +7 -4
  58. package/dist/index.js +5 -2
  59. package/dist/mcp.d.ts +2 -1
  60. package/dist/mcp.js +40 -70
  61. package/dist/message.d.ts +7 -0
  62. package/dist/message.js +23 -0
  63. package/dist/model.d.ts +15 -91
  64. package/dist/model.js +2 -140
  65. package/dist/neutral.d.ts +72 -0
  66. package/dist/neutral.js +27 -0
  67. package/dist/notifications.d.ts +24 -26
  68. package/dist/notifications.js +39 -143
  69. package/dist/operation-control.d.ts +4 -0
  70. package/dist/operation-control.js +20 -0
  71. package/dist/operation-presentation.d.ts +1 -2
  72. package/dist/operation-presentation.js +1 -7
  73. package/dist/owner.d.ts +2 -2
  74. package/dist/owner.js +1 -1
  75. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  76. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  77. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  78. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  79. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  80. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  81. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  82. package/dist/packages/agent-delivery/src/codex.js +41 -4
  83. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  84. package/dist/packages/agent-delivery/src/index.js +1 -1
  85. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  86. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  87. package/dist/paseo-delivery.d.ts +5 -5
  88. package/dist/paseo-delivery.js +19 -99
  89. package/dist/paseo.d.ts +1 -1
  90. package/dist/paseo.js +1 -1
  91. package/dist/plugins/opencode/index.d.ts +5 -3
  92. package/dist/plugins/opencode/index.js +99 -21
  93. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  94. package/dist/plugins/opencode-shared/delivery.js +109 -0
  95. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  96. package/dist/plugins/opencode-v1/index.js +41 -0
  97. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  98. package/dist/plugins/opencode-v1/runtime.js +152 -0
  99. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  100. package/dist/plugins/opencode-v1/transport.js +50 -0
  101. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  102. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  103. package/dist/ports.d.ts +33 -214
  104. package/dist/presentation.d.ts +5 -5
  105. package/dist/presentation.js +1 -0
  106. package/dist/runtime.js +2 -2
  107. package/dist/search.js +1 -1
  108. package/dist/session-lifecycle.d.ts +8 -7
  109. package/dist/session-lifecycle.js +40 -42
  110. package/dist/sqlite-framing.d.ts +8 -0
  111. package/dist/sqlite-framing.js +31 -0
  112. package/dist/sqlite-square.d.ts +16 -26
  113. package/dist/sqlite-square.js +199 -262
  114. package/dist/sqlite-values.d.ts +21 -0
  115. package/dist/sqlite-values.js +78 -0
  116. package/dist/square-api.d.ts +1 -1
  117. package/dist/square-api.js +5 -8
  118. package/dist/square-core.d.ts +0 -1
  119. package/dist/square-domain.d.ts +12 -10
  120. package/dist/square-domain.js +105 -223
  121. package/dist/square-facade.d.ts +25 -76
  122. package/dist/square-location.d.ts +6 -1
  123. package/dist/square-location.js +1 -1
  124. package/dist/square-post.d.ts +53 -0
  125. package/dist/square-post.js +4 -0
  126. package/dist/square-posting.d.ts +13 -0
  127. package/dist/square-posting.js +174 -0
  128. package/dist/square-producer-sqlite.d.ts +11 -0
  129. package/dist/square-producer-sqlite.js +210 -0
  130. package/dist/square-receive.d.ts +2 -3
  131. package/dist/square-sqlite.d.ts +33 -42
  132. package/dist/square-sqlite.js +236 -768
  133. package/dist/square-tools.d.ts +8 -0
  134. package/dist/square-tools.js +39 -0
  135. package/dist/square-values.d.ts +27 -0
  136. package/dist/square-values.js +90 -0
  137. package/dist/views.d.ts +6 -6
  138. package/dist/views.js +14 -9
  139. package/dist/wake-sink.d.ts +4 -11
  140. package/dist/wake-sink.js +8 -32
  141. package/dist/wake-transports.d.ts +4 -0
  142. package/dist/wake-transports.js +11 -0
  143. package/examples/symmetric-domains.mjs +29 -249
  144. package/guides/change-waits.md +13 -53
  145. package/guides/claude-native.md +19 -172
  146. package/guides/codex-delivery.md +55 -78
  147. package/guides/delivery-primitives.md +48 -207
  148. package/guides/operation-cancellation.md +2 -5
  149. package/guides/participant-ownership.md +25 -89
  150. package/guides/pi-native-delivery.md +19 -56
  151. package/guides/pi-tools.md +6 -8
  152. package/guides/row-runtime-delivery-evidence.md +2 -0
  153. package/guides/row-storage-design.md +19 -44
  154. package/guides/square-api.md +113 -538
  155. package/package.json +8 -8
  156. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  157. package/plugins/claude/hooks/hooks.json +1 -1
  158. package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
  159. package/plugins/claude/types.d.ts +1 -1
  160. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  161. package/plugins/codex/hooks/hooks.json +11 -23
  162. package/plugins/opencode/index.ts +91 -23
  163. package/plugins/opencode-shared/delivery.ts +91 -0
  164. package/plugins/opencode-v1/index.ts +36 -0
  165. package/plugins/opencode-v1/runtime.ts +134 -0
  166. package/plugins/opencode-v1/transport.ts +41 -0
  167. package/plugins/paseo/OVERVIEW.md +5 -5
  168. package/plugins/paseo/README.md +7 -6
  169. package/plugins/paseo/client/markdown.tsx +5 -5
  170. package/plugins/paseo/client/square-message.tsx +28 -24
  171. package/plugins/paseo/client/transform.ts +8 -5
  172. package/plugins/paseo/shared/card-presentation.ts +16 -0
  173. package/plugins/paseo/shared/message.ts +6 -11
  174. package/plugins/pi/square-pi.js +167 -451
  175. package/dist/channel.d.ts +0 -74
  176. package/dist/channel.js +0 -92
  177. package/dist/claude-delivery-status.d.ts +0 -13
  178. package/dist/claude-delivery-status.js +0 -46
  179. package/dist/cmd/notify-once.d.ts +0 -2
  180. package/dist/cmd/notify-once.js +0 -29
  181. package/dist/delivery-attempts.d.ts +0 -52
  182. package/dist/delivery-attempts.js +0 -73
  183. package/dist/delivery-effects.d.ts +0 -139
  184. package/dist/delivery-effects.js +0 -163
  185. package/dist/delivery-health.d.ts +0 -51
  186. package/dist/delivery-health.js +0 -173
  187. package/dist/delivery-operations.d.ts +0 -9
  188. package/dist/delivery-operations.js +0 -168
  189. package/dist/delivery-primitives.d.ts +0 -5
  190. package/dist/delivery-primitives.js +0 -3
  191. package/dist/delivery.d.ts +0 -45
  192. package/dist/delivery.js +0 -106
  193. package/dist/plugins/opencode/delivery.d.ts +0 -19
  194. package/dist/plugins/opencode/delivery.js +0 -238
  195. package/dist/presentation-operations.d.ts +0 -18
  196. package/dist/presentation-operations.js +0 -86
  197. package/dist/square-index.d.ts +0 -16
  198. package/dist/square-index.js +0 -106
  199. package/dist/square-legacy-framing.d.ts +0 -11
  200. package/dist/square-legacy-framing.js +0 -64
  201. package/dist/square-migration-host-ledger.d.ts +0 -26
  202. package/dist/square-migration-host-ledger.js +0 -169
  203. package/dist/square-migration.d.ts +0 -30
  204. package/dist/square-migration.js +0 -599
  205. package/guides/migration.md +0 -156
  206. package/plugins/opencode/delivery.ts +0 -211
@@ -0,0 +1,496 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { inSqliteTransaction } from './sqlite-transaction.js';
6
+ import { validateSchema } from './sqlite-framing.js';
7
+ const FORMAT = 3;
8
+ const APPLICATION_ID = 0x444c4752;
9
+ const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
10
+ const SCHEMA = `CREATE TABLE meta (format INTEGER NOT NULL, next_seq INTEGER NOT NULL);
11
+ CREATE TABLE sessions (owner TEXT PRIMARY KEY, generation TEXT NOT NULL, state TEXT NOT NULL, routes_json TEXT NOT NULL, at REAL NOT NULL);
12
+ CREATE TABLE cards (seq INTEGER PRIMARY KEY, owner TEXT NOT NULL, key TEXT NOT NULL, text TEXT NOT NULL, details TEXT, posted_at REAL NOT NULL, UNIQUE(owner,key));
13
+ CREATE INDEX cards_owner ON cards(owner,seq);
14
+ CREATE TABLE suppressions (id TEXT PRIMARY KEY, owner TEXT NOT NULL, key TEXT NOT NULL, scope TEXT NOT NULL CHECK(scope IN ('exact','prefix')), created_at REAL NOT NULL, expires_at REAL);
15
+ CREATE INDEX suppressions_owner ON suppressions(owner);
16
+ CREATE UNIQUE INDEX suppressions_permanent_exact ON suppressions(owner,key) WHERE scope='exact' AND expires_at IS NULL;
17
+ CREATE TABLE attempts (token TEXT PRIMARY KEY, seq INTEGER NOT NULL, generation TEXT NOT NULL, track TEXT NOT NULL, route_kind TEXT, outcome TEXT NOT NULL, correlation_json TEXT NOT NULL, started_at REAL NOT NULL, expires_at REAL NOT NULL, settled_at REAL);
18
+ CREATE INDEX attempts_seq ON attempts(seq);
19
+ CREATE TABLE receipts (owner TEXT NOT NULL, key TEXT NOT NULL, at REAL NOT NULL, PRIMARY KEY(owner,key));
20
+ CREATE INDEX receipts_time ON receipts(at);
21
+ CREATE TABLE cancellations (owner TEXT PRIMARY KEY, generation TEXT NOT NULL, through_seq INTEGER NOT NULL);`;
22
+ function validate(db, location) {
23
+ const header = db.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
24
+ if (header?.application_id !== APPLICATION_ID || header.user_version !== FORMAT)
25
+ throw new Error('Unsupported delivery ledger format');
26
+ validateSchema(db, SCHEMA, location);
27
+ const meta = db.prepare('SELECT * FROM meta').all();
28
+ if (meta.length !== 1 || meta[0].format !== FORMAT || !Number.isSafeInteger(meta[0].next_seq) || Number(meta[0].next_seq) < 1)
29
+ throw new Error('Invalid delivery ledger header');
30
+ }
31
+ function session(row) {
32
+ return row ? { owner: row.owner, generation: row.generation, state: row.state,
33
+ routes: JSON.parse(row.routes_json), at: row.at } : undefined;
34
+ }
35
+ function card(row) {
36
+ return { seq: row.seq, owner: row.owner, key: row.key, text: row.text,
37
+ ...(row.details === null ? {} : { details: JSON.parse(row.details) }), postedAt: row.posted_at };
38
+ }
39
+ function attempt(row) {
40
+ return { token: row.token, seq: row.seq, generation: row.generation,
41
+ track: row.track, routeKind: row.route_kind, outcome: row.outcome,
42
+ correlation: JSON.parse(row.correlation_json), startedAt: row.started_at, expiresAt: row.expires_at,
43
+ ...(row.settled_at === null ? {} : { settledAt: row.settled_at }) };
44
+ }
45
+ function holds(row) { return !['failed', 'released'].includes(row.outcome); }
46
+ /** All ledger file access and SQL are confined here. Transactions never span external effects. */
47
+ export class LedgerStore {
48
+ location;
49
+ db;
50
+ clock;
51
+ pending = Promise.resolve();
52
+ closed = false;
53
+ constructor(location, db, clock) {
54
+ this.location = location;
55
+ this.db = db;
56
+ this.clock = clock;
57
+ }
58
+ static async open(location, clock) {
59
+ try {
60
+ await fs.access(location);
61
+ }
62
+ catch (error) {
63
+ if (error.code !== 'ENOENT')
64
+ throw error;
65
+ await fs.mkdir(path.dirname(location), { recursive: true, mode: 0o700 });
66
+ const temporary = location + '.' + randomUUID() + '.tmp';
67
+ let initial;
68
+ try {
69
+ await (await fs.open(temporary, 'wx', 0o600)).close();
70
+ initial = new DatabaseSync(temporary);
71
+ await inSqliteTransaction(initial, true, () => undefined, () => {
72
+ initial.exec(SCHEMA);
73
+ initial.prepare('INSERT INTO meta VALUES(?,1)').run(FORMAT);
74
+ initial.exec(`PRAGMA application_id=${APPLICATION_ID}; PRAGMA user_version=${FORMAT}`);
75
+ });
76
+ initial.close();
77
+ initial = undefined;
78
+ try {
79
+ await fs.link(temporary, location);
80
+ }
81
+ catch (error) {
82
+ if (error.code !== 'EEXIST')
83
+ throw error;
84
+ }
85
+ }
86
+ finally {
87
+ initial?.close();
88
+ await fs.unlink(temporary).catch(() => undefined);
89
+ }
90
+ }
91
+ // Existing formats are inspected on a read-only connection before any writable open.
92
+ const probe = new DatabaseSync(location, { readOnly: true });
93
+ try {
94
+ validate(probe, location);
95
+ }
96
+ finally {
97
+ probe.close();
98
+ }
99
+ const db = new DatabaseSync(location);
100
+ try {
101
+ validate(db, location);
102
+ return new LedgerStore(location, db, clock);
103
+ }
104
+ catch (error) {
105
+ db.close();
106
+ throw error;
107
+ }
108
+ }
109
+ close() {
110
+ this.closed = true;
111
+ return this.pending.then(() => this.db.close());
112
+ }
113
+ transaction(write, change, signal) {
114
+ if (this.closed)
115
+ return Promise.reject(new Error('Delivery ledger is closed'));
116
+ const result = this.pending.then(() => inSqliteTransaction(this.db, write, () => validate(this.db, this.location), () => {
117
+ const now = this.clock();
118
+ if (!Number.isFinite(now))
119
+ throw new TypeError('Expected finite ledger time');
120
+ const changes = Number(this.db.prepare('SELECT total_changes() AS n').get().n);
121
+ const result = change(now);
122
+ if (write && Number(this.db.prepare('SELECT total_changes() AS n').get().n) > changes) {
123
+ this.normalize(now);
124
+ this.gc(now);
125
+ }
126
+ return result;
127
+ }, signal));
128
+ this.pending = result.catch(() => undefined);
129
+ return result;
130
+ }
131
+ getSession(owner) {
132
+ return session(this.db.prepare('SELECT * FROM sessions WHERE owner=?').get(owner));
133
+ }
134
+ attached(owner, generation) {
135
+ const s = this.getSession(owner);
136
+ return s?.state === 'attached' && s.generation === generation ? s : undefined;
137
+ }
138
+ listAttempts(seq) {
139
+ return this.db.prepare('SELECT * FROM attempts WHERE seq=? ORDER BY started_at,token').all(seq).map(attempt);
140
+ }
141
+ awaitingEffect(seq) {
142
+ const attempts = this.listAttempts(seq);
143
+ if (attempts.some(a => ['dispatching', 'unknown'].includes(a.outcome)))
144
+ return true;
145
+ return attempts.some(a => a.track === 'handoff' && a.outcome === 'accepted')
146
+ && !attempts.some(a => a.track === 'presentation' && a.outcome === 'presented');
147
+ }
148
+ getAttempt(token) {
149
+ const row = this.db.prepare('SELECT * FROM attempts WHERE token=?').get(token);
150
+ return row ? attempt(row) : undefined;
151
+ }
152
+ normalize(now) {
153
+ this.db.prepare(`UPDATE attempts SET outcome='unknown', settled_at=? WHERE outcome='dispatching' AND
154
+ (expires_at<=? OR NOT EXISTS(SELECT 1 FROM cards c JOIN sessions s ON s.owner=c.owner
155
+ WHERE c.seq=attempts.seq AND s.generation=attempts.generation AND s.state='attached'))`).run(now, now);
156
+ this.db.prepare(`UPDATE cards SET text='',details=NULL WHERE (text<>'' OR details IS NOT NULL)
157
+ AND EXISTS(SELECT 1 FROM attempts a WHERE a.seq=cards.seq AND a.outcome IN ('accepted','unknown','presented'))`).run();
158
+ const terminal = this.db.prepare(`SELECT seq FROM cards WHERE
159
+ EXISTS(SELECT 1 FROM attempts a WHERE a.seq=cards.seq AND a.track='presentation' AND a.outcome='presented')
160
+ AND NOT EXISTS(SELECT 1 FROM attempts a WHERE a.seq=cards.seq AND a.outcome='dispatching')`).all();
161
+ for (const row of terminal)
162
+ this.deleteCard(row.seq, now);
163
+ }
164
+ deleteCard(seq, tombstoneAt) {
165
+ if (tombstoneAt !== undefined)
166
+ this.db.prepare(`INSERT INTO suppressions
167
+ SELECT ?,owner,key,'exact',?,NULL FROM cards WHERE seq=?
168
+ ON CONFLICT(owner,key) WHERE scope='exact' AND expires_at IS NULL DO UPDATE SET created_at=excluded.created_at`)
169
+ .run(randomUUID(), tombstoneAt, seq);
170
+ this.db.prepare('DELETE FROM attempts WHERE seq=?').run(seq);
171
+ this.db.prepare('DELETE FROM cards WHERE seq=?').run(seq);
172
+ }
173
+ gc(now) {
174
+ const cutoff = now - RETENTION_MS;
175
+ this.db.prepare('DELETE FROM suppressions WHERE expires_at IS NOT NULL AND expires_at<=?').run(now);
176
+ const suppressed = this.db.prepare(`SELECT seq FROM cards WHERE EXISTS(SELECT 1 FROM suppressions s WHERE s.owner=cards.owner AND s.scope='exact' AND s.key=cards.key AND s.expires_at IS NULL)
177
+ AND NOT EXISTS(SELECT 1 FROM attempts a WHERE a.seq=cards.seq AND a.outcome IN ('dispatching','unknown'))`).all();
178
+ for (const row of suppressed) {
179
+ if (this.awaitingEffect(row.seq))
180
+ continue;
181
+ this.deleteCard(row.seq);
182
+ }
183
+ this.db.prepare(`DELETE FROM suppressions WHERE id IN (SELECT id FROM suppressions
184
+ WHERE expires_at IS NULL AND created_at<? ORDER BY created_at LIMIT 100)`).run(cutoff);
185
+ const expiredSessions = this.db.prepare(`SELECT c.seq FROM cards c JOIN sessions s ON s.owner=c.owner
186
+ WHERE s.state='deleted' OR (s.state='detached' AND s.at<?) ORDER BY c.seq`).all(cutoff);
187
+ for (const row of expiredSessions)
188
+ this.deleteCard(row.seq);
189
+ this.db.prepare("DELETE FROM suppressions WHERE owner IN (SELECT owner FROM sessions WHERE state='deleted')").run();
190
+ const old = this.db.prepare(`SELECT c.seq FROM cards c WHERE
191
+ EXISTS(SELECT 1 FROM attempts a WHERE a.seq=c.seq AND a.outcome IN ('accepted','unknown'))
192
+ AND (SELECT MAX(a.settled_at) FROM attempts a WHERE a.seq=c.seq)<?
193
+ AND NOT EXISTS(SELECT 1 FROM attempts a WHERE a.seq=c.seq AND a.outcome='dispatching') ORDER BY c.seq LIMIT 100`).all(cutoff);
194
+ for (const row of old)
195
+ this.deleteCard(row.seq, now);
196
+ this.db.prepare('DELETE FROM receipts WHERE rowid IN (SELECT rowid FROM receipts WHERE at<? ORDER BY at LIMIT 100)').run(cutoff);
197
+ }
198
+ session(owner) { return this.transaction(false, () => this.getSession(owner)); }
199
+ sessions() {
200
+ return this.transaction(false, () => this.db.prepare('SELECT * FROM sessions ORDER BY owner').all().map(row => session(row)));
201
+ }
202
+ pendingCards(owner, generation) {
203
+ return this.transaction(false, now => this.attached(owner, generation)
204
+ ? this.db.prepare('SELECT * FROM cards WHERE owner=? ORDER BY seq').all(owner).filter(row => this.eligible(row, now)).map(card) : []);
205
+ }
206
+ handoffs(owner, generation) {
207
+ return this.transaction(false, () => this.db.prepare(`SELECT a.* FROM attempts a JOIN cards c ON c.seq=a.seq
208
+ WHERE c.owner=? AND a.generation=? AND a.track='handoff' ORDER BY a.rowid`).all(owner, generation).map(attempt));
209
+ }
210
+ attach(input) {
211
+ const encoded = JSON.stringify(input.routes);
212
+ return this.transaction(true, now => {
213
+ const previous = this.getSession(input.owner);
214
+ if (previous?.state === 'deleted' || (previous ? previous.generation !== input.expectedGeneration : input.expectedGeneration !== null))
215
+ return false;
216
+ const generation = randomUUID();
217
+ this.db.prepare(`INSERT INTO sessions VALUES(?,?,'attached',?,?) ON CONFLICT(owner)
218
+ DO UPDATE SET generation=excluded.generation,state=excluded.state,routes_json=excluded.routes_json,at=excluded.at`).run(input.owner, generation, encoded, now);
219
+ return this.getSession(input.owner);
220
+ });
221
+ }
222
+ updateRoutes(input) {
223
+ const encoded = JSON.stringify(input.routes);
224
+ return this.transaction(true, now => {
225
+ if (!this.attached(input.owner, input.generation))
226
+ return false;
227
+ this.db.prepare('UPDATE sessions SET routes_json=?,at=? WHERE owner=?').run(encoded, now, input.owner);
228
+ return true;
229
+ });
230
+ }
231
+ lifecycle(input, state) {
232
+ return this.transaction(true, now => {
233
+ const s = this.getSession(input.owner);
234
+ if (!s || s.generation !== input.generation || s.state === 'deleted')
235
+ return false;
236
+ this.db.prepare('UPDATE sessions SET state=?,at=? WHERE owner=?').run(state, now, input.owner);
237
+ return true;
238
+ });
239
+ }
240
+ post(input) {
241
+ return this.transaction(true, now => {
242
+ const s = this.getSession(input.owner);
243
+ if (!s)
244
+ return { status: 'unknown-session' };
245
+ if (s.state === 'deleted')
246
+ return { status: 'deleted-session' };
247
+ const previous = this.db.prepare('SELECT * FROM cards WHERE owner=? AND key=?').get(input.owner, input.key);
248
+ if (previous) {
249
+ if (this.listAttempts(previous.seq).some(a => ['accepted', 'unknown', 'presented'].includes(a.outcome)))
250
+ return { status: 'duplicate' };
251
+ return { status: previous.text !== input.text || previous.details !== input.details ? 'conflict' : this.isSuppressed(input.owner, input.key, now) ? 'suppressed' : 'duplicate' };
252
+ }
253
+ if (this.db.prepare("SELECT 1 FROM suppressions WHERE owner=? AND key=? AND scope='exact' AND expires_at IS NULL").get(input.owner, input.key))
254
+ return { status: 'suppressed' };
255
+ const suppressed = this.isSuppressed(input.owner, input.key, now);
256
+ const seq = Number(this.db.prepare('SELECT next_seq FROM meta').get().next_seq);
257
+ if (!Number.isSafeInteger(seq + 1))
258
+ throw new RangeError('Delivery sequence exhausted');
259
+ this.db.prepare('UPDATE meta SET next_seq=?').run(seq + 1);
260
+ this.db.prepare('INSERT INTO cards VALUES(?,?,?,?,?,?)').run(seq, input.owner, input.key, input.text, input.details, now);
261
+ return { status: suppressed ? 'suppressed' : 'posted' };
262
+ });
263
+ }
264
+ suppress(input) {
265
+ return this.transaction(true, now => {
266
+ const permanentId = input.scope === 'exact' && input.expiresAt === undefined
267
+ ? this.db.prepare("SELECT id FROM suppressions WHERE owner=? AND key=? AND scope='exact' AND expires_at IS NULL").get(input.owner, input.key)?.id
268
+ : undefined;
269
+ const id = permanentId ?? randomUUID();
270
+ const matching = this.db.prepare('SELECT * FROM cards WHERE owner=?').all(input.owner)
271
+ .filter(row => input.scope === 'exact' ? row.key === input.key : String(row.key).startsWith(input.key));
272
+ const inCustody = matching.some(row => this.awaitingEffect(row.seq));
273
+ if (permanentId === undefined)
274
+ this.db.prepare('INSERT INTO suppressions VALUES(?,?,?,?,?,?)').run(id, input.owner, input.key, input.scope, now, input.expiresAt ?? null);
275
+ if (input.scope === 'exact' && input.expiresAt === undefined)
276
+ for (const row of matching) {
277
+ if (!this.awaitingEffect(row.seq))
278
+ this.deleteCard(row.seq);
279
+ }
280
+ return { id, status: inCustody ? 'in-custody' : 'suppressed' };
281
+ });
282
+ }
283
+ renewSuppression(input) {
284
+ return this.transaction(true, now => this.db.prepare('UPDATE suppressions SET expires_at=? WHERE owner=? AND id=? AND (expires_at IS NULL OR expires_at>?)').run(input.expiresAt, input.owner, input.id, now).changes > 0);
285
+ }
286
+ releaseSuppression(input) {
287
+ return this.transaction(true, () => this.db.prepare('DELETE FROM suppressions WHERE owner=? AND id=?').run(input.owner, input.id).changes > 0);
288
+ }
289
+ cancel(input) {
290
+ return this.transaction(true, () => {
291
+ if (!this.attached(input.owner, input.generation))
292
+ return false;
293
+ this.db.prepare(`INSERT INTO cancellations VALUES(?,?,(SELECT next_seq-1 FROM meta))
294
+ ON CONFLICT(owner) DO UPDATE SET generation=excluded.generation,through_seq=MAX(through_seq,excluded.through_seq)`).run(input.owner, input.generation);
295
+ return true;
296
+ });
297
+ }
298
+ isSuppressed(owner, key, now) {
299
+ return !!this.db.prepare(`SELECT 1 FROM suppressions WHERE owner=? AND (expires_at IS NULL OR expires_at>?)
300
+ AND ((scope='exact' AND key=?) OR (scope='prefix' AND substr(?,1,length(key))=key)) LIMIT 1`).get(owner, now, key, key);
301
+ }
302
+ eligible(row, now) {
303
+ if (this.isSuppressed(String(row.owner), String(row.key), now))
304
+ return false;
305
+ if (this.db.prepare('SELECT 1 FROM receipts WHERE owner=? AND key=?').get(row.owner, row.key))
306
+ return false;
307
+ const cancellation = this.db.prepare('SELECT through_seq FROM cancellations WHERE owner=?').get(row.owner);
308
+ return !(cancellation && Number(cancellation.through_seq) >= Number(row.seq));
309
+ }
310
+ insertAttempt(seq, generation, track, routeKind, now, leaseMs) {
311
+ const token = randomUUID();
312
+ this.db.prepare("INSERT INTO attempts VALUES(?,?,?,?,?,'dispatching','{}',?,?,NULL)").run(token, seq, generation, track, routeKind, now, now + leaseMs);
313
+ return this.getAttempt(token);
314
+ }
315
+ claimPresentation(owner, generation, budgetChars, leaseMs, signal) {
316
+ return this.transaction(true, now => {
317
+ if (!this.attached(owner, generation))
318
+ return undefined;
319
+ this.normalize(now);
320
+ const cards = [], attempts = [];
321
+ let size = 0;
322
+ for (const row of this.db.prepare('SELECT * FROM cards WHERE owner=? ORDER BY seq').all(owner)) {
323
+ if (!this.eligible(row, now) || this.listAttempts(row.seq).some(holds))
324
+ continue;
325
+ const next = card(row);
326
+ const nextSize = size + next.text.length + (cards.length ? 2 : 0);
327
+ if (nextSize > budgetChars && cards.length > 0)
328
+ break;
329
+ size = nextSize;
330
+ cards.push(next);
331
+ attempts.push(this.insertAttempt(next.seq, generation, 'presentation', null, now, leaseMs));
332
+ if (size > budgetChars)
333
+ break;
334
+ }
335
+ return cards.length ? { cards, attempts } : undefined;
336
+ }, signal);
337
+ }
338
+ claimHandoff(owner, key, generation, kind, leaseMs, signal) {
339
+ return this.transaction(true, now => {
340
+ const route = this.attached(owner, generation)?.routes.find(route => route.kind === kind);
341
+ if (!route)
342
+ return undefined;
343
+ this.normalize(now);
344
+ const row = this.db.prepare('SELECT * FROM cards WHERE owner=? AND key=?').get(owner, key);
345
+ if (!row || !this.eligible(row, now) || this.listAttempts(row.seq).some(holds))
346
+ return undefined;
347
+ return { card: card(row), route, attempt: this.insertAttempt(row.seq, generation, 'handoff', route.kind, now, leaseMs) };
348
+ }, signal);
349
+ }
350
+ current(tokens, signal, route) {
351
+ if (signal?.aborted)
352
+ return Promise.resolve(false);
353
+ return this.transaction(false, now => tokens.every(token => {
354
+ const a = this.getAttempt(token);
355
+ if (!a || a.outcome !== 'dispatching' || a.expiresAt <= now)
356
+ return false;
357
+ const row = this.db.prepare('SELECT * FROM cards WHERE seq=?').get(a.seq);
358
+ const s = row && this.attached(row.owner, a.generation);
359
+ const registered = s?.routes.find(candidate => candidate.kind === route?.kind);
360
+ return !!row && !!s && this.eligible(row, now) && (route === undefined || !!registered
361
+ && Object.keys(route.address).length === Object.keys(registered.address).length
362
+ && Object.entries(route.address).every(([key, value]) => Object.hasOwn(registered.address, key) && registered.address[key] === value));
363
+ }), signal);
364
+ }
365
+ prepare(token, value, signal) {
366
+ const encoded = JSON.stringify(value);
367
+ return this.transaction(true, now => {
368
+ const a = this.getAttempt(token);
369
+ if (!a || a.outcome !== 'dispatching' || a.expiresAt <= now)
370
+ return false;
371
+ if (a.correlation.payloadHash !== undefined && a.correlation.payloadHash !== value.payloadHash)
372
+ return false;
373
+ const row = this.db.prepare('SELECT owner FROM cards WHERE seq=?').get(a.seq);
374
+ if (!row || !this.attached(row.owner, a.generation))
375
+ return false;
376
+ this.db.prepare('UPDATE attempts SET correlation_json=? WHERE token=?').run(encoded, token);
377
+ return true;
378
+ }, signal);
379
+ }
380
+ receipt(seq, now) {
381
+ this.db.prepare('INSERT INTO receipts SELECT owner,key,? FROM cards WHERE seq=? ON CONFLICT(owner,key) DO NOTHING').run(now, seq);
382
+ }
383
+ confirm(handoff, now) {
384
+ if (handoff.track !== 'handoff' || !['dispatching', 'accepted', 'unknown'].includes(handoff.outcome))
385
+ return false;
386
+ const previous = this.listAttempts(handoff.seq).filter(a => a.track === 'presentation');
387
+ if (previous.some(a => a.outcome === 'presented'))
388
+ return true;
389
+ if (previous.some(holds))
390
+ return false;
391
+ const p = this.insertAttempt(handoff.seq, handoff.generation, 'presentation', handoff.routeKind, now, 1);
392
+ this.db.prepare("UPDATE attempts SET outcome='presented',correlation_json=?,settled_at=? WHERE token=?").run(JSON.stringify(handoff.correlation), now, p.token);
393
+ this.receipt(handoff.seq, now);
394
+ return true;
395
+ }
396
+ settle(tokens, outcome) {
397
+ return this.transaction(true, now => {
398
+ const attempts = tokens.map(token => this.getAttempt(token));
399
+ if (attempts.some(a => !a || a.track === 'presentation' && outcome === 'accepted'
400
+ || !['dispatching', 'unknown'].includes(a.outcome) && a.outcome !== outcome))
401
+ return false;
402
+ for (const a of attempts) {
403
+ if (a.outcome === outcome)
404
+ continue;
405
+ if (a.outcome === 'unknown' && !['accepted', 'presented'].includes(outcome))
406
+ return false;
407
+ }
408
+ for (const a of attempts) {
409
+ if (a.outcome === outcome)
410
+ continue;
411
+ if (a.track === 'handoff' && outcome === 'presented') {
412
+ if (!this.confirm(a, now))
413
+ throw new Error('Cannot confirm handoff presentation');
414
+ this.db.prepare("UPDATE attempts SET outcome='accepted',settled_at=? WHERE token=?").run(now, a.token);
415
+ }
416
+ else {
417
+ this.db.prepare('UPDATE attempts SET outcome=?,settled_at=? WHERE token=?').run(outcome, now, a.token);
418
+ if (outcome === 'presented')
419
+ this.receipt(a.seq, now);
420
+ }
421
+ }
422
+ return true;
423
+ });
424
+ }
425
+ handoffFacts(token) {
426
+ const a = this.getAttempt(token);
427
+ if (!a || a.track !== 'handoff' || !a.routeKind || !a.correlation.payloadHash)
428
+ return undefined;
429
+ const row = this.db.prepare('SELECT * FROM cards WHERE seq=?').get(a.seq);
430
+ if (!row)
431
+ return undefined;
432
+ return { attempt: a, row, ref: { token: a.token, owner: row.owner, generation: a.generation,
433
+ payloadHash: a.correlation.payloadHash, routeKind: a.routeKind } };
434
+ }
435
+ matchingHandoff(ref) {
436
+ if (!ref || typeof ref !== 'object' || typeof ref.token !== 'string')
437
+ return undefined;
438
+ const facts = this.handoffFacts(ref.token);
439
+ return facts && facts.ref.owner === ref.owner && facts.ref.generation === ref.generation
440
+ && facts.ref.payloadHash === ref.payloadHash && ref.routeKind === facts.ref.routeKind ? facts : undefined;
441
+ }
442
+ currentHandoff(facts, now) {
443
+ const s = this.attached(facts.ref.owner, facts.ref.generation);
444
+ return facts.attempt.expiresAt > now && !!s && s.routes.some(route => route.kind === facts.ref.routeKind) && this.eligible(facts.row, now);
445
+ }
446
+ handoff(token) {
447
+ return this.transaction(false, () => this.handoffFacts(token)?.ref);
448
+ }
449
+ guardHandoff(ref) {
450
+ return this.transaction(false, now => {
451
+ const facts = this.matchingHandoff(ref);
452
+ return !!facts && ['dispatching', 'unknown'].includes(facts.attempt.outcome) && this.currentHandoff(facts, now);
453
+ });
454
+ }
455
+ acceptHandoff(ref) {
456
+ return this.transaction(true, now => {
457
+ const a = this.matchingHandoff(ref)?.attempt;
458
+ if (!a || !['dispatching', 'unknown', 'accepted'].includes(a.outcome))
459
+ return false;
460
+ if (a.outcome !== 'accepted')
461
+ this.db.prepare("UPDATE attempts SET outcome='accepted',settled_at=? WHERE token=?").run(now, a.token);
462
+ return true;
463
+ });
464
+ }
465
+ observePresentation(handoff, options) {
466
+ return this.transaction(true, now => {
467
+ const facts = typeof handoff === 'string' ? undefined : this.matchingHandoff(handoff);
468
+ if (options?.requireCurrent) {
469
+ const s = facts && this.attached(facts.ref.owner, facts.ref.generation);
470
+ if (!facts || !s || !s.routes.some(route => route.kind === facts.ref.routeKind) || !this.eligible(facts.row, now)
471
+ || !!this.db.prepare(`SELECT 1 FROM attempts WHERE seq=? AND rowid>(SELECT rowid FROM attempts WHERE token=?) LIMIT 1`)
472
+ .get(facts.attempt.seq, facts.attempt.token))
473
+ return false;
474
+ }
475
+ const a = typeof handoff === 'string' ? this.getAttempt(handoff) : facts?.attempt;
476
+ return a ? this.confirm(a, now) : false;
477
+ });
478
+ }
479
+ receipts(owner, options) {
480
+ return this.transaction(false, () => this.db.prepare('SELECT * FROM receipts WHERE owner=? AND at>=? ORDER BY at,key').all(owner, options.since ?? -Number.MAX_VALUE)
481
+ .filter(row => options.keys === undefined || options.keys.includes(row.key))
482
+ .map(row => ({ owner: row.owner, key: row.key, at: row.at })));
483
+ }
484
+ health(owner) {
485
+ return this.transaction(false, now => {
486
+ const all = this.db.prepare('SELECT a.* FROM attempts a JOIN cards c ON c.seq=a.seq WHERE (? IS NULL OR c.owner=?) ORDER BY a.started_at,a.token').all(owner ?? null, owner ?? null).map(attempt);
487
+ const repeatedFailures = this.db.prepare(`SELECT c.owner,c.key,COUNT(*) AS count FROM attempts a JOIN cards c ON c.seq=a.seq
488
+ WHERE a.outcome='failed' AND (? IS NULL OR c.owner=?) GROUP BY c.owner,c.key HAVING COUNT(*)>=2 ORDER BY c.seq`).all(owner ?? null, owner ?? null)
489
+ .map(row => ({ owner: row.owner, key: row.key, count: row.count }));
490
+ const sessions = this.db.prepare("SELECT * FROM sessions WHERE state='attached' AND (? IS NULL OR owner=?) ORDER BY owner").all(owner ?? null, owner ?? null).map(row => session(row));
491
+ return { unknownAttempts: all.filter(a => a.outcome === 'unknown' || a.outcome === 'dispatching' && a.expiresAt <= now)
492
+ .map(a => a.outcome === 'dispatching' ? { ...a, outcome: 'unknown' } : a), repeatedFailures,
493
+ sessionsWithoutRoute: sessions.filter(s => s.routes.length === 0).map(s => s.owner) };
494
+ });
495
+ }
496
+ }