@a-dray/aglib 0.1.0

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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/dist/agent.d.ts +158 -0
  4. package/dist/agent.js +2 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/content.d.ts +45 -0
  7. package/dist/content.js +11 -0
  8. package/dist/content.js.map +1 -0
  9. package/dist/harness/adapters/acp/index.d.ts +92 -0
  10. package/dist/harness/adapters/acp/index.js +479 -0
  11. package/dist/harness/adapters/acp/index.js.map +1 -0
  12. package/dist/harness/adapters/acp/rpc.d.ts +23 -0
  13. package/dist/harness/adapters/acp/rpc.js +103 -0
  14. package/dist/harness/adapters/acp/rpc.js.map +1 -0
  15. package/dist/harness/adapters/native/compaction.d.ts +16 -0
  16. package/dist/harness/adapters/native/compaction.js +55 -0
  17. package/dist/harness/adapters/native/compaction.js.map +1 -0
  18. package/dist/harness/adapters/native/index.d.ts +2 -0
  19. package/dist/harness/adapters/native/index.js +2 -0
  20. package/dist/harness/adapters/native/index.js.map +1 -0
  21. package/dist/harness/adapters/native/loop.d.ts +25 -0
  22. package/dist/harness/adapters/native/loop.js +153 -0
  23. package/dist/harness/adapters/native/loop.js.map +1 -0
  24. package/dist/harness/harness.d.ts +129 -0
  25. package/dist/harness/harness.js +2 -0
  26. package/dist/harness/harness.js.map +1 -0
  27. package/dist/harness/index.d.ts +3 -0
  28. package/dist/harness/index.js +2 -0
  29. package/dist/harness/index.js.map +1 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.js +5 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/json.d.ts +4 -0
  34. package/dist/json.js +2 -0
  35. package/dist/json.js.map +1 -0
  36. package/dist/model/adapters/anthropic/index.d.ts +34 -0
  37. package/dist/model/adapters/anthropic/index.js +271 -0
  38. package/dist/model/adapters/anthropic/index.js.map +1 -0
  39. package/dist/model/adapters/fake/index.d.ts +27 -0
  40. package/dist/model/adapters/fake/index.js +37 -0
  41. package/dist/model/adapters/fake/index.js.map +1 -0
  42. package/dist/model/adapters/openai-compatible/index.d.ts +29 -0
  43. package/dist/model/adapters/openai-compatible/index.js +226 -0
  44. package/dist/model/adapters/openai-compatible/index.js.map +1 -0
  45. package/dist/model/conformance.d.ts +210 -0
  46. package/dist/model/conformance.js +427 -0
  47. package/dist/model/conformance.js.map +1 -0
  48. package/dist/model/index.d.ts +2 -0
  49. package/dist/model/index.js +2 -0
  50. package/dist/model/index.js.map +1 -0
  51. package/dist/model/model.d.ts +85 -0
  52. package/dist/model/model.js +8 -0
  53. package/dist/model/model.js.map +1 -0
  54. package/dist/result.d.ts +23 -0
  55. package/dist/result.js +3 -0
  56. package/dist/result.js.map +1 -0
  57. package/dist/run.d.ts +10 -0
  58. package/dist/run.js +477 -0
  59. package/dist/run.js.map +1 -0
  60. package/dist/sandbox/adapters/docker.d.ts +22 -0
  61. package/dist/sandbox/adapters/docker.js +443 -0
  62. package/dist/sandbox/adapters/docker.js.map +1 -0
  63. package/dist/sandbox/adapters/local.d.ts +11 -0
  64. package/dist/sandbox/adapters/local.js +284 -0
  65. package/dist/sandbox/adapters/local.js.map +1 -0
  66. package/dist/sandbox/conformance.d.ts +73 -0
  67. package/dist/sandbox/conformance.js +309 -0
  68. package/dist/sandbox/conformance.js.map +1 -0
  69. package/dist/sandbox/index.d.ts +1 -0
  70. package/dist/sandbox/index.js +2 -0
  71. package/dist/sandbox/index.js.map +1 -0
  72. package/dist/sandbox/sandbox.d.ts +172 -0
  73. package/dist/sandbox/sandbox.js +2 -0
  74. package/dist/sandbox/sandbox.js.map +1 -0
  75. package/dist/session/entry.d.ts +196 -0
  76. package/dist/session/entry.js +2 -0
  77. package/dist/session/entry.js.map +1 -0
  78. package/dist/session/index.d.ts +2 -0
  79. package/dist/session/index.js +2 -0
  80. package/dist/session/index.js.map +1 -0
  81. package/dist/session/log.d.ts +20 -0
  82. package/dist/session/log.js +24 -0
  83. package/dist/session/log.js.map +1 -0
  84. package/dist/session/messages.d.ts +42 -0
  85. package/dist/session/messages.js +99 -0
  86. package/dist/session/messages.js.map +1 -0
  87. package/dist/store/adapters/sqlite.d.ts +20 -0
  88. package/dist/store/adapters/sqlite.js +365 -0
  89. package/dist/store/adapters/sqlite.js.map +1 -0
  90. package/dist/store/conformance.d.ts +66 -0
  91. package/dist/store/conformance.js +580 -0
  92. package/dist/store/conformance.js.map +1 -0
  93. package/dist/store/index.d.ts +1 -0
  94. package/dist/store/index.js +2 -0
  95. package/dist/store/index.js.map +1 -0
  96. package/dist/store/store.d.ts +244 -0
  97. package/dist/store/store.js +2 -0
  98. package/dist/store/store.js.map +1 -0
  99. package/dist/tools/execute.d.ts +24 -0
  100. package/dist/tools/execute.js +105 -0
  101. package/dist/tools/execute.js.map +1 -0
  102. package/dist/tools/index.d.ts +2 -0
  103. package/dist/tools/index.js +2 -0
  104. package/dist/tools/index.js.map +1 -0
  105. package/dist/tools/tool.d.ts +106 -0
  106. package/dist/tools/tool.js +34 -0
  107. package/dist/tools/tool.js.map +1 -0
  108. package/package.json +119 -0
