@frockbot/kernel-do 0.3.15 → 0.3.16
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 +176 -136
- package/src/conversations.test.ts +7 -9
- package/src/index.ts +1 -0
- package/src/model-timeout-settlement.test.ts +8 -8
- package/src/run-liveness.test.ts +2 -1
- package/src/run-records.ts +93 -2
- package/src/run-terminal-open-turn.test.ts +9 -4
- package/src/run-terminal.ts +61 -44
- package/src/session-event-log.test.ts +267 -0
- package/src/session-event-log.ts +731 -0
- package/src/session-log-size.test.ts +250 -0
- package/src/storage-keys.ts +4 -0
- package/src/turn-admission.test.ts +15 -9
- package/src/turn-supersede.test.ts +13 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/kernel-do",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@frockbot/kernel-composition": "0.3.
|
|
16
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
15
|
+
"@frockbot/kernel-composition": "0.3.16",
|
|
16
|
+
"@frockbot/kernel-contracts": "0.3.16",
|
|
17
17
|
"cordis": "4.0.0-rc.8"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
package/src/authority.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
botTurnCommandFingerprintV1,
|
|
17
17
|
defaultRunLaneV1,
|
|
18
18
|
storedRunAdmissionV1,
|
|
19
|
+
storedRunEventFieldsV2,
|
|
20
|
+
storedRunRecordV2,
|
|
19
21
|
storedRunSubagentRoleV1,
|
|
20
22
|
storedRunTurnTypeV1,
|
|
21
23
|
type BotNotificationIntent,
|
|
@@ -41,6 +43,10 @@ import {
|
|
|
41
43
|
unresolvedModelRequestFailure,
|
|
42
44
|
} from "./run-recovery.js";
|
|
43
45
|
import { runLivenessV1, STALE_RUNNING_RUN_FAILURE_V1 } from "./run-liveness.js";
|
|
46
|
+
import {
|
|
47
|
+
SessionEventLog,
|
|
48
|
+
type SessionEventLogStorage,
|
|
49
|
+
} from "./session-event-log.js";
|
|
44
50
|
import {
|
|
45
51
|
BotTurnReconciliationRequiredError,
|
|
46
52
|
BotTurnRecoveryRequiredError,
|
|
@@ -395,9 +401,8 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
395
401
|
if (await transaction.get<string>(ACTIVE_RUN_KEY)) {
|
|
396
402
|
return "blocked" as const;
|
|
397
403
|
}
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
).map(decodeSessionEvent);
|
|
404
|
+
const eventLog = new SessionEventLog(transaction);
|
|
405
|
+
const storedEvents = await eventLog.migrate(run.sessionId);
|
|
401
406
|
// A queued Turn was admitted while another was executing, so admission
|
|
402
407
|
// could not repair the log: something was still entitled to close that
|
|
403
408
|
// Turn. Here the active-run marker is gone and nothing is, so the same
|
|
@@ -408,17 +413,12 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
408
413
|
...run,
|
|
409
414
|
phase: "admitted",
|
|
410
415
|
previousEventCount: latestEvents.length,
|
|
416
|
+
...storedRunEventFieldsV2(latestEvents.length, []),
|
|
411
417
|
} satisfies StoredRunV1<Snapshot>);
|
|
418
|
+
if (repaired) await eventLog.rewrite(run.sessionId, latestEvents);
|
|
412
419
|
await transaction.put({
|
|
413
|
-
[key]: structuredClone(promoted),
|
|
420
|
+
[key]: structuredClone(storedRunRecordV2(promoted)),
|
|
414
421
|
[ACTIVE_RUN_KEY]: runId,
|
|
415
|
-
...(repaired
|
|
416
|
-
? {
|
|
417
|
-
[LATEST_EVENTS_KEY]: structuredClone(
|
|
418
|
-
repaired.map(decodeSessionEvent),
|
|
419
|
-
),
|
|
420
|
-
}
|
|
421
|
-
: {}),
|
|
422
422
|
});
|
|
423
423
|
await transaction.delete(PENDING_RUN_KEY);
|
|
424
424
|
await this.refreshRecoveryAlarm(transaction);
|
|
@@ -437,7 +437,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
437
437
|
await this.assertIdentity(identity);
|
|
438
438
|
const key = `${RUN_PREFIX}${runId}`;
|
|
439
439
|
const recovery = await this.ctx.storage.transaction(async (transaction) => {
|
|
440
|
-
const run = this.
|
|
440
|
+
const run = await this.readRunFrom(transaction, runId);
|
|
441
441
|
const activeRunId = await transaction.get<string>(ACTIVE_RUN_KEY);
|
|
442
442
|
if (
|
|
443
443
|
!run ||
|
|
@@ -446,9 +446,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
446
446
|
) {
|
|
447
447
|
throw new Error(`run "${runId}" does not require reconciliation`);
|
|
448
448
|
}
|
|
449
|
-
const latest = (
|
|
450
|
-
(await transaction.get<SessionEvent[]>(LATEST_EVENTS_KEY)) ?? []
|
|
451
|
-
).map(decodeSessionEvent);
|
|
449
|
+
const latest = await new SessionEventLog(transaction).read(run.sessionId);
|
|
452
450
|
const settings = run.configurationSnapshot;
|
|
453
451
|
// The failure is *removed*, not set to `undefined`: a running run that
|
|
454
452
|
// carries a `failure` key is a shape the run record does not allow, and
|
|
@@ -456,15 +454,14 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
456
454
|
// read afterwards. `require` checks it here, where the write is, rather
|
|
457
455
|
// than leaving the projector to fail on every later read.
|
|
458
456
|
const { failure: _failure, ...resumed } = run;
|
|
457
|
+
const resumedRun = this.codec.require({
|
|
458
|
+
...resumed,
|
|
459
|
+
status: "running",
|
|
460
|
+
phase: "executing",
|
|
461
|
+
} satisfies StoredRunV1<Snapshot>);
|
|
459
462
|
await transaction.put(
|
|
460
463
|
key,
|
|
461
|
-
structuredClone(
|
|
462
|
-
this.codec.require({
|
|
463
|
-
...resumed,
|
|
464
|
-
status: "running",
|
|
465
|
-
phase: "executing",
|
|
466
|
-
} satisfies StoredRunV1<Snapshot>),
|
|
467
|
-
),
|
|
464
|
+
structuredClone(storedRunRecordV2(resumedRun)),
|
|
468
465
|
);
|
|
469
466
|
await this.refreshRecoveryAlarm(transaction);
|
|
470
467
|
return { run, latest, settings };
|
|
@@ -477,9 +474,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
477
474
|
recovery.settings,
|
|
478
475
|
);
|
|
479
476
|
} catch (error) {
|
|
480
|
-
const current = this.
|
|
481
|
-
await this.ctx.storage.get<unknown>(key),
|
|
482
|
-
);
|
|
477
|
+
const current = await this.readRun(runId);
|
|
483
478
|
if (current?.status === "reconciliation-required") {
|
|
484
479
|
const previous = recovery.latest.slice(0, current.previousEventCount);
|
|
485
480
|
const failure =
|
|
@@ -511,9 +506,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
511
506
|
private async settledTerminalRunResult(
|
|
512
507
|
runId: string,
|
|
513
508
|
): Promise<BotTurnCompletion | undefined> {
|
|
514
|
-
const run = this.
|
|
515
|
-
await this.ctx.storage.get<unknown>(`${RUN_PREFIX}${runId}`),
|
|
516
|
-
);
|
|
509
|
+
const run = await this.readRun(runId);
|
|
517
510
|
if (
|
|
518
511
|
run?.status !== "failed" &&
|
|
519
512
|
run?.status !== "cancelled" &&
|
|
@@ -572,10 +565,13 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
572
565
|
if (!run || run.status !== "running") {
|
|
573
566
|
throw new Error(`run "${command.runId}" is not resumable`);
|
|
574
567
|
}
|
|
575
|
-
await transaction.put(
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
568
|
+
await transaction.put(
|
|
569
|
+
key,
|
|
570
|
+
storedRunRecordV2({
|
|
571
|
+
...run,
|
|
572
|
+
phase: "executing",
|
|
573
|
+
} satisfies StoredRunV1<Snapshot>),
|
|
574
|
+
);
|
|
579
575
|
await this.refreshRecoveryAlarm(transaction);
|
|
580
576
|
});
|
|
581
577
|
const result = await this.hooks.executeTurn({
|
|
@@ -592,9 +588,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
592
588
|
await this.completeRun(command.runId, previous, completed, settings);
|
|
593
589
|
return completed;
|
|
594
590
|
} catch (error) {
|
|
595
|
-
const durableRun = this.
|
|
596
|
-
await this.ctx.storage.get<unknown>(`${RUN_PREFIX}${command.runId}`),
|
|
597
|
-
);
|
|
591
|
+
const durableRun = await this.readRun(command.runId);
|
|
598
592
|
const events = eventsForFailedRun(durableRun, error);
|
|
599
593
|
const message =
|
|
600
594
|
error instanceof Error ? error.message : "Bot turn failed";
|
|
@@ -652,9 +646,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
652
646
|
private async discardedRunResult(
|
|
653
647
|
runId: string,
|
|
654
648
|
): Promise<BotTurnCompletion | undefined> {
|
|
655
|
-
const run = this.
|
|
656
|
-
await this.ctx.storage.get<unknown>(`${RUN_PREFIX}${runId}`),
|
|
657
|
-
);
|
|
649
|
+
const run = await this.readRun(runId);
|
|
658
650
|
if (run?.status !== "superseded" && run?.status !== "cancelled") {
|
|
659
651
|
return undefined;
|
|
660
652
|
}
|
|
@@ -669,9 +661,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
669
661
|
private async terminalRunResult(
|
|
670
662
|
runId: string,
|
|
671
663
|
): Promise<BotTurnCompletion | undefined> {
|
|
672
|
-
const run = this.
|
|
673
|
-
await this.ctx.storage.get<unknown>(`${RUN_PREFIX}${runId}`),
|
|
674
|
-
);
|
|
664
|
+
const run = await this.readRun(runId);
|
|
675
665
|
if (run?.status === "superseded" || run?.status === "cancelled") {
|
|
676
666
|
return { runId, text: "", events: structuredClone(run.events) };
|
|
677
667
|
}
|
|
@@ -730,9 +720,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
730
720
|
? { admittedRequest: modelState.request.request }
|
|
731
721
|
: {}),
|
|
732
722
|
});
|
|
733
|
-
const durableRun = this.
|
|
734
|
-
await this.ctx.storage.get<unknown>(`${RUN_PREFIX}${run.runId}`),
|
|
735
|
-
);
|
|
723
|
+
const durableRun = await this.readRun(run.runId);
|
|
736
724
|
if (!durableRun) throw new Error(`run "${run.runId}" was not accepted`);
|
|
737
725
|
const fullResult = {
|
|
738
726
|
...result,
|
|
@@ -742,9 +730,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
742
730
|
await this.completeRun(run.runId, previous, completed, settings);
|
|
743
731
|
return completed;
|
|
744
732
|
} catch (error) {
|
|
745
|
-
const durableRun = this.
|
|
746
|
-
await this.ctx.storage.get<unknown>(`${RUN_PREFIX}${run.runId}`),
|
|
747
|
-
);
|
|
733
|
+
const durableRun = await this.readRun(run.runId);
|
|
748
734
|
const events = durableRun?.events ?? run.events;
|
|
749
735
|
const message =
|
|
750
736
|
error instanceof Error ? error.message : "Bot turn failed";
|
|
@@ -780,16 +766,17 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
780
766
|
|
|
781
767
|
private async deferRunRecovery(runId: string): Promise<void> {
|
|
782
768
|
await this.ctx.storage.transaction(async (transaction) => {
|
|
783
|
-
const run = this.
|
|
784
|
-
await transaction.get<unknown>(`${RUN_PREFIX}${runId}`),
|
|
785
|
-
);
|
|
769
|
+
const run = await this.readRunFrom(transaction, runId);
|
|
786
770
|
if (!run || run.status !== "running") {
|
|
787
771
|
throw new Error(`run "${runId}" is not resumable`);
|
|
788
772
|
}
|
|
789
|
-
await transaction.put(
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
773
|
+
await transaction.put(
|
|
774
|
+
`${RUN_PREFIX}${runId}`,
|
|
775
|
+
storedRunRecordV2({
|
|
776
|
+
...run,
|
|
777
|
+
phase: "executing",
|
|
778
|
+
} satisfies StoredRunV1<Snapshot>),
|
|
779
|
+
);
|
|
793
780
|
await this.refreshRecoveryAlarm(transaction);
|
|
794
781
|
});
|
|
795
782
|
}
|
|
@@ -798,9 +785,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
798
785
|
command: OwnedBotTurnCommand,
|
|
799
786
|
): Promise<BotTurnCompletion | undefined> {
|
|
800
787
|
const { runId } = command;
|
|
801
|
-
const run = this.
|
|
802
|
-
await this.ctx.storage.get<unknown>(`${RUN_PREFIX}${runId}`),
|
|
803
|
-
);
|
|
788
|
+
const run = await this.readRun(runId);
|
|
804
789
|
if (!run) return undefined;
|
|
805
790
|
if (run.commandFingerprint !== botTurnCommandFingerprintV1(command)) {
|
|
806
791
|
throw new BotTurnRefusedError(
|
|
@@ -900,10 +885,13 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
900
885
|
throw new Error(`run "${runId}" is not resumable`);
|
|
901
886
|
}
|
|
902
887
|
if (run.compositionGenerationId === compositionGenerationId) return;
|
|
903
|
-
await transaction.put(
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
888
|
+
await transaction.put(
|
|
889
|
+
key,
|
|
890
|
+
storedRunRecordV2({
|
|
891
|
+
...run,
|
|
892
|
+
compositionGenerationId,
|
|
893
|
+
} satisfies StoredRunV1<Snapshot>),
|
|
894
|
+
);
|
|
907
895
|
});
|
|
908
896
|
}
|
|
909
897
|
|
|
@@ -1083,12 +1071,17 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1083
1071
|
ordinal: current.ordinal + 1,
|
|
1084
1072
|
startedAt: endedAt,
|
|
1085
1073
|
};
|
|
1074
|
+
const currentSessionId = conversationSessionIdV1(base, current.ordinal);
|
|
1075
|
+
const nextSessionId = conversationSessionIdV1(base, next.ordinal);
|
|
1076
|
+
const eventLog = new SessionEventLog(transaction);
|
|
1077
|
+
await eventLog.migrate(currentSessionId);
|
|
1078
|
+
await eventLog.clearCurrent(nextSessionId);
|
|
1086
1079
|
await transaction.put({
|
|
1087
1080
|
[CONVERSATION_KEY]: next,
|
|
1088
1081
|
[CONVERSATION_INDEX_KEY]: [
|
|
1089
1082
|
{
|
|
1090
1083
|
schemaVersion: 1 as const,
|
|
1091
|
-
sessionId:
|
|
1084
|
+
sessionId: currentSessionId,
|
|
1092
1085
|
ordinal: current.ordinal,
|
|
1093
1086
|
startedAt: current.startedAt,
|
|
1094
1087
|
endedAt,
|
|
@@ -1097,13 +1090,10 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1097
1090
|
]
|
|
1098
1091
|
.sort((left, right) => right.ordinal - left.ordinal)
|
|
1099
1092
|
.slice(0, MAX_LISTED_CONVERSATIONS),
|
|
1100
|
-
// The next Turn derives its messages from an empty log. Nothing is
|
|
1101
|
-
// deleted: `run:<id>` still holds every event of every Turn.
|
|
1102
|
-
[LATEST_EVENTS_KEY]: [],
|
|
1103
1093
|
});
|
|
1104
1094
|
return {
|
|
1105
1095
|
schemaVersion: 1 as const,
|
|
1106
|
-
sessionId:
|
|
1096
|
+
sessionId: nextSessionId,
|
|
1107
1097
|
ordinal: next.ordinal,
|
|
1108
1098
|
startedAt: next.startedAt,
|
|
1109
1099
|
};
|
|
@@ -1135,9 +1125,64 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1135
1125
|
async readStoredRun(
|
|
1136
1126
|
runId: string,
|
|
1137
1127
|
): Promise<StoredRunV1<Snapshot> | undefined> {
|
|
1138
|
-
return this.
|
|
1128
|
+
return this.readRunFrom(this.ctx.storage, runId);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private async readRunFrom(
|
|
1132
|
+
storage: SessionEventLogStorage,
|
|
1133
|
+
runId: string,
|
|
1134
|
+
): Promise<StoredRunV1<Snapshot> | undefined> {
|
|
1135
|
+
const run = this.codec.optional(
|
|
1136
|
+
await storage.get<unknown>(`${RUN_PREFIX}${runId}`),
|
|
1137
|
+
);
|
|
1138
|
+
if (!run?.eventRange) return run;
|
|
1139
|
+
const events = await new SessionEventLog(storage).readRange(
|
|
1140
|
+
run.sessionId,
|
|
1141
|
+
run.eventRange.startSeq,
|
|
1142
|
+
run.eventRange.endSeq,
|
|
1143
|
+
);
|
|
1144
|
+
if (events.length !== run.eventRange.endSeq - run.eventRange.startSeq) {
|
|
1145
|
+
throw new Error(`run "${run.runId}" has an incomplete event range`);
|
|
1146
|
+
}
|
|
1147
|
+
return this.codec.require({ ...run, events });
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/** Exact Session history, reconstructed through the paged durable log. */
|
|
1151
|
+
async readSessionEvents(sessionId: string): Promise<SessionEvent[]> {
|
|
1152
|
+
return new SessionEventLog(this.ctx.storage).read(sessionId);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* The bounded durable event projections for a run. This is the inspection
|
|
1157
|
+
* path: recovery and client transcript projection use `readStoredRun` and
|
|
1158
|
+
* therefore receive exact events, while a debug snapshot never hydrates a
|
|
1159
|
+
* multi-megabyte prompt merely to cut it again.
|
|
1160
|
+
*/
|
|
1161
|
+
async readRunEventProjections(runId: string): Promise<
|
|
1162
|
+
| {
|
|
1163
|
+
run: StoredRunV1<Snapshot>;
|
|
1164
|
+
events: unknown[];
|
|
1165
|
+
eventCount: number;
|
|
1166
|
+
}
|
|
1167
|
+
| undefined
|
|
1168
|
+
> {
|
|
1169
|
+
const run = this.codec.optional(
|
|
1139
1170
|
await this.ctx.storage.get<unknown>(`${RUN_PREFIX}${runId}`),
|
|
1140
1171
|
);
|
|
1172
|
+
if (!run) return undefined;
|
|
1173
|
+
if (!run.eventRange) {
|
|
1174
|
+
return { run, events: run.events, eventCount: run.events.length };
|
|
1175
|
+
}
|
|
1176
|
+
const events = await new SessionEventLog(this.ctx.storage).readProjections(
|
|
1177
|
+
run.sessionId,
|
|
1178
|
+
run.eventRange.startSeq,
|
|
1179
|
+
run.eventRange.endSeq,
|
|
1180
|
+
);
|
|
1181
|
+
const eventCount = run.eventRange.endSeq - run.eventRange.startSeq;
|
|
1182
|
+
if (events.length !== eventCount) {
|
|
1183
|
+
throw new Error(`run "${run.runId}" has an incomplete event range`);
|
|
1184
|
+
}
|
|
1185
|
+
return { run, events, eventCount };
|
|
1141
1186
|
}
|
|
1142
1187
|
|
|
1143
1188
|
/** Durable run record, checked against the key it was looked up by. */
|
|
@@ -1183,9 +1228,9 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1183
1228
|
if (runId === this.executingRunId) return true;
|
|
1184
1229
|
const run = await this.readRun(runId);
|
|
1185
1230
|
if (!run || run.status !== "running") return false;
|
|
1186
|
-
const sessionEvents = (
|
|
1187
|
-
|
|
1188
|
-
)
|
|
1231
|
+
const sessionEvents = await new SessionEventLog(this.ctx.storage).read(
|
|
1232
|
+
run.sessionId,
|
|
1233
|
+
);
|
|
1189
1234
|
if (runLivenessV1({ run, sessionEvents }).working) return true;
|
|
1190
1235
|
await this.settleStaleRun(runId);
|
|
1191
1236
|
return false;
|
|
@@ -1202,13 +1247,9 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1202
1247
|
private async settleStaleRun(runId: string): Promise<void> {
|
|
1203
1248
|
await this.ctx.storage.transaction(async (transaction) => {
|
|
1204
1249
|
if (runId === this.executingRunId) return;
|
|
1205
|
-
const run = this.
|
|
1206
|
-
await transaction.get<unknown>(`${RUN_PREFIX}${runId}`),
|
|
1207
|
-
);
|
|
1250
|
+
const run = await this.readRunFrom(transaction, runId);
|
|
1208
1251
|
if (!run || run.runId !== runId || run.status !== "running") return;
|
|
1209
|
-
const latest = (
|
|
1210
|
-
(await transaction.get<SessionEvent[]>(LATEST_EVENTS_KEY)) ?? []
|
|
1211
|
-
).map(decodeSessionEvent);
|
|
1252
|
+
const latest = await new SessionEventLog(transaction).read(run.sessionId);
|
|
1212
1253
|
if (runLivenessV1({ run, sessionEvents: latest }).working) return;
|
|
1213
1254
|
await failStoredRun(
|
|
1214
1255
|
this.codec,
|
|
@@ -1389,9 +1430,8 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1389
1430
|
const supersede = activeRunId
|
|
1390
1431
|
? await this.planSupersede(transaction, command, activeRunId)
|
|
1391
1432
|
: undefined;
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
).map(decodeSessionEvent);
|
|
1433
|
+
const eventLog = new SessionEventLog(transaction);
|
|
1434
|
+
const storedEvents = await eventLog.migrate(command.sessionId);
|
|
1395
1435
|
// A Turn that died between `turn/start` and `turn/end` — an event the
|
|
1396
1436
|
// encoder refused, a durable write that failed — left the log open, and
|
|
1397
1437
|
// every later Turn failed validation with "turn N started while turn
|
|
@@ -1426,12 +1466,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1426
1466
|
? undefined
|
|
1427
1467
|
: repairedSessionLogV1(command.sessionId, storedEvents);
|
|
1428
1468
|
const latestEvents = repaired ?? storedEvents;
|
|
1429
|
-
if (repaired)
|
|
1430
|
-
await transaction.put(
|
|
1431
|
-
LATEST_EVENTS_KEY,
|
|
1432
|
-
structuredClone(latestEvents.map(decodeSessionEvent)),
|
|
1433
|
-
);
|
|
1434
|
-
}
|
|
1469
|
+
if (repaired) await eventLog.rewrite(command.sessionId, latestEvents);
|
|
1435
1470
|
const admittedSettings = await this.hooks.admittedSnapshot(
|
|
1436
1471
|
transaction,
|
|
1437
1472
|
settings,
|
|
@@ -1464,7 +1499,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1464
1499
|
: {}),
|
|
1465
1500
|
} satisfies StoredRunV1<Snapshot>);
|
|
1466
1501
|
await transaction.put({
|
|
1467
|
-
[key]: admittedRun,
|
|
1502
|
+
[key]: storedRunRecordV2(admittedRun),
|
|
1468
1503
|
[runIndexKey(command.acceptedAt, command.runId)]: command.runId,
|
|
1469
1504
|
...(activeRunId
|
|
1470
1505
|
? { [PENDING_RUN_KEY]: command.runId }
|
|
@@ -1521,9 +1556,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1521
1556
|
if (lane !== "user" || !command.supersedes) {
|
|
1522
1557
|
throw new BotTurnRefusedError("busy", "bot already has an active run");
|
|
1523
1558
|
}
|
|
1524
|
-
const active = this.
|
|
1525
|
-
await transaction.get<unknown>(`${RUN_PREFIX}${activeRunId}`),
|
|
1526
|
-
);
|
|
1559
|
+
const active = await this.readRunFrom(transaction, activeRunId);
|
|
1527
1560
|
if (!active)
|
|
1528
1561
|
throw new BotTurnRefusedError("busy", "bot already has an active run");
|
|
1529
1562
|
if (active.status === "reconciliation-required") {
|
|
@@ -1551,15 +1584,14 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1551
1584
|
}
|
|
1552
1585
|
if (!dispatched) return false;
|
|
1553
1586
|
if (active.supersededAt) return true;
|
|
1587
|
+
const superseded = this.codec.require({
|
|
1588
|
+
...active,
|
|
1589
|
+
supersededAt: new Date().toISOString(),
|
|
1590
|
+
supersededBy,
|
|
1591
|
+
} satisfies StoredRunV1<Snapshot>);
|
|
1554
1592
|
await transaction.put(
|
|
1555
1593
|
`${RUN_PREFIX}${activeRunId}`,
|
|
1556
|
-
structuredClone(
|
|
1557
|
-
this.codec.require({
|
|
1558
|
-
...active,
|
|
1559
|
-
supersededAt: new Date().toISOString(),
|
|
1560
|
-
supersededBy,
|
|
1561
|
-
} satisfies StoredRunV1<Snapshot>),
|
|
1562
|
-
),
|
|
1594
|
+
structuredClone(storedRunRecordV2(superseded)),
|
|
1563
1595
|
);
|
|
1564
1596
|
return true;
|
|
1565
1597
|
};
|
|
@@ -1580,18 +1612,14 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1580
1612
|
return;
|
|
1581
1613
|
}
|
|
1582
1614
|
const { responseText: _text, failure: _failure, ...settled } = queued;
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
supersededBy,
|
|
1592
|
-
} satisfies StoredRunV1<Snapshot>),
|
|
1593
|
-
),
|
|
1594
|
-
);
|
|
1615
|
+
const superseded = this.codec.require({
|
|
1616
|
+
...settled,
|
|
1617
|
+
status: "superseded",
|
|
1618
|
+
phase: "admitted",
|
|
1619
|
+
supersededAt: new Date().toISOString(),
|
|
1620
|
+
supersededBy,
|
|
1621
|
+
} satisfies StoredRunV1<Snapshot>);
|
|
1622
|
+
await transaction.put(key, structuredClone(storedRunRecordV2(superseded)));
|
|
1595
1623
|
}
|
|
1596
1624
|
|
|
1597
1625
|
private async persistRunEvents(
|
|
@@ -1604,11 +1632,10 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1604
1632
|
if (durableEvents.length === 0) return;
|
|
1605
1633
|
const key = `${RUN_PREFIX}${runId}`;
|
|
1606
1634
|
await this.ctx.storage.transaction(async (transaction) => {
|
|
1607
|
-
const run = this.
|
|
1635
|
+
const run = await this.readRunFrom(transaction, runId);
|
|
1608
1636
|
if (!run) throw new Error(`run "${runId}" was not accepted`);
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
).map(decodeSessionEvent);
|
|
1637
|
+
const eventLog = new SessionEventLog(transaction);
|
|
1638
|
+
const latest = await eventLog.read(run.sessionId);
|
|
1612
1639
|
for (const [index, event] of durableEvents.entries()) {
|
|
1613
1640
|
if (event.seq !== latest.length + index) {
|
|
1614
1641
|
throw new Error(
|
|
@@ -1618,12 +1645,13 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1618
1645
|
}
|
|
1619
1646
|
const next = this.codec.require({
|
|
1620
1647
|
...run,
|
|
1621
|
-
|
|
1648
|
+
...storedRunEventFieldsV2(run.previousEventCount, [
|
|
1649
|
+
...run.events,
|
|
1650
|
+
...durableEvents,
|
|
1651
|
+
]),
|
|
1622
1652
|
} satisfies StoredRunV1<Snapshot>);
|
|
1623
|
-
await
|
|
1624
|
-
|
|
1625
|
-
[LATEST_EVENTS_KEY]: structuredClone([...latest, ...durableEvents]),
|
|
1626
|
-
});
|
|
1653
|
+
await eventLog.append(run.sessionId, durableEvents);
|
|
1654
|
+
await transaction.put(key, structuredClone(storedRunRecordV2(next)));
|
|
1627
1655
|
});
|
|
1628
1656
|
}
|
|
1629
1657
|
|
|
@@ -1841,7 +1869,7 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1841
1869
|
const recovery = await this.ctx.storage.transaction(async (transaction) => {
|
|
1842
1870
|
const current = await transaction.get<string>(ACTIVE_RUN_KEY);
|
|
1843
1871
|
if (!current || current === this.executingRunId) return undefined;
|
|
1844
|
-
const run = this.
|
|
1872
|
+
const run = await this.readRunFrom(transaction, activeRunId);
|
|
1845
1873
|
if (run?.status === "reconciliation-required") {
|
|
1846
1874
|
await this.refreshRecoveryAlarm(transaction);
|
|
1847
1875
|
return undefined;
|
|
@@ -1850,9 +1878,8 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1850
1878
|
await this.refreshRecoveryAlarm(transaction);
|
|
1851
1879
|
return undefined;
|
|
1852
1880
|
}
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
).map(decodeSessionEvent);
|
|
1881
|
+
const eventLog = new SessionEventLog(transaction);
|
|
1882
|
+
const latest = await eventLog.read(run.sessionId);
|
|
1856
1883
|
// A Turn the User stopped, or one a later message replaced, is terminal
|
|
1857
1884
|
// in intent before recovery ever looks at it. There is nothing to
|
|
1858
1885
|
// recover: no answer is owed, and the provider outcome cannot change what
|
|
@@ -1928,14 +1955,20 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1928
1955
|
}
|
|
1929
1956
|
if (plan.kind === "restart") {
|
|
1930
1957
|
const settings = run.configurationSnapshot;
|
|
1931
|
-
await
|
|
1932
|
-
|
|
1958
|
+
await eventLog.rewrite(run.sessionId, plan.previous);
|
|
1959
|
+
await transaction.put(
|
|
1960
|
+
key,
|
|
1961
|
+
storedRunRecordV2({
|
|
1933
1962
|
...run,
|
|
1934
1963
|
events: [],
|
|
1964
|
+
eventRange: {
|
|
1965
|
+
startSeq: plan.previous.length,
|
|
1966
|
+
endSeq: plan.previous.length,
|
|
1967
|
+
},
|
|
1968
|
+
previousEventCount: plan.previous.length,
|
|
1935
1969
|
phase: "admitted",
|
|
1936
|
-
} satisfies StoredRunV1<Snapshot
|
|
1937
|
-
|
|
1938
|
-
});
|
|
1970
|
+
} satisfies StoredRunV1<Snapshot>),
|
|
1971
|
+
);
|
|
1939
1972
|
await this.refreshRecoveryAlarm(transaction);
|
|
1940
1973
|
return {
|
|
1941
1974
|
kind: "restart" as const,
|
|
@@ -1946,24 +1979,31 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1946
1979
|
}
|
|
1947
1980
|
if (plan.kind === "resume") {
|
|
1948
1981
|
const settings = run.configurationSnapshot;
|
|
1949
|
-
await transaction.put(
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1982
|
+
await transaction.put(
|
|
1983
|
+
key,
|
|
1984
|
+
storedRunRecordV2({
|
|
1985
|
+
...run,
|
|
1986
|
+
phase: "executing",
|
|
1987
|
+
} satisfies StoredRunV1<Snapshot>),
|
|
1988
|
+
);
|
|
1953
1989
|
await this.refreshRecoveryAlarm(transaction);
|
|
1954
1990
|
return { kind: "resume" as const, run, latest, settings };
|
|
1955
1991
|
}
|
|
1956
|
-
await
|
|
1957
|
-
|
|
1992
|
+
await eventLog.append(run.sessionId, plan.repairs);
|
|
1993
|
+
await transaction.put(
|
|
1994
|
+
key,
|
|
1995
|
+
storedRunRecordV2({
|
|
1958
1996
|
...run,
|
|
1959
|
-
|
|
1997
|
+
...storedRunEventFieldsV2(run.previousEventCount, [
|
|
1998
|
+
...run.events,
|
|
1999
|
+
...plan.repairs,
|
|
2000
|
+
]),
|
|
1960
2001
|
status: "reconciliation-required",
|
|
1961
2002
|
phase: "reconciliation-required",
|
|
1962
2003
|
failure:
|
|
1963
2004
|
"Execution outcome requires reconciliation before it can resume",
|
|
1964
|
-
} satisfies StoredRunV1<Snapshot
|
|
1965
|
-
|
|
1966
|
-
});
|
|
2005
|
+
} satisfies StoredRunV1<Snapshot>),
|
|
2006
|
+
);
|
|
1967
2007
|
await this.refreshRecoveryAlarm(transaction);
|
|
1968
2008
|
return undefined;
|
|
1969
2009
|
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./conversations.ts";
|
|
17
17
|
import { MemoryStorage } from "./memory-storage.fixture.ts";
|
|
18
18
|
import { createStoredRunCodecV1 } from "./run-records.ts";
|
|
19
|
+
import { SessionEventLog } from "./session-event-log.ts";
|
|
19
20
|
|
|
20
21
|
const codec = createStoredRunCodecV1<undefined>({
|
|
21
22
|
decodeRunId: (value) => value as string,
|
|
@@ -108,20 +109,17 @@ describe("starting a new conversation", () => {
|
|
|
108
109
|
const probe = createAuthority(storage);
|
|
109
110
|
|
|
110
111
|
await probe.authority.run(command("run-1"));
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
112
|
+
const log = new SessionEventLog(storage);
|
|
113
|
+
expect((await log.read("user-1:primary")).length).toBe(1);
|
|
114
114
|
|
|
115
115
|
const started = await probe.authority.startConversation(IDENTITY);
|
|
116
116
|
expect(started.ordinal).toBe(2);
|
|
117
117
|
expect(started.sessionId).toBe("user-1:primary#2");
|
|
118
|
-
// The
|
|
119
|
-
expect(
|
|
118
|
+
// The next Turn starts from a distinct, empty paged log.
|
|
119
|
+
expect(await log.read("user-1:primary#2")).toEqual([]);
|
|
120
120
|
// The conversation just ended is still on disk, Turn for Turn.
|
|
121
|
-
expect(
|
|
122
|
-
|
|
123
|
-
.length,
|
|
124
|
-
).toBe(1);
|
|
121
|
+
expect((await probe.authority.readRun("run-1"))?.events.length).toBe(1);
|
|
122
|
+
expect(storage.values.get("run:run-1")).not.toHaveProperty("events");
|
|
125
123
|
|
|
126
124
|
await probe.authority.run(command("run-2"));
|
|
127
125
|
// The new Turn ran in the new Session, and saw none of the old history.
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./run-records.js";
|
|
|
7
7
|
export * from "./run-liveness.js";
|
|
8
8
|
export * from "./run-recovery.js";
|
|
9
9
|
export * from "./run-terminal.js";
|
|
10
|
+
export * from "./session-event-log.js";
|
|
10
11
|
export * from "./storage-keys.js";
|
|
11
12
|
export * from "./turn-errors.js";
|
|
12
13
|
export * from "./workspace-generations.js";
|