@frockbot/kernel-do 0.3.2 → 0.3.4
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 +468 -13
- package/src/memory-storage.fixture.ts +14 -1
- package/src/run-records.ts +132 -4
- package/src/run-terminal.ts +109 -2
- package/src/storage-keys.ts +8 -0
- package/src/turn-supersede.test.ts +578 -0
|
@@ -45,8 +45,21 @@ export class MemoryStorage {
|
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Transactions run one at a time, as a Durable Object's do. Two admissions
|
|
50
|
+
* that arrive together must not both read "no run is pending" and both
|
|
51
|
+
* write themselves into the slot, and a fixture that let them would prove
|
|
52
|
+
* the opposite of what the tests are for.
|
|
53
|
+
*/
|
|
54
|
+
#serialized: Promise<unknown> = Promise.resolve();
|
|
55
|
+
|
|
48
56
|
transaction<T>(callback: (storage: MemoryStorage) => Promise<T>): Promise<T> {
|
|
49
|
-
|
|
57
|
+
const next = this.#serialized.then(
|
|
58
|
+
() => callback(this),
|
|
59
|
+
() => callback(this),
|
|
60
|
+
);
|
|
61
|
+
this.#serialized = next.catch(() => undefined);
|
|
62
|
+
return next;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
setAlarm(scheduledTime: number): Promise<void> {
|
package/src/run-records.ts
CHANGED
|
@@ -22,7 +22,34 @@ export interface StoredRunCodecV1<Snapshot> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export type StoredRunStatus =
|
|
25
|
-
|
|
25
|
+
| "running"
|
|
26
|
+
| "completed"
|
|
27
|
+
| "failed"
|
|
28
|
+
| "cancelled"
|
|
29
|
+
| "superseded"
|
|
30
|
+
| "reconciliation-required";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The admission lane a Turn was accepted on.
|
|
34
|
+
*
|
|
35
|
+
* A `user` admission is a person speaking to the Bot and may supersede
|
|
36
|
+
* whatever is running; a `background` admission — a Routine firing, a subagent
|
|
37
|
+
* dispatch — never supersedes and waits. The lane is durable because the
|
|
38
|
+
* decision to interrupt is made in the admission transaction and has to
|
|
39
|
+
* survive eviction alongside the run it interrupted.
|
|
40
|
+
*/
|
|
41
|
+
export type RunLaneV1 = "user" | "background";
|
|
42
|
+
|
|
43
|
+
const RUN_LANES_V1: readonly RunLaneV1[] = ["user", "background"];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The lane a turn type belongs to when its record names none. Chat is the
|
|
47
|
+
* conversation, so it is the User's lane; every other turn type is work the
|
|
48
|
+
* Bot started for itself.
|
|
49
|
+
*/
|
|
50
|
+
export function defaultRunLaneV1(turnType: TurnTypeV1): RunLaneV1 {
|
|
51
|
+
return turnType === "chat" ? "user" : "background";
|
|
52
|
+
}
|
|
26
53
|
|
|
27
54
|
export type StoredEffectAdmissionOutcome = "admitted" | "fenced";
|
|
28
55
|
|
|
@@ -80,6 +107,13 @@ const STORED_RUN_ORIGIN_TRIGGERS: readonly StoredRunTriggerV1[] = [
|
|
|
80
107
|
export interface StoredRunAdmissionV1 {
|
|
81
108
|
schemaVersion: 1;
|
|
82
109
|
turnType: TurnTypeV1;
|
|
110
|
+
/**
|
|
111
|
+
* The lane this Turn was admitted on. Absent means the lane its turn type
|
|
112
|
+
* defaults to, so no producer writing today's lanes changes a stored byte;
|
|
113
|
+
* a later lane that is not a turn type's default — bot-to-bot messaging's
|
|
114
|
+
* `agent` lane — names itself here.
|
|
115
|
+
*/
|
|
116
|
+
lane?: RunLaneV1;
|
|
83
117
|
/**
|
|
84
118
|
* The subagent role the Turn was admitted under, when it had one. Recorded
|
|
85
119
|
* for the same reason the turn type is: recovery after eviction has to
|
|
@@ -90,7 +124,7 @@ export interface StoredRunAdmissionV1 {
|
|
|
90
124
|
}
|
|
91
125
|
|
|
92
126
|
export type StoredRunPhase =
|
|
93
|
-
"admitted" | "executing" | "reconciliation-required";
|
|
127
|
+
"queued" | "admitted" | "executing" | "reconciliation-required";
|
|
94
128
|
|
|
95
129
|
export interface StoredRunV1<Snapshot = unknown> {
|
|
96
130
|
runId: string;
|
|
@@ -106,6 +140,15 @@ export interface StoredRunV1<Snapshot = unknown> {
|
|
|
106
140
|
phase: StoredRunPhase;
|
|
107
141
|
/** Durable Stop intent; orthogonal to status and phase. */
|
|
108
142
|
stopRequestedAt?: string;
|
|
143
|
+
/**
|
|
144
|
+
* Durable supersede intent: a later user-lane admission has taken this
|
|
145
|
+
* Turn's place. Orthogonal to status and phase exactly as Stop is, and read
|
|
146
|
+
* the same way — every new effect is fenced from the instant it is
|
|
147
|
+
* recorded, and the settlement that follows is terminal `superseded`.
|
|
148
|
+
*/
|
|
149
|
+
supersededAt?: string;
|
|
150
|
+
/** The run whose admission superseded this one. */
|
|
151
|
+
supersededBy?: string;
|
|
109
152
|
/** The Composition generation pinned in the same transaction that admitted the run. */
|
|
110
153
|
compositionGenerationId: string;
|
|
111
154
|
configurationSnapshot: Snapshot;
|
|
@@ -137,6 +180,15 @@ export function storedRunTurnTypeV1(run: {
|
|
|
137
180
|
return run.admission?.turnType ?? "chat";
|
|
138
181
|
}
|
|
139
182
|
|
|
183
|
+
/** The lane a stored run was admitted on. */
|
|
184
|
+
export function storedRunLaneV1(run: {
|
|
185
|
+
admission?: StoredRunAdmissionV1;
|
|
186
|
+
}): RunLaneV1 {
|
|
187
|
+
return (
|
|
188
|
+
run.admission?.lane ?? defaultRunLaneV1(run.admission?.turnType ?? "chat")
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
140
192
|
/**
|
|
141
193
|
* The `admission` field a Turn records — nothing at all for a chat Turn with
|
|
142
194
|
* no recorded origin, so no stored bytes change for the Turn every producer
|
|
@@ -146,14 +198,25 @@ export function storedRunAdmissionV1(
|
|
|
146
198
|
turnType: TurnTypeV1 | undefined,
|
|
147
199
|
origin?: StoredRunOriginV1,
|
|
148
200
|
subagentRole?: string,
|
|
201
|
+
lane?: RunLaneV1,
|
|
149
202
|
): { admission?: StoredRunAdmissionV1 } {
|
|
150
203
|
const admitted = turnType ?? "chat";
|
|
151
|
-
|
|
204
|
+
// A lane that is already the turn type's default is not written: it says
|
|
205
|
+
// nothing the record does not, and writing it would change the bytes of the
|
|
206
|
+
// Turn every producer writes today.
|
|
207
|
+
const named = lane && lane !== defaultRunLaneV1(admitted) ? lane : undefined;
|
|
208
|
+
if (
|
|
209
|
+
admitted === "chat" &&
|
|
210
|
+
origin === undefined &&
|
|
211
|
+
subagentRole === undefined &&
|
|
212
|
+
named === undefined
|
|
213
|
+
)
|
|
152
214
|
return {};
|
|
153
215
|
return {
|
|
154
216
|
admission: {
|
|
155
217
|
schemaVersion: 1,
|
|
156
218
|
turnType: admitted,
|
|
219
|
+
...(named ? { lane: named } : {}),
|
|
157
220
|
...(subagentRole ? { subagentRole } : {}),
|
|
158
221
|
...(origin ? { origin } : {}),
|
|
159
222
|
},
|
|
@@ -168,9 +231,11 @@ const STORED_RUN_STATUSES: readonly StoredRunStatus[] = [
|
|
|
168
231
|
"completed",
|
|
169
232
|
"failed",
|
|
170
233
|
"cancelled",
|
|
234
|
+
"superseded",
|
|
171
235
|
"reconciliation-required",
|
|
172
236
|
];
|
|
173
237
|
const STORED_RUN_PHASES: readonly StoredRunPhase[] = [
|
|
238
|
+
"queued",
|
|
174
239
|
"admitted",
|
|
175
240
|
"executing",
|
|
176
241
|
"reconciliation-required",
|
|
@@ -193,6 +258,8 @@ const STORED_RUN_OPTIONAL_KEYS = [
|
|
|
193
258
|
"responseText",
|
|
194
259
|
"failure",
|
|
195
260
|
"stopRequestedAt",
|
|
261
|
+
"supersededAt",
|
|
262
|
+
"supersededBy",
|
|
196
263
|
"admission",
|
|
197
264
|
"directTool",
|
|
198
265
|
] as const;
|
|
@@ -333,6 +400,7 @@ function decodeStoredRunAdmission(
|
|
|
333
400
|
const allowed = new Set([
|
|
334
401
|
"schemaVersion",
|
|
335
402
|
"turnType",
|
|
403
|
+
"lane",
|
|
336
404
|
"subagentRole",
|
|
337
405
|
"origin",
|
|
338
406
|
]);
|
|
@@ -352,6 +420,13 @@ function decodeStoredRunAdmission(
|
|
|
352
420
|
} catch {
|
|
353
421
|
throw new Error(`run "${runId}" has an invalid admission turn type`);
|
|
354
422
|
}
|
|
423
|
+
const lane =
|
|
424
|
+
candidate.lane === undefined
|
|
425
|
+
? undefined
|
|
426
|
+
: RUN_LANES_V1.find((value) => value === candidate.lane);
|
|
427
|
+
if (candidate.lane !== undefined && !lane) {
|
|
428
|
+
throw new Error(`run "${runId}" has an invalid admission lane`);
|
|
429
|
+
}
|
|
355
430
|
if (
|
|
356
431
|
candidate.subagentRole !== undefined &&
|
|
357
432
|
(typeof candidate.subagentRole !== "string" ||
|
|
@@ -363,6 +438,7 @@ function decodeStoredRunAdmission(
|
|
|
363
438
|
return {
|
|
364
439
|
schemaVersion: 1,
|
|
365
440
|
turnType,
|
|
441
|
+
...(lane === undefined ? {} : { lane }),
|
|
366
442
|
...(candidate.subagentRole === undefined
|
|
367
443
|
? {}
|
|
368
444
|
: { subagentRole: candidate.subagentRole as string }),
|
|
@@ -521,6 +597,25 @@ function requireStoredRunRecordV1<Snapshot>(
|
|
|
521
597
|
) {
|
|
522
598
|
throw new Error(`run "${runId}" has invalid stopRequestedAt`);
|
|
523
599
|
}
|
|
600
|
+
if (
|
|
601
|
+
candidate.supersededAt !== undefined &&
|
|
602
|
+
(!boundedString(candidate.supersededAt, 64) ||
|
|
603
|
+
!Number.isFinite(Date.parse(candidate.supersededAt as string)))
|
|
604
|
+
) {
|
|
605
|
+
throw new Error(`run "${runId}" has invalid supersededAt`);
|
|
606
|
+
}
|
|
607
|
+
if (
|
|
608
|
+
candidate.supersededBy !== undefined &&
|
|
609
|
+
!boundedString(candidate.supersededBy, 128)
|
|
610
|
+
) {
|
|
611
|
+
throw new Error(`run "${runId}" has invalid supersededBy`);
|
|
612
|
+
}
|
|
613
|
+
if (
|
|
614
|
+
candidate.supersededBy !== undefined &&
|
|
615
|
+
candidate.supersededAt === undefined
|
|
616
|
+
) {
|
|
617
|
+
throw new Error(`run "${runId}" names a superseder with no supersede time`);
|
|
618
|
+
}
|
|
524
619
|
if (
|
|
525
620
|
status === "completed"
|
|
526
621
|
? candidate.responseText === undefined || candidate.failure !== undefined
|
|
@@ -538,6 +633,9 @@ function requireStoredRunRecordV1<Snapshot>(
|
|
|
538
633
|
if (status === "cancelled" && candidate.stopRequestedAt === undefined) {
|
|
539
634
|
throw new Error(`run "${runId}" has no durable stop intent`);
|
|
540
635
|
}
|
|
636
|
+
if (status === "superseded" && candidate.supersededAt === undefined) {
|
|
637
|
+
throw new Error(`run "${runId}" has no durable supersede intent`);
|
|
638
|
+
}
|
|
541
639
|
if (
|
|
542
640
|
(status === "reconciliation-required") !==
|
|
543
641
|
(phase === "reconciliation-required")
|
|
@@ -566,6 +664,12 @@ function requireStoredRunRecordV1<Snapshot>(
|
|
|
566
664
|
...(candidate.stopRequestedAt === undefined
|
|
567
665
|
? {}
|
|
568
666
|
: { stopRequestedAt: candidate.stopRequestedAt as string }),
|
|
667
|
+
...(candidate.supersededAt === undefined
|
|
668
|
+
? {}
|
|
669
|
+
: { supersededAt: candidate.supersededAt as string }),
|
|
670
|
+
...(candidate.supersededBy === undefined
|
|
671
|
+
? {}
|
|
672
|
+
: { supersededBy: candidate.supersededBy as string }),
|
|
569
673
|
...(candidate.admission === undefined
|
|
570
674
|
? {}
|
|
571
675
|
: { admission: decodeStoredRunAdmission(candidate.admission, runId) }),
|
|
@@ -602,6 +706,25 @@ export interface BotTurnCommand {
|
|
|
602
706
|
*/
|
|
603
707
|
skills?: SkillRefV1[];
|
|
604
708
|
directTool?: DirectToolCommandV1;
|
|
709
|
+
/**
|
|
710
|
+
* The lane this command asks to be admitted on. Absent means the lane its
|
|
711
|
+
* turn type defaults to.
|
|
712
|
+
*/
|
|
713
|
+
lane?: RunLaneV1;
|
|
714
|
+
/**
|
|
715
|
+
* The explicit intent to replace whatever is running with this command. A
|
|
716
|
+
* user-lane command that carries it is the authenticated cancellation of the
|
|
717
|
+
* active Turn: that run terminalizes `superseded` and this one takes its
|
|
718
|
+
* place. Without it a second command is refused exactly as it always was.
|
|
719
|
+
*
|
|
720
|
+
* `runId` is provenance — the run the sender had observed, which may already
|
|
721
|
+
* be stale — and never the target. Its absence means the sender had observed
|
|
722
|
+
* no run at all, which is a race rather than a different intention, so it
|
|
723
|
+
* supersedes just the same. The whole field is part of the command
|
|
724
|
+
* fingerprint, so a replayed command replays and never interrupts a second
|
|
725
|
+
* Turn.
|
|
726
|
+
*/
|
|
727
|
+
supersedes?: { runId?: string };
|
|
605
728
|
}
|
|
606
729
|
|
|
607
730
|
/**
|
|
@@ -616,12 +739,15 @@ export function botTurnCommandFingerprintV1(
|
|
|
616
739
|
): string {
|
|
617
740
|
const turnType = command.turnType ?? "chat";
|
|
618
741
|
const skills = command.skills ?? [];
|
|
742
|
+
const lane = command.lane ?? defaultRunLaneV1(turnType);
|
|
619
743
|
if (
|
|
620
744
|
turnType !== "chat" ||
|
|
621
745
|
command.origin !== undefined ||
|
|
622
746
|
command.subagentRole !== undefined ||
|
|
623
747
|
skills.length > 0 ||
|
|
624
|
-
command.directTool !== undefined
|
|
748
|
+
command.directTool !== undefined ||
|
|
749
|
+
lane !== defaultRunLaneV1(turnType) ||
|
|
750
|
+
command.supersedes !== undefined
|
|
625
751
|
) {
|
|
626
752
|
return `bot-turn-command-v2:${JSON.stringify({
|
|
627
753
|
userId: command.userId,
|
|
@@ -629,8 +755,10 @@ export function botTurnCommandFingerprintV1(
|
|
|
629
755
|
sessionId: command.sessionId,
|
|
630
756
|
text: command.text,
|
|
631
757
|
turnType,
|
|
758
|
+
...(lane === defaultRunLaneV1(turnType) ? {} : { lane }),
|
|
632
759
|
...(command.subagentRole ? { subagentRole: command.subagentRole } : {}),
|
|
633
760
|
...(command.origin ? { origin: command.origin } : {}),
|
|
761
|
+
...(command.supersedes ? { supersedes: command.supersedes } : {}),
|
|
634
762
|
...(skills.length > 0 ? { skills: skills.map(formatSkillRefV1) } : {}),
|
|
635
763
|
...(command.directTool ? { directTool: command.directTool } : {}),
|
|
636
764
|
})}`;
|
package/src/run-terminal.ts
CHANGED
|
@@ -50,6 +50,83 @@ function assertPackageRecordKeys(
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Records a Package writes in the transaction that settles a Turn as
|
|
55
|
+
* `superseded`. Same shape and same rule as `TerminalPackageRecords`: the
|
|
56
|
+
* kernel writes opaque keys and holds none of the policy that produced them.
|
|
57
|
+
* It is a separate seam because a superseded Turn is not a completed one — the
|
|
58
|
+
* Package that owns the conversation wants to leave the *next* Turn a durable
|
|
59
|
+
* note, not advance the records a finished Turn advances.
|
|
60
|
+
*/
|
|
61
|
+
export type SupersededPackageRecords<Snapshot> = (input: {
|
|
62
|
+
run: StoredRunV1<Snapshot>;
|
|
63
|
+
read<T>(key: string): Promise<T | undefined>;
|
|
64
|
+
}) => Promise<Record<string, unknown>>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Settles a superseded run as terminal `superseded` and clears its active
|
|
68
|
+
* marker. Like a cancelled run it produces no response text, no failure, and
|
|
69
|
+
* no notification: the Turn that replaced it is what the User is watching.
|
|
70
|
+
*/
|
|
71
|
+
export async function supersedeStoredRun<Snapshot>(
|
|
72
|
+
codec: StoredRunCodecV1<Snapshot>,
|
|
73
|
+
storage: RunTerminalStorage,
|
|
74
|
+
keys: RunTerminalKeys,
|
|
75
|
+
runId: string,
|
|
76
|
+
previous: readonly SessionEvent[],
|
|
77
|
+
events: readonly SessionEvent[],
|
|
78
|
+
packageRecords?: SupersededPackageRecords<Snapshot>,
|
|
79
|
+
): Promise<"superseded"> {
|
|
80
|
+
const stored = await storage.get<StoredRunV1<Snapshot>>(keys.run);
|
|
81
|
+
if (!stored) throw new Error(`run "${runId}" was not accepted`);
|
|
82
|
+
const run = codec.require(stored);
|
|
83
|
+
if (!run.supersededAt) {
|
|
84
|
+
throw new Error(`run "${runId}" has no durable supersede intent`);
|
|
85
|
+
}
|
|
86
|
+
const decodedEvents = events.map(decodeSessionEvent);
|
|
87
|
+
const { responseText: _text, failure: _failure, ...settled } = run;
|
|
88
|
+
// A run superseded while still queued never started, never appended an
|
|
89
|
+
// event, and never spoke: it settles as a record on its own and leaves both
|
|
90
|
+
// the session log and the running Turn exactly where they were.
|
|
91
|
+
const queued = settled.phase === "queued";
|
|
92
|
+
const superseded = codec.require({
|
|
93
|
+
...settled,
|
|
94
|
+
events: queued ? [] : decodedEvents,
|
|
95
|
+
status: "superseded",
|
|
96
|
+
phase:
|
|
97
|
+
settled.phase === "reconciliation-required"
|
|
98
|
+
? "executing"
|
|
99
|
+
: settled.phase === "queued"
|
|
100
|
+
? "admitted"
|
|
101
|
+
: settled.phase,
|
|
102
|
+
} satisfies StoredRunV1<Snapshot>);
|
|
103
|
+
const records: Record<string, unknown> = {
|
|
104
|
+
[keys.run]: structuredClone(superseded),
|
|
105
|
+
...(queued
|
|
106
|
+
? {}
|
|
107
|
+
: {
|
|
108
|
+
[keys.latestEvents]: structuredClone(
|
|
109
|
+
[...previous, ...decodedEvents].map(decodeSessionEvent),
|
|
110
|
+
),
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
if (packageRecords && !queued) {
|
|
114
|
+
const contributed = await packageRecords({
|
|
115
|
+
run: superseded,
|
|
116
|
+
read: <T>(key: string) => storage.get<T>(key),
|
|
117
|
+
});
|
|
118
|
+
assertPackageRecordKeys(contributed, keys);
|
|
119
|
+
for (const [key, value] of Object.entries(contributed)) {
|
|
120
|
+
records[key] = structuredClone(value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
await storage.put(records);
|
|
124
|
+
if ((await storage.get<string>(keys.activeRun)) === runId) {
|
|
125
|
+
await storage.delete(keys.activeRun);
|
|
126
|
+
}
|
|
127
|
+
return "superseded";
|
|
128
|
+
}
|
|
129
|
+
|
|
53
130
|
export async function completeStoredRun<Snapshot>(
|
|
54
131
|
codec: StoredRunCodecV1<Snapshot>,
|
|
55
132
|
storage: RunTerminalStorage,
|
|
@@ -58,7 +135,8 @@ export async function completeStoredRun<Snapshot>(
|
|
|
58
135
|
previous: readonly SessionEvent[],
|
|
59
136
|
result: BotTurnCompletion,
|
|
60
137
|
packageRecords?: TerminalPackageRecords<Snapshot>,
|
|
61
|
-
|
|
138
|
+
supersededRecords?: SupersededPackageRecords<Snapshot>,
|
|
139
|
+
): Promise<"completed" | "cancelled" | "superseded"> {
|
|
62
140
|
const activeRunId = await storage.get<string>(keys.activeRun);
|
|
63
141
|
if (activeRunId !== runId) throw new Error(`run "${runId}" is not active`);
|
|
64
142
|
const stored = await storage.get<StoredRunV1<Snapshot>>(keys.run);
|
|
@@ -66,6 +144,20 @@ export async function completeStoredRun<Snapshot>(
|
|
|
66
144
|
const run = codec.require(stored);
|
|
67
145
|
const events = result.events.map(decodeSessionEvent);
|
|
68
146
|
const latestEvents = [...previous, ...events].map(decodeSessionEvent);
|
|
147
|
+
// Stop outranks supersede: the User asked for this Turn to stop, and a
|
|
148
|
+
// message that arrived after that does not turn their cancellation into
|
|
149
|
+
// something else.
|
|
150
|
+
if (run.supersededAt && !run.stopRequestedAt) {
|
|
151
|
+
return supersedeStoredRun(
|
|
152
|
+
codec,
|
|
153
|
+
storage,
|
|
154
|
+
keys,
|
|
155
|
+
runId,
|
|
156
|
+
previous,
|
|
157
|
+
result.events,
|
|
158
|
+
supersededRecords,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
69
161
|
// Durable Stop intent recorded before this settlement wins: the run becomes
|
|
70
162
|
// terminal `cancelled` with no response text, failure, or notification.
|
|
71
163
|
if (run.stopRequestedAt) {
|
|
@@ -162,7 +254,10 @@ export async function failStoredRun<Snapshot>(
|
|
|
162
254
|
previous: readonly SessionEvent[],
|
|
163
255
|
events: readonly SessionEvent[],
|
|
164
256
|
failure: string,
|
|
165
|
-
|
|
257
|
+
supersededRecords?: SupersededPackageRecords<Snapshot>,
|
|
258
|
+
): Promise<
|
|
259
|
+
"failed" | "cancelled" | "superseded" | "preserved-completion" | "missing"
|
|
260
|
+
> {
|
|
166
261
|
const stored = await storage.get<StoredRunV1<Snapshot>>(keys.run);
|
|
167
262
|
if (!stored) return "missing";
|
|
168
263
|
const run = codec.require(stored);
|
|
@@ -171,6 +266,18 @@ export async function failStoredRun<Snapshot>(
|
|
|
171
266
|
if (run.stopRequestedAt) {
|
|
172
267
|
return cancelStoredRun(codec, storage, keys, runId, previous, events);
|
|
173
268
|
}
|
|
269
|
+
// Nor does a superseded one. The Turn that replaced it is the outcome.
|
|
270
|
+
if (run.supersededAt) {
|
|
271
|
+
return supersedeStoredRun(
|
|
272
|
+
codec,
|
|
273
|
+
storage,
|
|
274
|
+
keys,
|
|
275
|
+
runId,
|
|
276
|
+
previous,
|
|
277
|
+
events,
|
|
278
|
+
supersededRecords,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
174
281
|
const decodedEvents = events.map(decodeSessionEvent);
|
|
175
282
|
const latestEvents = [...previous, ...decodedEvents].map(decodeSessionEvent);
|
|
176
283
|
const failed = codec.require({
|
package/src/storage-keys.ts
CHANGED
|
@@ -8,6 +8,14 @@ export const RUN_ADMISSION_FENCE_PREFIX = "run-admission-fence:";
|
|
|
8
8
|
export const RUN_ADMISSION_FENCE_INDEX_KEY = "run-admission-fences";
|
|
9
9
|
export const MAX_RUN_ADMISSION_FENCES = 256;
|
|
10
10
|
export const ACTIVE_RUN_KEY = "active-run";
|
|
11
|
+
/**
|
|
12
|
+
* The one admitted user-lane Turn waiting for the object to become free.
|
|
13
|
+
*
|
|
14
|
+
* A single slot, not a queue: a second user message supersedes the first
|
|
15
|
+
* waiting one exactly as it supersedes a running one, so the Bot is never
|
|
16
|
+
* working through a backlog of things the User has already replaced.
|
|
17
|
+
*/
|
|
18
|
+
export const PENDING_RUN_KEY = "pending-run";
|
|
11
19
|
export const LATEST_EVENTS_KEY = "latest-events";
|
|
12
20
|
export const IDENTITY_KEY = "identity";
|
|
13
21
|
export const NOTIFICATION_PREFIX = "notification:";
|