@@ -0,0 +1,580 @@
1
+ /**
2
+ * Short enough that a case can wait a claim out, long enough that two calls in
3
+ * a row are inside one — a store reached over a socket takes real time, and a
4
+ * window measured in tens of milliseconds tests the scheduler rather than the
5
+ * contract.
6
+ */
7
+ const CLAIM_MS = 500;
8
+ const LAPSE_MS = CLAIM_MS + 150;
9
+ /**
10
+ * A readable name for a session or a run, and a real id for the store.
11
+ *
12
+ * The cases read better with `parent` and `child` than with two UUIDs, and a
13
+ * store reads them as what the port says they are. Memoised for the whole run
14
+ * rather than per case, which costs nothing: every case gets its own store.
15
+ */
16
+ const named = new Map();
17
+ const id = (name) => {
18
+ const known = named.get(name);
19
+ if (known)
20
+ return known;
21
+ const minted = crypto.randomUUID();
22
+ named.set(name, minted);
23
+ return minted;
24
+ };
25
+ const agent = { id: "conformance", version: "1" };
26
+ const said = (input, runId = id("r")) => ({ type: "run.started", runId, input });
27
+ const ended = (runId = id("r")) => ({ type: "run.finished", runId, outcome: "completed" });
28
+ const wrote = (content, runId = id("r")) => ({ type: "assistant", runId, content });
29
+ const wait = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
30
+ function fail(what) {
31
+ throw new Error(`store conformance: ${what}`);
32
+ }
33
+ function holds(condition, what) {
34
+ if (!condition)
35
+ fail(what);
36
+ }
37
+ function same(actual, expected) {
38
+ if (actual === expected)
39
+ return true;
40
+ if (Array.isArray(actual) && Array.isArray(expected)) {
41
+ return actual.length === expected.length && actual.every((item, at) => same(item, expected[at]));
42
+ }
43
+ if (typeof actual !== "object" || typeof expected !== "object" || !actual || !expected)
44
+ return false;
45
+ const left = Object.keys(actual).sort();
46
+ const right = Object.keys(expected).sort();
47
+ return same(left, right)
48
+ && left.every((key) => same(actual[key], expected[key]));
49
+ }
50
+ function equals(actual, expected, what) {
51
+ if (!same(actual, expected)) {
52
+ fail(`${what} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
53
+ }
54
+ }
55
+ /** Unwraps a `Result` the case expects to have succeeded. */
56
+ function got(result, what) {
57
+ if (!result.ok)
58
+ fail(`${what} — ${result.error.code}: ${result.error.message}`);
59
+ return result.value;
60
+ }
61
+ export function defineStoreConformance(subject) {
62
+ const cases = [];
63
+ const define = (name, body) => {
64
+ cases.push({
65
+ name,
66
+ async run() {
67
+ const store = await subject.open({ claimMs: CLAIM_MS });
68
+ try {
69
+ await body(store);
70
+ }
71
+ finally {
72
+ await store.close();
73
+ await subject.release?.(store);
74
+ }
75
+ },
76
+ });
77
+ };
78
+ /** An empty session — no entries, nothing owed — under an optional application key. */
79
+ const created = async (store, sessionId, key) => {
80
+ got(await store.create({ sessionId, agent, ...(key ? { key } : {}) }), `create ${sessionId}`);
81
+ };
82
+ const entriesOf = async (store, sessionId) => got(await store.read({ sessionId }), `read ${sessionId}`).entries;
83
+ const claim = async (store) => got(await store.next({}), "next");
84
+ const interrupted = async (store) => got(await store.interrupted({}), "interrupted");
85
+ // ---- The log ------------------------------------------------------------
86
+ define("entries come back in order, and a cursor returns only what is new", async (store) => {
87
+ await created(store, id("s"));
88
+ const written = got(await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [said("one"), said("two")] }), "append");
89
+ equals(written.seq, 2, "the append reports the position it left the session at");
90
+ const all = got(await store.read({ sessionId: id("s") }), "read");
91
+ equals(all.entries.map((entry) => entry.seq), [1, 2], "positions are 1 and 2");
92
+ equals(all.seq, 2, "the session is at 2");
93
+ holds(all.entries.every((entry) => typeof entry.at === "string" && !Number.isNaN(Date.parse(entry.at))), "every stored entry carries an ISO 8601 commit time");
94
+ const after = got(await store.read({ sessionId: id("s"), afterSeq: 1 }), "read after 1");
95
+ equals(after.entries.map((entry) => entry.seq), [2], "a cursor returns only what is new");
96
+ equals(after.seq, 2, "a cursor read still reports the session's real position");
97
+ });
98
+ define("a session that was never created is not-found rather than empty", async (store) => {
99
+ const read = await store.read({ sessionId: id("nobody") });
100
+ holds(!read.ok, "reading an unknown session fails");
101
+ equals(read.error.code, "not-found", "the code says not-found");
102
+ });
103
+ define("entries survive the round trip whole", async (store) => {
104
+ await created(store, id("s"));
105
+ const rich = {
106
+ type: "assistant", runId: id("r"),
107
+ content: [{ type: "text", text: "here" }, { type: "opaque", provider: "acme", data: { keep: [1, null, "x"] } }],
108
+ calls: [{ callId: "c1", name: "look", arguments: "{\"at\":1}" }],
109
+ usage: { inputTokens: 10, outputTokens: 2 },
110
+ generation: { model: "m", startedAt: "2026-01-01T00:00:00.000Z", endedAt: "2026-01-01T00:00:01.000Z" },
111
+ };
112
+ got(await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [rich] }), "append");
113
+ const [stored] = await entriesOf(store, id("s"));
114
+ holds(stored !== undefined, "the entry was stored");
115
+ const { seq, at, ...body } = stored;
116
+ equals(body, rich, "the entry comes back exactly as it went in");
117
+ });
118
+ define("a session carries the agent and the metadata it was created with", async (store) => {
119
+ got(await store.create({ sessionId: id("s"), agent, key: "tenant", metadata: { model: "one" } }), "create");
120
+ got(await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [said("go")] }), "append");
121
+ const read = got(await store.read({ sessionId: id("s") }), "read");
122
+ equals(read.agent, agent, "a read says whose session this is");
123
+ equals(read.metadata, { model: "one" }, "and hands back what create was given, uninterpreted");
124
+ // A summary carries the same facts because a listing is how an application
125
+ // finds a session it has no id for — one that had to read each row to learn
126
+ // its agent or position would be a listing in name only.
127
+ const [summary] = got(await store.list({ key: "tenant" }), "list");
128
+ holds(summary !== undefined, "the session is listed under its key");
129
+ equals(summary.agent, agent, "a summary says whose session it is");
130
+ equals(summary.seq, 1, "and where its log has got to");
131
+ equals(summary.metadata, { model: "one" }, "and carries the metadata without a second read");
132
+ });
133
+ // ---- Compare-and-swap ---------------------------------------------------
134
+ define("a write from a stale position is refused and told the real one", async (store) => {
135
+ await created(store, id("s"));
136
+ got(await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [said("one")] }), "append");
137
+ const stale = await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [said("two")] });
138
+ holds(!stale.ok, "a stale write is refused");
139
+ equals(stale.error.code, "conflict", "the code says conflict");
140
+ equals(stale.error.actualSeq, 1, "the refusal carries the real position");
141
+ equals((await entriesOf(store, id("s"))).length, 1, "nothing of the refused write survives");
142
+ });
143
+ define("an append to a session that is not there is not-found", async (store) => {
144
+ const missing = await store.append({ sessionId: id("nobody"), expectedSeq: 0, entries: [said("one")] });
145
+ holds(!missing.ok, "appending to an unknown session fails");
146
+ equals(missing.error.code, "not-found", "the code says not-found");
147
+ });
148
+ define("metadata merges rather than replacing", async (store) => {
149
+ await created(store, id("s"));
150
+ got(await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [], metadata: { model: "one", tenant: "acme" } }), "first merge");
151
+ got(await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [], metadata: { model: "two" } }), "second merge");
152
+ equals(got(await store.read({ sessionId: id("s") }), "read").metadata, { model: "two", tenant: "acme" }, "the key that was not written keeps its value");
153
+ });
154
+ // ---- Delivery, atomically ----------------------------------------------
155
+ define("a delivery to another session commits with the sender's own entries", async (store) => {
156
+ await created(store, id("parent"));
157
+ await created(store, id("child"), "parent");
158
+ got(await store.append({
159
+ sessionId: id("parent"), expectedSeq: 0, entries: [said("spawn a child")],
160
+ enqueue: [{ sessionId: id("child"), input: "do the thing", from: { kind: "session", id: id("parent") } }],
161
+ }), "append with a delivery");
162
+ const runnable = await claim(store);
163
+ holds(runnable?.sessionId === id("child"), "the recipient is runnable");
164
+ // The fields the port names, not the object. A store is entitled to hand
165
+ // back a delivery with its default priority materialised — the caller sent
166
+ // none and `next` is what none means — and comparing whole objects called
167
+ // that a difference.
168
+ equals(runnable.pending.length, 1, "one delivery is waiting");
169
+ const [waiting] = runnable.pending;
170
+ equals(waiting?.sessionId, id("child"), "addressed to the recipient");
171
+ equals(waiting?.input, "do the thing", "carrying what was sent");
172
+ equals(waiting?.from?.kind, "session", "sent by a session");
173
+ equals(waiting?.from?.id, id("parent"), "and which one");
174
+ equals(runnable.seq, 0, "the recipient's position is its own, not the sender's");
175
+ });
176
+ define("a rejected append delivers nothing", async (store) => {
177
+ await created(store, id("sender"));
178
+ await created(store, id("target"));
179
+ got(await store.append({ sessionId: id("sender"), expectedSeq: 0, entries: [said("one")] }), "append");
180
+ const stale = await store.append({
181
+ sessionId: id("sender"), expectedSeq: 0, entries: [said("two")],
182
+ enqueue: [{ sessionId: id("target"), input: "never sent" }],
183
+ });
184
+ holds(!stale.ok, "the stale write is refused");
185
+ equals(got(await store.read({ sessionId: id("target") }), "read target").pending, [], "the recipient received nothing");
186
+ });
187
+ define("a delivery to a session that is not there fails and commits nothing", async (store) => {
188
+ await created(store, id("sender"));
189
+ const missing = await store.append({
190
+ sessionId: id("sender"), expectedSeq: 0, entries: [said("one")],
191
+ enqueue: [{ sessionId: id("nobody"), input: "into the void" }],
192
+ });
193
+ holds(!missing.ok, "a delivery to an unknown session fails");
194
+ equals(missing.error.code, "not-found", "the code says not-found");
195
+ equals((await entriesOf(store, id("sender"))).length, 0, "the sender's own entries went with it");
196
+ });
197
+ // ---- The queue and the claim -------------------------------------------
198
+ define("next claims without consuming: the queue survives until an append commits it", async (store) => {
199
+ await created(store, id("s"));
200
+ got(await store.append({
201
+ sessionId: id("s"), expectedSeq: 0, entries: [], enqueue: [{ sessionId: id("s"), input: "read me" }],
202
+ }), "enqueue");
203
+ const claimed = await claim(store);
204
+ holds(claimed?.sessionId === id("s"), "the session is claimed");
205
+ equals(got(await store.read({ sessionId: id("s") }), "read").pending.length, 1, "reading the queue did not empty it");
206
+ got(await store.append({
207
+ sessionId: id("s"), expectedSeq: claimed.seq, entries: [said("read me")], takePending: 1,
208
+ }), "commit the delivery as an entry");
209
+ equals(got(await store.read({ sessionId: id("s") }), "read").pending, [], "the write that recorded it is what removed it");
210
+ });
211
+ define("an append that loses its position destroys no messages", async (store) => {
212
+ await created(store, id("s"));
213
+ got(await store.append({
214
+ sessionId: id("s"), expectedSeq: 0, entries: [], enqueue: [{ sessionId: id("s"), input: "read me" }],
215
+ }), "enqueue");
216
+ const claimed = await claim(store);
217
+ holds(claimed !== undefined, "the session is claimed");
218
+ // Somebody else advances the session while this worker holds the queue.
219
+ got(await store.append({ sessionId: id("s"), expectedSeq: claimed.seq, entries: [wrote("elsewhere")] }), "other writer");
220
+ const lost = await store.append({
221
+ sessionId: id("s"), expectedSeq: claimed.seq, entries: [said("read me")], takePending: 1,
222
+ });
223
+ holds(!lost.ok, "the loser is refused");
224
+ equals(got(await store.read({ sessionId: id("s") }), "read").pending.length, 1, "the messages it was carrying are where it found them");
225
+ });
226
+ define("takePending removes what was committed and nothing that arrived since", async (store) => {
227
+ await created(store, id("s"));
228
+ got(await store.append({
229
+ sessionId: id("s"), expectedSeq: 0, entries: [],
230
+ enqueue: [{ sessionId: id("s"), input: "first" }, { sessionId: id("s"), input: "second" }],
231
+ }), "enqueue two");
232
+ const claimed = await claim(store);
233
+ holds(claimed?.pending.length === 2, "both are reported");
234
+ got(await store.append({
235
+ sessionId: id("s"), expectedSeq: claimed.seq, entries: [], enqueue: [{ sessionId: id("s"), input: "third" }],
236
+ }), "a third arrives while the activation runs");
237
+ got(await store.append({
238
+ sessionId: id("s"), expectedSeq: claimed.seq, entries: [said("first"), said("second")], takePending: 2,
239
+ }), "commit the two that were taken");
240
+ equals(got(await store.read({ sessionId: id("s") }), "read").pending.map((one) => one.input), ["third"], "what arrived since is behind them and survives");
241
+ });
242
+ define("a session is claimed while it runs and released when the run finishes", async (store) => {
243
+ await created(store, id("s"));
244
+ got(await store.append({
245
+ sessionId: id("s"), expectedSeq: 0, entries: [], enqueue: [{ sessionId: id("s"), input: "go" }],
246
+ }), "enqueue");
247
+ const claimed = await claim(store);
248
+ holds(claimed !== undefined, "the session is claimed");
249
+ got(await store.append({
250
+ sessionId: id("s"), expectedSeq: claimed.seq, entries: [said("go")], takePending: 1,
251
+ }), "open the activation");
252
+ got(await store.append({
253
+ sessionId: id("s"), expectedSeq: claimed.seq + 1, entries: [], enqueue: [{ sessionId: id("s"), input: "and again" }],
254
+ }), "a message arrives mid-activation");
255
+ equals(await claim(store), undefined, "a second activation of a running session is not offered");
256
+ got(await store.append({ sessionId: id("s"), expectedSeq: claimed.seq + 1, entries: [ended()] }), "finish the run");
257
+ const again = await claim(store);
258
+ holds(again?.sessionId === id("s"), "the finished session is offered with what arrived while it ran");
259
+ });
260
+ define("an open activation holds its claim by writing, not by having started", async (store) => {
261
+ await created(store, id("s"));
262
+ got(await store.append({
263
+ sessionId: id("s"), expectedSeq: 0, entries: [], enqueue: [{ sessionId: id("s"), input: "go" }],
264
+ }), "enqueue");
265
+ const claimed = await claim(store);
266
+ holds(claimed !== undefined, "the session is claimed");
267
+ got(await store.append({
268
+ sessionId: id("s"), expectedSeq: claimed.seq, entries: [said("go")], takePending: 1,
269
+ }), "open the activation");
270
+ got(await store.append({
271
+ sessionId: id("s"), expectedSeq: claimed.seq + 1, entries: [], enqueue: [{ sessionId: id("s"), input: "more" }],
272
+ }), "a message arrives mid-activation");
273
+ // A turn longer than the claim window. Every write the activation makes
274
+ // renews it, or a second worker is handed a session that is still running —
275
+ // and a compare-and-swap can refuse a write, not un-run a tool call.
276
+ await wait(LAPSE_MS);
277
+ got(await store.append({
278
+ sessionId: id("s"), expectedSeq: claimed.seq + 1, entries: [wrote("still working")],
279
+ }), "the activation writes again");
280
+ equals(await claim(store), undefined, "the running session is still not offered");
281
+ });
282
+ define("a claim lapses, so a worker that died does not strand its session", async (store) => {
283
+ await created(store, id("s"));
284
+ got(await store.append({
285
+ sessionId: id("s"), expectedSeq: 0, entries: [], enqueue: [{ sessionId: id("s"), input: "go" }],
286
+ }), "enqueue");
287
+ holds((await claim(store)) !== undefined, "the session is claimed");
288
+ equals(await claim(store), undefined, "and held");
289
+ await wait(LAPSE_MS);
290
+ const again = await claim(store);
291
+ holds(again?.sessionId === id("s"), "the lapsed claim releases the session");
292
+ equals(again.pending.length, 1, "with the messages still on it");
293
+ });
294
+ define("next takes the oldest waiting session, and does not read priority", async (store) => {
295
+ await created(store, id("first"));
296
+ await created(store, id("second"));
297
+ got(await store.append({
298
+ sessionId: id("first"), expectedSeq: 0, entries: [], enqueue: [{ sessionId: id("first"), input: "older" }],
299
+ }), "enqueue to first");
300
+ await wait(15);
301
+ got(await store.append({
302
+ sessionId: id("second"), expectedSeq: 0, entries: [],
303
+ enqueue: [{ sessionId: id("second"), input: "urgent", priority: "interrupt" }],
304
+ }), "enqueue to second");
305
+ // Where a message lands in a running activation says nothing about which
306
+ // session a worker picks up next. An application that needs urgent work
307
+ // scheduled first orders its own workers.
308
+ equals((await claim(store))?.sessionId, id("first"), "the oldest goes first");
309
+ const urgent = await claim(store);
310
+ equals(urgent?.sessionId, id("second"), "then the newer one");
311
+ // Recorded and reported, though nothing here acted on it: the mode is the
312
+ // harness's to honour, and it cannot honour what the store dropped.
313
+ equals(urgent?.pending[0]?.priority, "interrupt", "the place the sender asked for reaches the reader");
314
+ });
315
+ // ---- Delivery identity --------------------------------------------------
316
+ define("a delivery already waiting under the same id does not arrive twice", async (store) => {
317
+ await created(store, id("s"));
318
+ for (let attempt = 0; attempt < 2; attempt += 1) {
319
+ got(await store.append({
320
+ sessionId: id("s"), expectedSeq: 0, entries: [],
321
+ enqueue: [{ sessionId: id("s"), input: "once", id: "d1" }],
322
+ }), `send ${attempt}`);
323
+ }
324
+ equals(got(await store.read({ sessionId: id("s") }), "read").pending.length, 1, "the retry collapsed into one");
325
+ });
326
+ define("a delivery id is remembered after its first copy has been read", async (store) => {
327
+ await created(store, id("s"));
328
+ got(await store.append({
329
+ sessionId: id("s"), expectedSeq: 0, entries: [], enqueue: [{ sessionId: id("s"), input: "once", id: "d1" }],
330
+ }), "first send");
331
+ const claimed = await claim(store);
332
+ holds(claimed !== undefined, "the session is claimed");
333
+ got(await store.append({
334
+ sessionId: id("s"), expectedSeq: claimed.seq, entries: [said("once")], takePending: 1,
335
+ }), "read it");
336
+ got(await store.append({
337
+ sessionId: id("s"), expectedSeq: claimed.seq + 1, entries: [],
338
+ enqueue: [{ sessionId: id("s"), input: "once", id: "d1" }],
339
+ }), "the same delivery again");
340
+ equals(got(await store.read({ sessionId: id("s") }), "read").pending, [], "an id is not forgotten the moment the queue drains");
341
+ });
342
+ define("two senders numbering their messages from one do not suppress each other", async (store) => {
343
+ await created(store, id("one"));
344
+ await created(store, id("two"));
345
+ await created(store, id("target"));
346
+ got(await store.append({
347
+ sessionId: id("one"), expectedSeq: 0, entries: [],
348
+ enqueue: [{ sessionId: id("target"), input: "from one", from: { kind: "session", id: id("one") }, id: "1" }],
349
+ }), "sender one");
350
+ got(await store.append({
351
+ sessionId: id("two"), expectedSeq: 0, entries: [],
352
+ enqueue: [{ sessionId: id("target"), input: "from two", from: { kind: "session", id: id("two") }, id: "1" }],
353
+ }), "sender two");
354
+ equals(got(await store.read({ sessionId: id("target") }), "read").pending.length, 2, "an id is the sender's own, so two of them are two deliveries");
355
+ });
356
+ // ---- Listing ------------------------------------------------------------
357
+ define("listing is by the application's opaque key, newest first", async (store) => {
358
+ await created(store, id("a"), "tenant-1");
359
+ await wait(15);
360
+ await created(store, id("b"), "tenant-1");
361
+ await created(store, id("c"), "tenant-2");
362
+ const mine = got(await store.list({ key: "tenant-1" }), "list");
363
+ equals(mine.map((row) => row.sessionId), [id("b"), id("a")], "only that key, newest first");
364
+ equals(mine[0]?.key, "tenant-1", "the key comes back as it was given");
365
+ equals(got(await store.list({}), "list all").length, 3, "listing without a key sees every session");
366
+ });
367
+ define("a listing page does not step over the sessions sharing its boundary", async (store) => {
368
+ // Written in one tick on purpose: without the id in the cursor, a page
369
+ // boundary is an instant and every row sharing it is skipped.
370
+ // `id(...)` shadowed by a loop variable is how a raw label reached a store
371
+ // that keeps session ids as UUIDs, so these are named rather than reused.
372
+ for (const label of ["a", "b", "c"])
373
+ await created(store, id(label), "tenant");
374
+ const first = got(await store.list({ key: "tenant", limit: 1 }), "first page");
375
+ holds(first.length === 1, "one row");
376
+ const cursor = `${first[0].updatedAt}|${first[0].sessionId}`;
377
+ const rest = got(await store.list({ key: "tenant", before: cursor }), "second page");
378
+ equals(rest.length, 2, "the remaining two are on the next page");
379
+ holds(!rest.some((row) => row.sessionId === first[0].sessionId), "and the first is not repeated");
380
+ });
381
+ // ---- Resumption ---------------------------------------------------------
382
+ define("an interrupted activation is found once its claim lapses, and next cannot see it", async (store) => {
383
+ await created(store, id("s"));
384
+ got(await store.append({
385
+ sessionId: id("s"), expectedSeq: 0, entries: [], enqueue: [{ sessionId: id("s"), input: "go" }],
386
+ }), "enqueue");
387
+ const claimed = await claim(store);
388
+ holds(claimed !== undefined, "the session is claimed");
389
+ // The commit that opened the run took the queue with it. This is the state
390
+ // a killed worker leaves: an open activation and nothing owed.
391
+ got(await store.append({
392
+ sessionId: id("s"), expectedSeq: claimed.seq, entries: [said("go")], takePending: 1,
393
+ }), "open the activation");
394
+ equals(await claim(store), undefined, "next has nothing: the session is running and nothing waits");
395
+ equals(await interrupted(store), undefined, "and while the claim holds, nothing is handed back either");
396
+ await wait(LAPSE_MS);
397
+ equals(await claim(store), undefined, "still nothing has been asked for");
398
+ const found = await interrupted(store);
399
+ holds(found?.sessionId === id("s"), "the interrupted activation is found");
400
+ equals(found.seq, claimed.seq + 1, "at the position its last commit left it");
401
+ equals(found.pending, [], "with nothing waiting, by construction");
402
+ });
403
+ define("interrupted claims what it hands out", async (store) => {
404
+ await created(store, id("s"));
405
+ got(await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [said("go")] }), "open the activation");
406
+ await wait(LAPSE_MS);
407
+ holds((await interrupted(store))?.sessionId === id("s"), "the first caller takes it");
408
+ equals(await interrupted(store), undefined, "a second is not handed the same work");
409
+ });
410
+ define("a session that finished is neither runnable nor interrupted", async (store) => {
411
+ await created(store, id("s"));
412
+ got(await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [said("go"), ended()] }), "a whole run");
413
+ await wait(LAPSE_MS);
414
+ equals(await claim(store), undefined, "nothing is owed");
415
+ equals(await interrupted(store), undefined, "and nothing was left half-done");
416
+ });
417
+ define("a session with input waiting belongs to next, not to interrupted", async (store) => {
418
+ await created(store, id("s"));
419
+ got(await store.append({
420
+ sessionId: id("s"), expectedSeq: 0, entries: [said("go")],
421
+ enqueue: [{ sessionId: id("s"), input: "and this too" }],
422
+ }), "an open activation with a message waiting");
423
+ await wait(LAPSE_MS);
424
+ equals(await interrupted(store), undefined, "interrupted leaves it alone");
425
+ equals((await claim(store))?.sessionId, id("s"), "next takes it, with what it is owed");
426
+ });
427
+ define("a claim carries the session's metadata, from either question", async (store) => {
428
+ // The claim is passed whole to the activation that follows, and metadata is
429
+ // how an application configured this session — which model, whose tenant,
430
+ // which sandbox. A claim without it is a claim the worker has to re-read.
431
+ got(await store.create({ sessionId: id("owed"), agent, metadata: { model: "one" } }), "create owed");
432
+ got(await store.create({ sessionId: id("open"), agent, metadata: { model: "two" } }), "create open");
433
+ got(await store.append({
434
+ sessionId: id("owed"), expectedSeq: 0, entries: [], enqueue: [{ sessionId: id("owed"), input: "go" }],
435
+ }), "enqueue to the one that is owed input");
436
+ got(await store.append({ sessionId: id("open"), expectedSeq: 0, entries: [said("go")] }), "open the other");
437
+ const claimed = await claim(store);
438
+ holds(claimed?.sessionId === id("owed"), "next takes the session with input");
439
+ equals(claimed.metadata, { model: "one" }, "and hands back that session's metadata");
440
+ await wait(LAPSE_MS);
441
+ const found = await interrupted(store);
442
+ holds(found?.sessionId === id("open"), "interrupted takes the open one");
443
+ equals(found.metadata, { model: "two" }, "and hands back the same thing");
444
+ });
445
+ define("a claim lapses while a session is only spoken to, never worked", async (store) => {
446
+ // The trap: a store that renewed on any append at all let a message *to* a
447
+ // session renew the claim of the worker that was meant to read it. A dead
448
+ // worker's session then stayed invisible to `next` — the claim never
449
+ // lapsed — and to `interrupted`, whose queue was not empty. The messages
450
+ // stayed there for ever.
451
+ await created(store, id("s"));
452
+ got(await store.append({
453
+ sessionId: id("s"), expectedSeq: 0, entries: [], enqueue: [{ sessionId: id("s"), input: "go" }],
454
+ }), "enqueue");
455
+ const claimed = await claim(store);
456
+ holds(claimed !== undefined, "the session is claimed");
457
+ got(await store.append({
458
+ sessionId: id("s"), expectedSeq: claimed.seq, entries: [said("go")], takePending: 1,
459
+ }), "open the activation");
460
+ // The worker dies here. Somebody keeps talking to the session, right up to
461
+ // the moment of asking — so a store that renews on any write at all still
462
+ // holds a fresh claim, and one that renews only on work does not.
463
+ for (let sent = 0; sent < 4; sent += 1) {
464
+ await wait(CLAIM_MS / 2);
465
+ got(await store.append({
466
+ sessionId: id("s"), expectedSeq: claimed.seq + 1, entries: [],
467
+ enqueue: [{ sessionId: id("s"), input: `and ${sent}` }],
468
+ }), `a later message ${sent}`);
469
+ }
470
+ const again = await claim(store);
471
+ holds(again?.sessionId === id("s"), "the session is offered again once its own writes stop");
472
+ equals(again.pending.length, 4, "with everything that arrived while nobody was reading");
473
+ });
474
+ define("taking more deliveries than are queued is refused, not clamped", async (store) => {
475
+ // Silently dropping messages nothing recorded is the one loss this design
476
+ // exists to prevent, and a caller's off-by-one reaches it.
477
+ await created(store, id("s"));
478
+ got(await store.append({
479
+ sessionId: id("s"), expectedSeq: 0, entries: [],
480
+ enqueue: [{ sessionId: id("s"), input: "one" }, { sessionId: id("s"), input: "two" }],
481
+ }), "enqueue two");
482
+ const over = await store.append({
483
+ sessionId: id("s"), expectedSeq: 0, entries: [said("one")], takePending: 9,
484
+ });
485
+ holds(!over.ok, "a count larger than the queue is refused");
486
+ equals(got(await store.read({ sessionId: id("s") }), "read").pending.length, 2, "and the messages are still there");
487
+ });
488
+ // ---- Being told to look again -------------------------------------------
489
+ if (subject.changes === "none") {
490
+ define("a store with no push channel says so by not offering one", async (store) => {
491
+ holds(store.watch === undefined, "a store declaring no feed must not carry a `watch` that never fires");
492
+ });
493
+ return cases;
494
+ }
495
+ /** The next wake matching `want`, or a failure naming what never arrived. */
496
+ const woken = (store, want, what) => {
497
+ holds(store.watch !== undefined, "a store declaring a feed must offer `watch`");
498
+ return new Promise((resolve, reject) => {
499
+ const timer = setTimeout(() => { off(); reject(new Error(`store conformance: no wake for ${what}`)); }, 4_000);
500
+ const off = store.watch((change) => {
501
+ if (!want(change))
502
+ return;
503
+ clearTimeout(timer);
504
+ off();
505
+ resolve(change);
506
+ });
507
+ });
508
+ };
509
+ define("a change committed through this store wakes a watcher on this store", async (store) => {
510
+ await created(store, id("s"));
511
+ // Not a hypothetical: an implementation skipped its own notification to
512
+ // avoid an echo, and since the surface that writes and the worker that
513
+ // reads were one process, a delivery woke nobody at all.
514
+ const wake = woken(store, (change) => change.sessionId === id("s"), "an append on the same store");
515
+ got(await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [said("one")] }), "append");
516
+ await wake;
517
+ });
518
+ define("a wake means the change is already readable", async (store) => {
519
+ await created(store, id("s"));
520
+ const wake = woken(store, (change) => change.sessionId === id("s"), "an append");
521
+ got(await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [said("one")] }), "append");
522
+ await wake;
523
+ // Looking after a wake finds at least what woke it. A notification sent
524
+ // before the commit is worse than none: the look finds nothing, and nothing
525
+ // ever tells it again.
526
+ equals((await entriesOf(store, id("s"))).length, 1, "the entry that caused the wake is there");
527
+ });
528
+ define("a delivery says the session it landed on is runnable", async (store) => {
529
+ await created(store, id("sender"));
530
+ await created(store, id("target"));
531
+ const wake = woken(store, (change) => change.sessionId === id("target"), "a delivery to another session");
532
+ got(await store.append({
533
+ sessionId: id("sender"), expectedSeq: 0, entries: [said("go")],
534
+ enqueue: [{ sessionId: id("target"), input: "do the thing" }],
535
+ }), "append with a delivery");
536
+ equals((await wake).runnable, true, "the recipient now has work owed to it");
537
+ });
538
+ if (subject.changes === "deployment") {
539
+ cases.push({
540
+ name: "a change on one connection wakes a watcher on another",
541
+ async run() {
542
+ // The reach a database store exists for. One that only hears itself is
543
+ // a real feed and a useful one, but a worker in another process is then
544
+ // carried entirely by its heartbeat — which is the difference between a
545
+ // message arriving in milliseconds and in half a minute.
546
+ holds(subject.peer !== undefined, "a feed claiming deployment reach owes a peer to prove it");
547
+ const store = await subject.open({ claimMs: CLAIM_MS });
548
+ const other = await subject.peer({ claimMs: CLAIM_MS });
549
+ try {
550
+ await created(store, id("s"));
551
+ const wake = woken(other, (change) => change.sessionId === id("s"), "a write on another connection");
552
+ // Registering `LISTEN` is itself a round trip; the heartbeat is what
553
+ // covers a change that lands before it completes.
554
+ await wait(400);
555
+ got(await store.append({
556
+ sessionId: id("s"), expectedSeq: 0, entries: [], enqueue: [{ sessionId: id("s"), input: "wake up" }],
557
+ }), "a delivery from the other connection");
558
+ equals((await wake).runnable, true, "the far watcher hears that work is owed");
559
+ }
560
+ finally {
561
+ await other.close();
562
+ await store.close();
563
+ await subject.release?.(store);
564
+ }
565
+ },
566
+ });
567
+ }
568
+ define("unsubscribing stops the wakes", async (store) => {
569
+ await created(store, id("s"));
570
+ let seen = 0;
571
+ const off = store.watch(() => { seen += 1; });
572
+ off();
573
+ off(); // idempotent: releasing twice is what releasing asked for
574
+ got(await store.append({ sessionId: id("s"), expectedSeq: 0, entries: [said("one")] }), "append");
575
+ await wait(200);
576
+ equals(seen, 0, "a watcher that let go is not called again");
577
+ });
578
+ return cases;
579
+ }
580
+ //# sourceMappingURL=conformance.js.map