@frockbot/kernel-do 0.3.15 → 0.3.17

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.
@@ -0,0 +1,250 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ bootstrapGeneration,
4
+ type CompositionGenerationV1,
5
+ } from "@frockbot/kernel-composition/generation";
6
+ import { Session, type SessionEvent } from "@frockbot/kernel-contracts";
7
+ import {
8
+ BotDurableAuthority,
9
+ type BotDurableAuthorityHooks,
10
+ } from "./authority.ts";
11
+ import { MemoryStorage } from "./memory-storage.fixture.ts";
12
+ import { createStoredRunCodecV1, type StoredRunV1 } from "./run-records.ts";
13
+
14
+ const SQLITE_VALUE_LIMIT_BYTES = 2 * 1024 * 1024;
15
+ const LARGE_REQUEST_BYTES = 80_000;
16
+ const STEPS = 30;
17
+ const SESSION_ID = "user-1:primary";
18
+
19
+ const codec = createStoredRunCodecV1<undefined>({
20
+ decodeRunId: (value) => String(value),
21
+ decodeConfigurationSnapshot: () => undefined,
22
+ });
23
+
24
+ /** A storage double that enforces Durable Object SQLite's per-value ceiling. */
25
+ class SqliteLimitedMemoryStorage extends MemoryStorage {
26
+ readonly rejectedKeys: string[] = [];
27
+
28
+ override put(
29
+ key: string | Record<string, unknown>,
30
+ value?: unknown,
31
+ ): Promise<void> {
32
+ const entries =
33
+ typeof key === "string" ? [[key, value] as const] : Object.entries(key);
34
+ for (const [entryKey, entry] of entries) {
35
+ const bytes = new TextEncoder().encode(JSON.stringify(entry)).byteLength;
36
+ if (bytes > SQLITE_VALUE_LIMIT_BYTES) {
37
+ this.rejectedKeys.push(entryKey);
38
+ throw new Error("string or blob too big: SQLITE_TOOBIG");
39
+ }
40
+ }
41
+ return super.put(key, value);
42
+ }
43
+
44
+ override async transaction<T>(
45
+ callback: (storage: MemoryStorage) => Promise<T>,
46
+ ): Promise<T> {
47
+ const before = structuredClone([...this.values.entries()]);
48
+ try {
49
+ return await callback(this);
50
+ } catch (error) {
51
+ this.values.clear();
52
+ for (const [key, value] of before) this.values.set(key, value);
53
+ throw error;
54
+ }
55
+ }
56
+ }
57
+
58
+ function bootstrap(): Promise<CompositionGenerationV1> {
59
+ return bootstrapGeneration(
60
+ [
61
+ {
62
+ packageId: "shell",
63
+ specifier: "@frockbot/plugin-shell",
64
+ version: "0.0.1",
65
+ manifest: { id: "shell", version: "0.0.1" },
66
+ },
67
+ ],
68
+ { createdAt: "2026-09-04T00:00:00.000Z" },
69
+ );
70
+ }
71
+
72
+ function legacyEvents(): SessionEvent[] {
73
+ const session = new Session(SESSION_ID, () => {});
74
+ session.appendBatch([
75
+ { type: "turn/start", turn: 1 },
76
+ { type: "step/start", turn: 1, step: 1 },
77
+ {
78
+ type: "user/message",
79
+ turn: 1,
80
+ step: 1,
81
+ messageId: "legacy-message",
82
+ text: "Earlier work",
83
+ },
84
+ {
85
+ type: "model/request",
86
+ turn: 1,
87
+ step: 1,
88
+ request: {
89
+ requestId: "legacy-request",
90
+ provider: "fake",
91
+ model: "large-context",
92
+ system: "p".repeat(1_900_000),
93
+ messages: [{ role: "user", content: "Earlier work" }],
94
+ tools: [],
95
+ },
96
+ },
97
+ {
98
+ type: "assistant/message",
99
+ turn: 1,
100
+ step: 1,
101
+ requestId: "legacy-request",
102
+ text: "Done earlier.",
103
+ toolCalls: [],
104
+ },
105
+ { type: "step/end", turn: 1, step: 1, outcome: "completed" },
106
+ { type: "turn/end", turn: 1, outcome: "completed" },
107
+ ]);
108
+ return [...session.events];
109
+ }
110
+
111
+ function legacyRun(events: SessionEvent[]): StoredRunV1<undefined> {
112
+ return {
113
+ runId: "legacy-run",
114
+ commandFingerprint: "legacy-fingerprint",
115
+ sessionId: SESSION_ID,
116
+ acceptedAt: "2026-09-04T00:00:00.000Z",
117
+ input: "Earlier work",
118
+ events,
119
+ effectAdmissions: [],
120
+ status: "completed",
121
+ responseText: "Done earlier.",
122
+ phase: "executing",
123
+ compositionGenerationId: "legacy-generation",
124
+ configurationSnapshot: undefined,
125
+ previousEventCount: 0,
126
+ };
127
+ }
128
+
129
+ function createAuthority(
130
+ storage: SqliteLimitedMemoryStorage,
131
+ ): BotDurableAuthority<undefined> {
132
+ const hooks: BotDurableAuthorityHooks<undefined> = {
133
+ resolveAdmissionSnapshot: () => Promise.resolve(undefined),
134
+ bootstrapComposition: () => bootstrap(),
135
+ admittedSnapshot: () => Promise.resolve(undefined),
136
+ executeTurn: async (input) => {
137
+ let seq = input.previousEvents.length;
138
+ const events: SessionEvent[] = [];
139
+ const persist = async (
140
+ batch: Array<Omit<SessionEvent, "seq" | "timestamp">>,
141
+ ) => {
142
+ const stamped = batch.map(
143
+ (event) =>
144
+ ({
145
+ ...event,
146
+ seq: seq++,
147
+ timestamp: "2026-09-04T00:01:00.000Z",
148
+ }) as SessionEvent,
149
+ );
150
+ events.push(...stamped);
151
+ await input.persistSessionEvents(input.command.sessionId, stamped);
152
+ };
153
+
154
+ await persist([{ type: "turn/start", turn: 2 } as never]);
155
+ for (let step = 1; step <= STEPS; step += 1) {
156
+ await persist([
157
+ { type: "step/start", turn: 2, step } as never,
158
+ ...(step === 1
159
+ ? [
160
+ {
161
+ type: "user/message",
162
+ turn: 2,
163
+ step,
164
+ messageId: "message-2",
165
+ text: input.command.text,
166
+ } as never,
167
+ ]
168
+ : []),
169
+ {
170
+ type: "model/request",
171
+ turn: 2,
172
+ step,
173
+ request: {
174
+ requestId: `request-${step}`,
175
+ provider: "fake",
176
+ model: "large-context",
177
+ system: "s".repeat(LARGE_REQUEST_BYTES),
178
+ messages: [{ role: "user", content: input.command.text }],
179
+ tools: [],
180
+ },
181
+ } as never,
182
+ {
183
+ type: "assistant/message",
184
+ turn: 2,
185
+ step,
186
+ requestId: `request-${step}`,
187
+ text: step === STEPS ? "All done." : "",
188
+ toolCalls: [],
189
+ } as never,
190
+ { type: "step/end", turn: 2, step, outcome: "completed" } as never,
191
+ ]);
192
+ }
193
+ await persist([
194
+ { type: "turn/end", turn: 2, outcome: "completed" } as never,
195
+ ]);
196
+ return { runId: input.command.runId, text: "All done.", events };
197
+ },
198
+ notification: () => undefined,
199
+ scheduledDeadlines: () => Promise.resolve([]),
200
+ scheduledWorkInFlight: () => false,
201
+ deferScheduledWork: () => Promise.resolve(),
202
+ settleScheduledWork: () => Promise.resolve(),
203
+ };
204
+ return new BotDurableAuthority<undefined>({
205
+ state: { storage } as unknown as DurableObjectState,
206
+ codec,
207
+ hooks,
208
+ });
209
+ }
210
+
211
+ describe("a long Turn on a legacy near-limit Session", () => {
212
+ test("migrates to bounded values and completes thirty large model steps", async () => {
213
+ const storage = new SqliteLimitedMemoryStorage();
214
+ const previous = legacyEvents();
215
+ const legacyBytes = new TextEncoder().encode(
216
+ JSON.stringify(previous),
217
+ ).byteLength;
218
+ expect(legacyBytes).toBeGreaterThan(1_900_000);
219
+ expect(legacyBytes).toBeLessThan(SQLITE_VALUE_LIMIT_BYTES);
220
+ // Seed through the backing map because these are values an older deploy
221
+ // already wrote. Every write made by the code under test is size-checked.
222
+ storage.values.set("latest-events", structuredClone(previous));
223
+ storage.values.set("run:legacy-run", structuredClone(legacyRun(previous)));
224
+
225
+ const completion = await createAuthority(storage).run({
226
+ userId: "user-1",
227
+ botId: "primary",
228
+ runId: "large-run",
229
+ sessionId: SESSION_ID,
230
+ acceptedAt: "2026-09-04T00:01:00.000Z",
231
+ text: "Keep going",
232
+ });
233
+
234
+ const stored = codec.require(storage.values.get("run:large-run"));
235
+ expect(storage.rejectedKeys).not.toContain("run:large-run");
236
+ expect(stored.failure ?? "").not.toContain("SQLITE_TOOBIG");
237
+ expect(completion.text).toBe("All done.");
238
+ expect(
239
+ completion.events.filter((event) => event.type === "model/request"),
240
+ ).toHaveLength(STEPS);
241
+ expect(storage.values.has("latest-events")).toBe(false);
242
+ expect(
243
+ [...storage.values.values()].every(
244
+ (value) =>
245
+ new TextEncoder().encode(JSON.stringify(value)).byteLength <=
246
+ SQLITE_VALUE_LIMIT_BYTES,
247
+ ),
248
+ ).toBe(true);
249
+ });
250
+ });
@@ -16,7 +16,15 @@ export const ACTIVE_RUN_KEY = "active-run";
16
16
  * working through a backlog of things the User has already replaced.
