@frockbot/kernel-do 0.3.42 → 0.3.44
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 +75 -1
- package/src/run-records.ts +57 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/kernel-do",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.44",
|
|
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.44",
|
|
16
|
+
"@frockbot/kernel-contracts": "0.3.44",
|
|
17
17
|
"cordis": "4.0.0-rc.8"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
package/src/authority.ts
CHANGED
|
@@ -25,11 +25,13 @@ import {
|
|
|
25
25
|
storedRunRecordV2,
|
|
26
26
|
storedRunSubagentRoleV1,
|
|
27
27
|
storedRunTurnTypeV1,
|
|
28
|
+
unreadableStoredRunV1,
|
|
28
29
|
type BotNotificationIntent,
|
|
29
30
|
type BotTurnCommand,
|
|
30
31
|
type BotTurnCompletion,
|
|
31
32
|
type StoredRunCodecV1,
|
|
32
33
|
type StoredRunV1,
|
|
34
|
+
type UnreadableStoredRunV1,
|
|
33
35
|
} from "./run-records.js";
|
|
34
36
|
import {
|
|
35
37
|
completeStoredRun,
|
|
@@ -239,6 +241,14 @@ function runWasDiscardedV1(
|
|
|
239
241
|
return Boolean(run?.stopRequestedAt || run?.supersededAt);
|
|
240
242
|
}
|
|
241
243
|
|
|
244
|
+
/**
|
|
245
|
+
* One run as the display-only read boundary sees it: either the decoded
|
|
246
|
+
* record, or the bounded identity of a record that could not be decoded.
|
|
247
|
+
*/
|
|
248
|
+
export type DisplayRunReadV1<Snapshot> =
|
|
249
|
+
| { readonly readable: true; readonly run: StoredRunV1<Snapshot> }
|
|
250
|
+
| { readonly readable: false; readonly run: UnreadableStoredRunV1 };
|
|
251
|
+
|
|
242
252
|
export interface BotDurableAuthorityOptions<Snapshot> {
|
|
243
253
|
state: DurableObjectState;
|
|
244
254
|
codec: StoredRunCodecV1<Snapshot>;
|
|
@@ -1248,6 +1258,61 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1248
1258
|
return this.readRunFrom(this.ctx.storage, runId, "display");
|
|
1249
1259
|
}
|
|
1250
1260
|
|
|
1261
|
+
/**
|
|
1262
|
+
* The record alone, for display, never throwing on a record it cannot read.
|
|
1263
|
+
*
|
|
1264
|
+
* The transcript is the one reader that must survive a bad row. Execution
|
|
1265
|
+
* and recovery stay strict — they act on the record, and acting on a record
|
|
1266
|
+
* nobody can decode is how a Turn gets settled twice — but a read that only
|
|
1267
|
+
* draws the conversation owes the person the other forty Turns. An
|
|
1268
|
+
* undecodable record comes back as {@link UnreadableStoredRunV1}: the run id
|
|
1269
|
+
* from the lookup key plus whatever scraped strings are safe, which is
|
|
1270
|
+
* enough to render exactly one degraded row.
|
|
1271
|
+
*/
|
|
1272
|
+
async readRunHeaderForDisplay(
|
|
1273
|
+
runId: string,
|
|
1274
|
+
): Promise<DisplayRunReadV1<Snapshot> | undefined> {
|
|
1275
|
+
const raw = await this.ctx.storage.get<unknown>(`${RUN_PREFIX}${runId}`);
|
|
1276
|
+
if (raw === undefined) return undefined;
|
|
1277
|
+
try {
|
|
1278
|
+
return { readable: true, run: this.codec.require(raw) };
|
|
1279
|
+
} catch {
|
|
1280
|
+
return { readable: false, run: unreadableStoredRunV1(runId, raw) };
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* The journal behind a display header, degrading rather than throwing.
|
|
1286
|
+
*
|
|
1287
|
+
* Takes the header the caller already read rather than the run id: a
|
|
1288
|
+
* transcript page reads one record per candidate and hydrates only the ones
|
|
1289
|
+
* it keeps, and re-reading the record here would put that read back.
|
|
1290
|
+
*/
|
|
1291
|
+
async hydrateRunForDisplay(
|
|
1292
|
+
header: DisplayRunReadV1<Snapshot>,
|
|
1293
|
+
): Promise<DisplayRunReadV1<Snapshot>> {
|
|
1294
|
+
if (!header.readable) return header;
|
|
1295
|
+
try {
|
|
1296
|
+
return {
|
|
1297
|
+
readable: true,
|
|
1298
|
+
run: await this.hydrateRun(this.ctx.storage, header.run, "display"),
|
|
1299
|
+
};
|
|
1300
|
+
} catch {
|
|
1301
|
+
return {
|
|
1302
|
+
readable: false,
|
|
1303
|
+
run: unreadableStoredRunV1(header.run.runId, header.run),
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/** {@link readRunHeaderForDisplay} with its journal hydrated. */
|
|
1309
|
+
async readStoredRunForDisplayOrDegraded(
|
|
1310
|
+
runId: string,
|
|
1311
|
+
): Promise<DisplayRunReadV1<Snapshot> | undefined> {
|
|
1312
|
+
const header = await this.readRunHeaderForDisplay(runId);
|
|
1313
|
+
return header ? this.hydrateRunForDisplay(header) : undefined;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1251
1316
|
private async readRunFrom(
|
|
1252
1317
|
storage: SessionEventLogStorage,
|
|
1253
1318
|
runId: string,
|
|
@@ -1256,7 +1321,16 @@ export class BotDurableAuthority<Snapshot> {
|
|
|
1256
1321
|
const run = this.codec.optional(
|
|
1257
1322
|
await storage.get<unknown>(`${RUN_PREFIX}${runId}`),
|
|
1258
1323
|
);
|
|
1259
|
-
if (!run
|
|
1324
|
+
if (!run) return undefined;
|
|
1325
|
+
return this.hydrateRun(storage, run, fidelity);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
private async hydrateRun(
|
|
1329
|
+
storage: SessionEventLogStorage,
|
|
1330
|
+
run: StoredRunV1<Snapshot>,
|
|
1331
|
+
fidelity: "exact" | "display",
|
|
1332
|
+
): Promise<StoredRunV1<Snapshot>> {
|
|
1333
|
+
if (!run.eventRange) return run;
|
|
1260
1334
|
const log = new SessionEventLog(storage);
|
|
1261
1335
|
const events =
|
|
1262
1336
|
fidelity === "display"
|
package/src/run-records.ts
CHANGED
|
@@ -980,3 +980,60 @@ export interface BotTurnCompletion {
|
|
|
980
980
|
events: SessionEvent[];
|
|
981
981
|
notification?: BotNotificationIntent;
|
|
982
982
|
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* The little that can be trusted about a run record nobody can decode.
|
|
986
|
+
*
|
|
987
|
+
* A transcript read is display-only: it never resumes, settles or recovers a
|
|
988
|
+
* Turn, so it does not need the record to be valid — it needs enough to draw
|
|
989
|
+
* one row saying which Turn could not be read. The run id comes from the
|
|
990
|
+
* admission index key, which is authority; everything else is scraped from the
|
|
991
|
+
* raw value and kept only where it is plainly a safe, bounded string, so a
|
|
992
|
+
* record corrupt in any other field still yields a renderable row.
|
|
993
|
+
*/
|
|
994
|
+
export interface UnreadableStoredRunV1 {
|
|
995
|
+
readonly runId: string;
|
|
996
|
+
readonly sessionId?: string;
|
|
997
|
+
readonly acceptedAt?: string;
|
|
998
|
+
readonly input?: string;
|
|
999
|
+
readonly admission?: { readonly turnType?: TurnTypeV1 };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** Scrape a record that failed to decode down to {@link UnreadableStoredRunV1}. */
|
|
1003
|
+
export function unreadableStoredRunV1(
|
|
1004
|
+
runId: string,
|
|
1005
|
+
raw: unknown,
|
|
1006
|
+
): UnreadableStoredRunV1 {
|
|
1007
|
+
const candidate =
|
|
1008
|
+
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
1009
|
+
? (raw as Record<string, unknown>)
|
|
1010
|
+
: {};
|
|
1011
|
+
const admission =
|
|
1012
|
+
candidate.admission &&
|
|
1013
|
+
typeof candidate.admission === "object" &&
|
|
1014
|
+
!Array.isArray(candidate.admission)
|
|
1015
|
+
? (candidate.admission as Record<string, unknown>)
|
|
1016
|
+
: undefined;
|
|
1017
|
+
let turnType: TurnTypeV1 | undefined;
|
|
1018
|
+
try {
|
|
1019
|
+
if (admission?.turnType !== undefined) {
|
|
1020
|
+
turnType = decodeTurnTypeV1(admission.turnType);
|
|
1021
|
+
}
|
|
1022
|
+
} catch {
|
|
1023
|
+
turnType = undefined;
|
|
1024
|
+
}
|
|
1025
|
+
return {
|
|
1026
|
+
runId,
|
|
1027
|
+
...(boundedString(candidate.sessionId, 257)
|
|
1028
|
+
? { sessionId: candidate.sessionId }
|
|
1029
|
+
: {}),
|
|
1030
|
+
...(boundedString(candidate.acceptedAt, 64) &&
|
|
1031
|
+
Number.isFinite(Date.parse(candidate.acceptedAt))
|
|
1032
|
+
? { acceptedAt: candidate.acceptedAt }
|
|
1033
|
+
: {}),
|
|
1034
|
+
...(boundedString(candidate.input, 32_000)
|
|
1035
|
+
? { input: candidate.input }
|
|
1036
|
+
: {}),
|
|
1037
|
+
...(turnType ? { admission: { turnType } } : {}),
|
|
1038
|
+
};
|
|
1039
|
+
}
|