@frockbot/kernel-do 0.3.42 → 0.3.43

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/kernel-do",
3
- "version": "0.3.42",
3
+ "version": "0.3.43",
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.42",
16
- "@frockbot/kernel-contracts": "0.3.42",
15
+ "@frockbot/kernel-composition": "0.3.43",
16
+ "@frockbot/kernel-contracts": "0.3.43",
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?.eventRange) return 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"
@@ -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
+ }