17
17
  */
18
18
  export const PENDING_RUN_KEY = "pending-run";
19
+ /** Agent-lane Turns wait FIFO behind conversational work. */
20
+ export const PENDING_AGENT_RUN_PREFIX = "pending-agent-run:";
21
+ /** A Bot cannot accumulate an unbounded cross-Bot inbox. */
22
+ export const MAX_PENDING_AGENT_RUNS_V1 = 32;
23
+ /** Legacy pre-ADR-0033 Session value, read only for transparent migration. */
19
24
  export const LATEST_EVENTS_KEY = "latest-events";
25
+ export const SESSION_EVENT_LOG_INDEX_PREFIX = "session-events:index:";
26
+ export const SESSION_EVENT_LOG_PAGE_PREFIX = "session-events:page:";
27
+ export const SESSION_EVENT_PAYLOAD_PREFIX = "session-events:payload:";
20
28
  export const IDENTITY_KEY = "identity";
21
29
  export const NOTIFICATION_PREFIX = "notification:";
22
30
  export const COMPOSITION_CURRENT_KEY = "composition:current";
@@ -80,6 +88,10 @@ export function runIndexKey(acceptedAt: string, runId: string): string {
80
88
  return `${RUN_INDEX_PREFIX}${acceptedAt}:${runId}`;
81
89
  }
