@frockbot/kernel-do 0.3.5 → 0.3.7

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.
@@ -3,7 +3,10 @@ import {
3
3
  bootstrapGeneration,
4
4
  type CompositionGenerationV1,
5
5
  } from "@frockbot/kernel-composition/generation";
6
- import type { SessionEvent } from "@frockbot/kernel-contracts";
6
+ import {
7
+ type SessionEvent,
8
+ validateToolOccurrenceJournal,
9
+ } from "@frockbot/kernel-contracts";
7
10
  import {
8
11
  BotDurableAuthority,
9
12
  SUPERSEDED_TURN_REASON_V1,
@@ -690,6 +693,87 @@ describe("a durable log left inside a Turn", () => {
690
693
  });
691
694
  expect(storedRun(storage, "run-1").status).toBe("completed");
692
695
  });
696
+
697
+ test("is repaired even once refused Turns have been logged behind it", async () => {
698
+ const storage = new MemoryStorage();
699
+ // What production actually holds on a Bot wedged before the repair
700
+ // existed. The Agent loop journals `turn/start` durably and only then
701
+ // assembles the request that discovers turn 1 is still open, so every
702
+ // refused message left a *complete* Turn of its own behind the abandoned
703
+ // one — and the log stopped ending inside a Turn. The trailing-open test
704
+ // then said there was nothing to repair, so admission repaired nothing and
705
+ // the next message failed exactly the same way, forever.
706
+ storage.values.set("latest-events", [
707
+ {
708
+ type: "session/created",
709
+ createdAt: "2026-09-03T00:00:00.000Z",
710
+ seq: 0,
711
+ timestamp: "2026-09-03T00:00:00.000Z",
712
+ },
713
+ {
714
+ type: "turn/start",
715
+ turn: 1,
716
+ seq: 1,
717
+ timestamp: "2026-09-03T00:00:01.000Z",
718
+ },
719
+ {
720
+ type: "turn/start",
721
+ turn: 2,
722
+ seq: 2,
723
+ timestamp: "2026-09-03T00:00:02.000Z",
724
+ },
725
+ {
726
+ type: "turn/end",
727
+ turn: 2,
728
+ outcome: "model-error",
729
+ reason: "turn 2 started while turn 1 is open",
730
+ seq: 3,
731
+ timestamp: "2026-09-03T00:00:03.000Z",
732
+ },
733
+ ]);
734
+ const probe = createAuthority(storage);
735
+
736
+ const run = probe.authority.run(command("run-1", "hello"));
737
+ await probe.handle("run-1").started;
738
+ probe.handle("run-1").finish();
739
+ await run;
740
+
741
+ const events = storage.values.get("latest-events") as SessionEvent[];
742
+ // Turn 1 is closed where it was abandoned, not after the Turns that
743
+ // followed it, and the log is resequenced around the insertion.
744
+ expect(
745
+ events
746
+ .slice(0, 5)
747
+ .map((event) =>
748
+ event.type === "turn/start" || event.type === "turn/end"
749
+ ? `${event.type}:${event.turn}`
750
+ : event.type,
751
+ ),
752
+ ).toEqual([
753
+ "session/created",
754
+ "turn/start:1",
755
+ "turn/end:1",
756
+ "turn/start:2",
757
+ "turn/end:2",
758
+ ]);
759
+ expect(events[2]).toMatchObject({
760
+ type: "turn/end",
761
+ turn: 1,
762
+ outcome: "interrupted",
763
+ });
764
+ expect(events.map((event) => event.seq)).toEqual(
765
+ events.map((_event, index) => index),
766
+ );
767
+ // The repaired history is one the invariant accepts, which is what the
768
+ // refused Turns were failing on. (The stub Agent below numbers its own
769
+ // Turn rather than reading `nextTurn`, so only the repaired prefix is the
770
+ // subject here.)
771
+ expect(() =>
772
+ validateToolOccurrenceJournal(events.slice(0, 5)),
773
+ ).not.toThrow();
774
+ expect(storedRun(storage, "run-1").status).toBe("completed");
775
+ expect(storedRun(storage, "run-1").previousEventCount).toBe(5);
776
+ });
693
777
  });
694
778
 
695
779
  describe("a failing recovery of an older Turn", () => {