@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.
- package/package.json +3 -3
- package/src/authority.ts +234 -10
- package/src/conversations.test.ts +159 -0
- package/src/conversations.ts +126 -0
- package/src/index.ts +1 -0
- package/src/run-records.ts +9 -1
- package/src/run-recovery.test.ts +120 -12
- package/src/run-recovery.ts +116 -2
- package/src/run-terminal-open-turn.test.ts +203 -0
- package/src/run-terminal.ts +58 -8
- package/src/storage-keys.ts +15 -0
- package/src/turn-admission.test.ts +38 -0
- package/src/turn-supersede.test.ts +85 -1
|
@@ -3,7 +3,10 @@ import {
|
|
|
3
3
|
bootstrapGeneration,
|
|
4
4
|
type CompositionGenerationV1,
|
|
5
5
|
} from "@frockbot/kernel-composition/generation";
|
|
6
|
-
import
|
|
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", () => {
|