82
90
 
91
+ export function pendingAgentRunKey(acceptedAt: string, runId: string): string {
92
+ return `${PENDING_AGENT_RUN_PREFIX}${acceptedAt}:${runId}`;
93
+ }
94
+
83
95
  export function compositionGenerationKey(generationId: string): string {
84
96
  return `${COMPOSITION_GENERATION_PREFIX}${generationId}`;
85
97
  }
@@ -10,6 +10,7 @@ import {
10
10
  type BotTurnExecutionInput,
11
11
  } from "./authority.ts";
12
12
  import { MemoryStorage } from "./memory-storage.fixture.ts";
13
+ import { SessionEventLog } from "./session-event-log.ts";
13
14
  import {
14
15
  botTurnCommandFingerprintV1,
15
16
  createStoredRunCodecV1,
@@ -36,6 +37,13 @@ const SUBAGENT_ORIGIN: StoredRunOriginV1 = {
36
37
  parentRunId: "run-parent",
37
38
  };
38
39
 
40
+ const BOT_ORIGIN: StoredRunOriginV1 = {
41
+ kind: "bot",
42
+ fromBotId: "researcher",
43
+ fromBotName: "Researcher",
44
+ messageId: "agent-message-1",
45
+ };
46
+
39
47
  const codec = createStoredRunCodecV1<undefined>({
40
48
  decodeRunId: (value) => value as string,
41
49
  decodeConfigurationSnapshot: () => undefined,
@@ -157,6 +165,22 @@ describe("the admission record names what produced the Turn", () => {
157
165
  expect(codec.require(structuredClone(decoded))).toEqual(decoded);
158
166
  });
159
167
 
168
+ test("round-trips an agent Turn and its sending Bot", () => {
169
+ const decoded = codec.require(
170
+ legacyRun({
171
+ admission: {
172
+ schemaVersion: 1,
173
+ turnType: "agent",
174
+ origin: BOT_ORIGIN,
175
+ },
176
+ }),
177
+ );
178
+
179
+ expect(decoded.admission?.origin).toEqual(BOT_ORIGIN);
180
+ expect(storedRunTurnTypeV1(decoded)).toBe("agent");
181
+ expect(codec.require(structuredClone(decoded))).toEqual(decoded);
182
+ });
183
+
160
184
  test("each origin kind has its own exact fields, and cannot borrow another's", () => {
161
185
  const withOrigin = (origin: unknown) =>
162
186
  legacyRun({
@@ -359,9 +383,14 @@ function command(runId: string, turnType?: TurnTypeV1) {
359
383
  };
360
384
  }
361
385
 
362
- function admittedEvent(storage: MemoryStorage, runId: string) {
386
+ async function admittedEvent(storage: MemoryStorage, runId: string) {
363
387
  const run = storage.values.get(`run:${runId}`) as StoredRunV1<undefined>;
364
- return run.events.find((event) => event.type === "turn/admission");
388
+ const events = await new SessionEventLog(storage).readRange(
389
+ run.sessionId,
390
+ run.eventRange!.startSeq,
391
+ run.eventRange!.endSeq,
392
+ );
393
+ return events.find((event) => event.type === "turn/admission");
365
394
  }
366
395
 
367
396
  describe("an admitted Turn re-mounts on its recorded turn type", () => {
@@ -389,11 +418,27 @@ describe("an admitted Turn re-mounts on its recorded turn type", () => {
389
418
  turnType: "automation",
390
419
  });
391
420
  expect(probe.observed[0]?.command.turnType).toBe("automation");
392
- expect(admittedEvent(storage, "run-1")).toMatchObject({
421
+ expect(await admittedEvent(storage, "run-1")).toMatchObject({
393
422
  turnType: "automation",
394
423
  });
395
424
  });
396
425
 
426
+ test("an agent Turn defaults to the agent admission lane", async () => {
427
+ const storage = new MemoryStorage();
428
+ const probe = createAuthority(storage);
429
+
430
+ await probe.authority.run(command("run-agent", "agent"));
431
+
432
+ const stored = storage.values.get(
433
+ "run:run-agent",
434
+ ) as StoredRunV1<undefined>;
435
+ expect(stored.admission).toEqual({
436
+ schemaVersion: 1,
437
+ turnType: "agent",
438
+ });
439
+ expect(probe.observed[0]?.command.turnType).toBe("agent");
440
+ });
441
+
397
442
  test("after eviction the resumed run re-mounts on the recorded type", async () => {
398
443
  const storage = new MemoryStorage();
399
444
  const probe = createAuthority(storage);
@@ -407,17 +452,17 @@ describe("an admitted Turn re-mounts on its recorded turn type", () => {
407
452
  status: "running",
408
453
  phase: "executing",
409
454
  responseText: undefined,
410
- events: [],
455
+ eventRange: { startSeq: 0, endSeq: 0 },
411
456
  });
412
457
  storage.values.set("active-run", "run-1");
413
458
  storage.values.set("identity", { userId: "user-1", botId: "primary" });
414
- storage.values.set("latest-events", []);
459
+ await new SessionEventLog(storage).rewrite("user-1:primary", []);
415
460
 
416
461
  const resumed = createAuthority(storage);
417
462
  await resumed.authority.recoverActiveRun();
418
463
 
419
464
  expect(resumed.observed.at(-1)?.command.turnType).toBe("automation");
420
- expect(admittedEvent(storage, "run-1")).toMatchObject({
465
+ expect(await admittedEvent(storage, "run-1")).toMatchObject({
421
466
  turnType: "automation",
422
467
  });
423
468
  });
@@ -433,17 +478,17 @@ describe("an admitted Turn re-mounts on its recorded turn type", () => {
433
478
  status: "running",
434
479
  phase: "executing",
435
480
  responseText: undefined,
436
- events: [],
481
+ eventRange: { startSeq: 0, endSeq: 0 },
437
482
  });
438
483
  storage.values.set("active-run", "run-1");
439
484
  storage.values.set("identity", { userId: "user-1", botId: "primary" });
440
- storage.values.set("latest-events", []);
485
+ await new SessionEventLog(storage).rewrite("user-1:primary", []);
441
486
 
442
487
  const resumed = createAuthority(storage);
443
488
  await resumed.authority.recoverActiveRun();
444
489
 
445
490
  expect(resumed.observed.at(-1)?.command.turnType).toBe("chat");
446
- expect(admittedEvent(storage, "run-1")).toMatchObject({
491
+ expect(await admittedEvent(storage, "run-1")).toMatchObject({
447
492
  turnType: "chat",
448
493
  });
449
494
  });
@@ -15,6 +15,7 @@ import {
15
15
  type OwnedBotTurnCommand,
16
16
  } from "./authority.ts";
17
17
  import { MemoryStorage } from "./memory-storage.fixture.ts";
18
+ import { SessionEventLog } from "./session-event-log.ts";
18
19
  import {
19
20
  BotTurnReconciliationRequiredError,
20
21
  BotTurnRecoveryRequiredError,
@@ -24,6 +25,10 @@ import {
24
25
  storedRunLaneV1,
25
26
  type StoredRunV1,
26
27
  } from "./run-records.ts";
28
+ import {
29
+ MAX_PENDING_AGENT_RUNS_V1,
30
+ pendingAgentRunKey,
31
+ } from "./storage-keys.ts";
27
32
 
28
33
  const codec = createStoredRunCodecV1<undefined>({
29
34
  decodeRunId: (value) => value as string,
@@ -92,8 +97,13 @@ interface Deferred<T> {
92
97
  * requests never reach a Durable Object in the same microtask, and the tests
93
98
  * that send two messages are describing two requests.
94
99
  */
95
- function admitted(): Promise<void> {
96
- return new Promise((resolve) => setTimeout(resolve, 0));
100
+ async function admitted(): Promise<void> {
101
+ // Admission now also hashes any legacy event payloads it migrates. Let the
102
+ // Web Crypto promises and the Durable Object transaction both drain before
103
+ // inspecting the durable queue.
104
+ for (let turn = 0; turn < 8; turn += 1) {
105
+ await new Promise((resolve) => setTimeout(resolve, 0));
106
+ }
97
107
  }
98
108
 
99
109
  function deferred<T>(): Deferred<T> {
@@ -357,7 +367,9 @@ describe("a user message supersedes the running Turn", () => {
357
367
  probe.handle("run-2").finish();
358
368
  const result = await second;
359
369
 
360
- const superseded = storedRun(storage, "run-1");
370
+ const superseded = await probe.authority.readRun("run-1");
371
+ expect(superseded).toBeDefined();
372
+ if (superseded === undefined) throw new Error("run-1 was not stored");
361
373
  expect(superseded.status).toBe("superseded");
362
374
  expect(superseded.supersededBy).toBe("run-2");
363
375
  expect(turnEndOf(superseded)).toMatchObject({
@@ -604,6 +616,150 @@ describe("a background admission never supersedes", () => {
604
616
  });
605
617
  });
606
618
 
619
+ describe("the agent lane", () => {
620
+ test("queues behind the active Turn without superseding it", async () => {
621
+ const storage = new MemoryStorage();
622
+ const probe = createAuthority(storage);
623
+ const first = probe.authority.run(command("run-1", "person"));
624
+ await probe.handle("run-1").started;
625
+
626
+ const agent = probe.authority.run(
627
+ command("run-agent", "question", {
628
+ turnType: "agent",
629
+ origin: {
630
+ kind: "bot",
631
+ fromBotId: "researcher",
632
+ fromBotName: "Researcher",
633
+ messageId: "message-1",
634
+ },
635
+ }),
636
+ );
637
+ await admitted();
638
+ expect(probe.interrupts).toEqual([]);
639
+ expect(storedRun(storage, "run-agent")).toMatchObject({
640
+ phase: "queued",
641
+ admission: { turnType: "agent" },
642
+ });
643
+ expect(storedRunLaneV1(storedRun(storage, "run-agent"))).toBe("agent");
644
+
645
+ probe.handle("run-1").finish();
646
+ await first;
647
+ await probe.handle("run-agent").started;
648
+ probe.handle("run-agent").finish();
649
+ expect(await agent).toMatchObject({ text: "done: question" });
650
+ });
651
+
652
+ test("runs queued agent Turns FIFO", async () => {
653
+ const storage = new MemoryStorage();
654
+ const probe = createAuthority(storage);
655
+ const active = probe.authority.run(command("run-1", "person"));
656
+ await probe.handle("run-1").started;
657
+
658
+ const firstAgent = probe.authority.run(
659
+ command("run-agent-1", "first agent", { turnType: "agent" }),
660
+ );
661
+ await admitted();
662
+ const secondAgent = probe.authority.run(
663
+ command("run-agent-2", "second agent", { turnType: "agent" }),
664
+ );
665
+ await admitted();
666
+
667
+ probe.handle("run-1").finish();
668
+ await active;
669
+ await probe.handle("run-agent-1").started;
670
+ expect(probe.observed.map(({ command }) => command.runId)).toEqual([
671
+ "run-1",
672
+ "run-agent-1",
673
+ ]);
674
+ probe.handle("run-agent-1").finish();
675
+ await firstAgent;
676
+
677
+ await probe.handle("run-agent-2").started;
678
+ probe.handle("run-agent-2").finish();
679
+ await secondAgent;
680
+ expect(probe.observed.map(({ command }) => command.runId)).toEqual([
681
+ "run-1",
682
+ "run-agent-1",
683
+ "run-agent-2",
684
+ ]);
685
+ });
686
+
687
+ test("gives a queued User Turn priority over queued agent work", async () => {
688
+ const storage = new MemoryStorage();
689
+ const probe = createAuthority(storage);
690
+ const active = probe.authority.run(command("run-1", "person"));
691
+ await probe.handle("run-1").started;
692
+ const agent = probe.authority.run(
693
+ command("run-agent", "agent", { turnType: "agent" }),
694
+ );
695
+ await admitted();
696
+
697
+ const user = probe.authority.run(
698
+ command("run-user", "next message", {
699
+ lane: "user",
700
+ supersedes: { runId: "run-1" },
701
+ }),
702
+ );
703
+ await active;
704
+ await probe.handle("run-user").started;
705
+ expect(probe.observed.map(({ command }) => command.runId)).toEqual([
706
+ "run-1",
707
+ "run-user",
708
+ ]);
709
+ probe.handle("run-user").finish();
710
+ await user;
711
+
712
+ await probe.handle("run-agent").started;
713
+ probe.handle("run-agent").finish();
714
+ await agent;
715
+ expect(probe.observed.map(({ command }) => command.runId)).toEqual([
716
+ "run-1",
717
+ "run-user",
718
+ "run-agent",
719
+ ]);
720
+ });
721
+
722
+ test("refuses agent admission while the active Turn awaits reconciliation", async () => {
723
+ const storage = new MemoryStorage();
724
+ const probe = createAuthority(storage, { parkOnRelease: () => true });
725
+ const active = probe.authority.run(command("run-1", "person"));
726
+ await probe.handle("run-1").started;
727
+ probe.handle("run-1").finish();
728
+ await active.catch(() => undefined);
729
+
730
+ await expect(
731
+ probe.authority.run(command("run-agent", "agent", { turnType: "agent" })),
732
+ ).rejects.toThrow(/cannot admit agent work.*requires reconciliation/);
733
+ expect(storage.values.has("run:run-agent")).toBe(false);
734
+ });
735
+
736
+ test("refuses admission when the Bot's bounded agent queue is full", async () => {
737
+ const storage = new MemoryStorage();
738
+ const probe = createAuthority(storage);
739
+ const first = probe.authority.run(command("run-1", "person"));
740
+ await probe.handle("run-1").started;
741
+ for (let index = 0; index < MAX_PENDING_AGENT_RUNS_V1; index += 1) {
742
+ const runId = `queued-${index}`;
743
+ storage.values.set(
744
+ pendingAgentRunKey(
745
+ new Date(Date.UTC(2026, 8, 3, 1, 0, index)).toISOString(),
746
+ runId,
747
+ ),
748
+ runId,
749
+ );
750
+ }
751
+
752
+ await expect(
753
+ probe.authority.run(
754
+ command("run-past-bound", "question", { turnType: "agent" }),
755
+ ),
756
+ ).rejects.toThrow(/agent queue is full \(32 Turns\)/);
757
+
758
+ probe.handle("run-1").finish();
759
+ await first;
760
+ });
761
+ });
762
+
607
763
  describe("eviction between the two Turns", () => {
608
764
  /**
609
765
  * Exactly what the object holds at the moment between the superseded Turn
@@ -681,10 +837,7 @@ describe("a durable log left inside a Turn", () => {
681
837
  probe.handle("run-1").finish();
682
838
  await run;
683
839
 
684
- const events = storage.values.get("latest-events") as Array<{
685
- type: string;
686
- turn?: number;
687
- }>;
840
+ const events = await new SessionEventLog(storage).read("user-1:primary");
688
841
  // The orphaned Turn is closed, so the new one starts.
689
842
  expect(events[2]).toMatchObject({
690
843
  type: "turn/end",
@@ -738,7 +891,7 @@ describe("a durable log left inside a Turn", () => {
738
891
  probe.handle("run-1").finish();
739
892
  await run;
740
893
 
741
- const events = storage.values.get("latest-events") as SessionEvent[];
894
+ const events = await new SessionEventLog(storage).read("user-1:primary");
742
895
  // Turn 1 is closed where it was abandoned, not after the Turns that
743
896
  // followed it, and the log is resequenced around the insertion.
744
897
  expect(