@effect-agent/storage-cloudflare 0.1.0-beta.42 → 0.1.0-beta.45

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.
@@ -1 +1 @@
1
- {"version":3,"file":"testing.mjs","names":[],"sources":["../src/do-storage-failpoint-testing.ts"],"sourcesContent":["import { Context, Effect, Layer, Ref } from \"effect\";\n\nimport { DoStorageFailpoint, type DoStorageFailpointHandler } from \"./do-storage-failpoint.ts\";\nimport type { DoStorageFailpointLocation } from \"./errors.ts\";\n\nconst noFailpoint: DoStorageFailpointHandler = () => Effect.void;\n\n/** Test-only control for replacing the active Durable Object failpoint handler. */\nexport class DoStorageFailpointTestControl extends Context.Service<\n DoStorageFailpointTestControl,\n {\n readonly clear: Effect.Effect<void>;\n readonly setHandler: (handler: DoStorageFailpointHandler) => Effect.Effect<void>;\n }\n>()(\"@effect-agent/storage-cloudflare/DoStorageFailpointTestControl\") {\n /** Reusable test Layer with a control service backed by the same handler Ref. */\n static readonly layer = Layer.effectContext(\n Effect.gen(function* () {\n const handler = yield* Ref.make<DoStorageFailpointHandler>(noFailpoint);\n return Context.make(\n DoStorageFailpoint,\n DoStorageFailpoint.of({\n hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))),\n }),\n ).pipe(\n Context.add(\n DoStorageFailpointTestControl,\n DoStorageFailpointTestControl.of({\n clear: Ref.set(handler, noFailpoint),\n setHandler: (next) => Ref.set(handler, next),\n }),\n ),\n );\n }),\n );\n}\n\n/**\n * The DC-specific eviction failpoint mode: instead of failing typed, an armed hit evicts the\n * Durable Object through an injected `evict` thunk — in production-shaped harnesses that thunk\n * is `() => ctx.abort()`, the platform's real failure mode. `ctx.abort()` never returns (it\n * throws while destroying the in-memory instance and every in-flight implicit or explicit\n * storage transaction rolls back), so an armed hit ends the current Attempt exactly like an\n * unannounced platform eviction; DO storage — the only correctness-critical state — survives\n * for the next incarnation, which the persisted alarm wakes without any incoming request.\n *\n * The handles stay injected: this package never imports `cloudflare:workers`, so the harness\n * that owns a `DurableObjectState` supplies the thunk.\n */\nexport const evictionFailpointHandler =\n (options: {\n readonly isArmed: (location: DoStorageFailpointLocation) => Effect.Effect<boolean>;\n /** Kills the incarnation — e.g. `() => ctx.abort()`. Typed `void` because the platform\n * declares `abort` as returning, but it throws while destroying the instance. */\n readonly evict: (location: DoStorageFailpointLocation) => void;\n }): DoStorageFailpointHandler =>\n (location) =>\n options.isArmed(location).pipe(\n Effect.flatMap((armed) =>\n armed\n ? // `ctx.abort()` throws while destroying the instance; that throw surfaces as a\n // defect in the (already dying) incarnation. The defensive throw below keeps the\n // guarantee — nothing after an armed hit may observe in-memory state — even if a\n // harness supplies an evict thunk that returns.\n Effect.sync((): never => {\n options.evict(location);\n throw new Error(\n `Durable Object eviction did not interrupt execution at ${location}.`,\n );\n })\n : Effect.void,\n ),\n );\n"],"mappings":";;;AAKA,MAAM,oBAA+C,OAAO;;AAG5D,IAAa,gCAAb,MAAa,sCAAsC,QAAQ,QAMzD,CAAC,CAAC,gEAAgE,CAAC,CAAC;;CAEpE,OAAgB,QAAQ,MAAM,cAC5B,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,IAAI,KAAgC,WAAW;EACtE,OAAO,QAAQ,KACb,oBACA,mBAAmB,GAAG,EACpB,MAAM,aAAa,IAAI,IAAI,OAAO,CAAC,CAAC,KAAK,OAAO,SAAS,YAAY,QAAQ,QAAQ,CAAC,CAAC,EACzF,CAAC,CACH,CAAC,CAAC,KACA,QAAQ,IACN,+BACA,8BAA8B,GAAG;GAC/B,OAAO,IAAI,IAAI,SAAS,WAAW;GACnC,aAAa,SAAS,IAAI,IAAI,SAAS,IAAI;EAC7C,CAAC,CACH,CACF;CACF,CAAC,CACH;AACF;;;;;;;;;;;;;AAcA,MAAa,4BACV,aAMA,aACC,QAAQ,QAAQ,QAAQ,CAAC,CAAC,KACxB,OAAO,SAAS,UACd,QAKI,OAAO,WAAkB;CACvB,QAAQ,MAAM,QAAQ;CACtB,MAAM,IAAI,MACR,0DAA0D,SAAS,EACrE;AACF,CAAC,IACD,OAAO,IACb,CACF"}
1
+ {"version":3,"file":"testing.mjs","names":[],"sources":["../src/do-storage-failpoint-testing.ts"],"sourcesContent":["import { Context, Effect, Layer, Ref } from \"effect\";\n\nimport { DoStorageFailpoint, type DoStorageFailpointHandler } from \"./do-storage-failpoint.ts\";\nimport type { DoStorageFailpointLocation } from \"./errors.ts\";\n\nconst noFailpoint: DoStorageFailpointHandler = () => Effect.void;\n\n/** Test-only control for replacing the active Durable Object failpoint handler. */\nexport class DoStorageFailpointTestControl extends Context.Service<\n DoStorageFailpointTestControl,\n {\n readonly clear: Effect.Effect<void>;\n readonly setHandler: (handler: DoStorageFailpointHandler) => Effect.Effect<void>;\n }\n>()(\"@effect-agent/storage-cloudflare/DoStorageFailpointTestControl\") {\n /** Reusable test Layer with a control service backed by the same handler Ref. */\n static readonly layer = Layer.effectContext(\n Effect.gen(function* () {\n const handler = yield* Ref.make<DoStorageFailpointHandler>(noFailpoint);\n\n return Context.make(\n DoStorageFailpoint,\n DoStorageFailpoint.of({\n hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))),\n }),\n ).pipe(\n Context.add(\n DoStorageFailpointTestControl,\n DoStorageFailpointTestControl.of({\n clear: Ref.set(handler, noFailpoint),\n setHandler: (next) => Ref.set(handler, next),\n }),\n ),\n );\n }),\n );\n}\n\n/**\n * The DC-specific eviction failpoint mode: instead of failing typed, an armed hit evicts the\n * Durable Object through an injected `evict` thunk — in production-shaped harnesses that thunk\n * is `() => ctx.abort()`, the platform's real failure mode. `ctx.abort()` never returns (it\n * throws while destroying the in-memory instance and every in-flight implicit or explicit\n * storage transaction rolls back), so an armed hit ends the current Attempt exactly like an\n * unannounced platform eviction; DO storage — the only correctness-critical state — survives\n * for the next incarnation, which the persisted alarm wakes without any incoming request.\n *\n * The handles stay injected: this package never imports `cloudflare:workers`, so the harness\n * that owns a `DurableObjectState` supplies the thunk.\n */\nexport const evictionFailpointHandler =\n (options: {\n readonly isArmed: (location: DoStorageFailpointLocation) => Effect.Effect<boolean>;\n /** Kills the incarnation — e.g. `() => ctx.abort()`. Typed `void` because the platform\n * declares `abort` as returning, but it throws while destroying the instance. */\n readonly evict: (location: DoStorageFailpointLocation) => void;\n }): DoStorageFailpointHandler =>\n (location) =>\n options.isArmed(location).pipe(\n Effect.flatMap((armed) =>\n armed\n ? // `ctx.abort()` throws while destroying the instance; that throw surfaces as a\n // defect in the (already dying) incarnation. The defensive throw below keeps the\n // guarantee — nothing after an armed hit may observe in-memory state — even if a\n // harness supplies an evict thunk that returns.\n Effect.sync((): never => {\n options.evict(location);\n throw new Error(\n `Durable Object eviction did not interrupt execution at ${location}.`,\n );\n })\n : Effect.void,\n ),\n );\n"],"mappings":";;;AAKA,MAAM,oBAA+C,OAAO;;AAG5D,IAAa,gCAAb,MAAa,sCAAsC,QAAQ,QAMzD,CAAC,CAAC,gEAAgE,CAAC,CAAC;;CAEpE,OAAgB,QAAQ,MAAM,cAC5B,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,IAAI,KAAgC,WAAW;EAEtE,OAAO,QAAQ,KACb,oBACA,mBAAmB,GAAG,EACpB,MAAM,aAAa,IAAI,IAAI,OAAO,CAAC,CAAC,KAAK,OAAO,SAAS,YAAY,QAAQ,QAAQ,CAAC,CAAC,EACzF,CAAC,CACH,CAAC,CAAC,KACA,QAAQ,IACN,+BACA,8BAA8B,GAAG;GAC/B,OAAO,IAAI,IAAI,SAAS,WAAW;GACnC,aAAa,SAAS,IAAI,IAAI,SAAS,IAAI;EAC7C,CAAC,CACH,CACF;CACF,CAAC,CACH;AACF;;;;;;;;;;;;;AAcA,MAAa,4BACV,aAMA,aACC,QAAQ,QAAQ,QAAQ,CAAC,CAAC,KACxB,OAAO,SAAS,UACd,QAKI,OAAO,WAAkB;CACvB,QAAQ,MAAM,QAAQ;CACtB,MAAM,IAAI,MACR,0DAA0D,SAAS,EACrE;AACF,CAAC,IACD,OAAO,IACb,CACF"}
package/package.json CHANGED
@@ -1,52 +1 @@
1
- {
2
- "name": "@effect-agent/storage-cloudflare",
3
- "version": "0.1.0-beta.42",
4
- "exports": {
5
- ".": {
6
- "types": "./dist/index.d.mts",
7
- "default": "./dist/index.mjs"
8
- },
9
- "./testing": {
10
- "types": "./dist/testing.d.mts",
11
- "default": "./dist/testing.mjs"
12
- }
13
- },
14
- "dependencies": {
15
- "@effect-agent/core": "0.1.0-beta.42",
16
- "@effect-agent/thread": "0.1.0-beta.42",
17
- "@effect/platform-browser": "4.0.0-rc.111",
18
- "@effect/sql-sqlite-do": "4.0.0-rc.111"
19
- },
20
- "peerDependencies": {
21
- "effect": "^4.0.0-rc.111"
22
- },
23
- "description": "Durable Object SQLite storage adapters and the routed port protocol for Effect Agent on Cloudflare.",
24
- "license": "MIT",
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/danieljvdm/effect-agent.git",
28
- "directory": "packages/storage-cloudflare"
29
- },
30
- "files": [
31
- "dist",
32
- "src"
33
- ],
34
- "type": "module",
35
- "publishConfig": {
36
- "access": "public"
37
- },
38
- "scripts": {
39
- "build": "vp pack",
40
- "check": "tsc --noEmit -p tsconfig.json"
41
- },
42
- "devDependencies": {
43
- "@cloudflare/vitest-pool-workers": "0.21.3",
44
- "@cloudflare/workers-types": "5.20260825.1",
45
- "@effect-agent/testing": "0.1.0-beta.40",
46
- "@effect/vitest": "4.0.0-rc.111",
47
- "effect": "4.0.0-rc.111",
48
- "typescript": "7.0.2",
49
- "vite-plus": "0.3.0",
50
- "vitest": "4.1.11"
51
- }
52
- }
1
+ {"name":"@effect-agent/storage-cloudflare","version":"0.1.0-beta.45","dependencies":{"@effect-agent/core":"0.1.0-beta.45","@effect-agent/thread":"0.1.0-beta.45","@effect/platform-browser":"4.0.0-rc.112","@effect/sql-sqlite-do":"4.0.0-rc.112"},"devDependencies":{"@cloudflare/vitest-pool-workers":"0.21.3","@cloudflare/workers-types":"5.20260825.1","@effect-agent/testing":"0.1.0-beta.45","@effect/vitest":"4.0.0-rc.112","effect":"4.0.0-rc.112","typescript":"7.0.2","vite-plus":"0.3.0","vitest":"4.1.11"},"peerDependencies":{"effect":"^4.0.0-rc.112"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./testing":{"types":"./dist/testing.d.mts","default":"./dist/testing.mjs"}},"description":"Durable Object SQLite storage adapters and the routed port protocol for Effect Agent on Cloudflare.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/danieljvdm/effect-agent.git","directory":"packages/storage-cloudflare"},"files":["dist","src"],"type":"module","publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json"}}
package/src/do-journal.ts CHANGED
@@ -35,9 +35,11 @@ const storedTextBytes = (value: string): number => new TextEncoder().encode(valu
35
35
 
36
36
  const chunked = <A>(values: ReadonlyArray<A>, size: number): Array<ReadonlyArray<A>> => {
37
37
  const chunks: Array<ReadonlyArray<A>> = [];
38
+
38
39
  for (let index = 0; index < values.length; index += size) {
39
40
  chunks.push(values.slice(index, index + size));
40
41
  }
42
+
41
43
  return chunks;
42
44
  };
43
45
 
@@ -244,6 +246,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
244
246
  WHERE type = 'table'
245
247
  AND name = 'effect_agent_meta'
246
248
  `.pipe(Effect.mapError(storageError("read storage version table")));
249
+
247
250
  const metaTables = yield* decodeRows(
248
251
  Schema.Array(DoNameRow),
249
252
  "sqlite_master",
@@ -259,6 +262,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
259
262
  AND name LIKE 'effect_agent_%'
260
263
  ORDER BY name
261
264
  `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
265
+
262
266
  const existing = yield* decodeRows(
263
267
  Schema.Array(DoNameRow),
264
268
  "sqlite_master",
@@ -293,6 +297,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
293
297
  FROM effect_agent_meta
294
298
  WHERE key = 'storage_version'
295
299
  `.pipe(Effect.mapError(storageError("read storage version")));
300
+
296
301
  const version = yield* decodeSingleRow(
297
302
  Schema.Array(DoMetaRow),
298
303
  "effect_agent_meta",
@@ -305,6 +310,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
305
310
  // rather than being decoded incorrectly (DEPLOY-008).
306
311
  if (version.value !== String(CurrentDoStorageVersion)) {
307
312
  const actualVersion = Number.parseInt(version.value, 10);
313
+
308
314
  return yield* DoStorageCompatibilityError.make({
309
315
  actualVersion: Number.isSafeInteger(actualVersion) ? actualVersion : -1,
310
316
  supportedVersion: CurrentDoStorageVersion,
@@ -323,12 +329,14 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
323
329
  AND name IN ${sql.in([...REQUIRED_TABLES])}
324
330
  ORDER BY name
325
331
  `.pipe(Effect.mapError(storageError("verify storage tables")));
332
+
326
333
  const required = yield* decodeRows(
327
334
  Schema.Array(DoNameRow),
328
335
  "sqlite_master",
329
336
  "required_tables",
330
337
  requiredRows,
331
338
  );
339
+
332
340
  if (required.length !== REQUIRED_TABLES.length) {
333
341
  return yield* DoStorageCompatibilityError.make({
334
342
  actualVersion: CurrentDoStorageVersion,
@@ -352,6 +360,7 @@ const makeJournal = (
352
360
  value: string,
353
361
  ): Effect.Effect<void, DoValueBoundExceeded> => {
354
362
  const actualBytes = storedTextBytes(value);
363
+
355
364
  return actualBytes > maxStoredValueBytes
356
365
  ? Effect.fail(
357
366
  DoValueBoundExceeded.make({
@@ -411,12 +420,14 @@ const makeJournal = (
411
420
  FROM effect_agent_threads
412
421
  WHERE thread_id = ${threadId}
413
422
  `.pipe(Effect.mapError(storageError("read materialized thread")));
423
+
414
424
  const existing = yield* decodeRows(
415
425
  Schema.Array(ThreadRow),
416
426
  "effect_agent_threads",
417
427
  threadId,
418
428
  existingRows,
419
429
  );
430
+
420
431
  if (existing.length > 1) {
421
432
  return yield* DoStorageCorruptionError.make({
422
433
  table: "effect_agent_threads",
@@ -440,6 +451,7 @@ const makeJournal = (
440
451
  ${producerEpoch}
441
452
  )
442
453
  `.pipe(Effect.mapError(storageError("materialize thread")));
454
+
443
455
  return;
444
456
  }
445
457
  if (producerEpoch < existing[0].producer_epoch) {
@@ -471,6 +483,7 @@ const makeJournal = (
471
483
  FROM effect_agent_threads
472
484
  WHERE thread_id = ${threadId}
473
485
  `.pipe(Effect.mapError(storageError("read thread")));
486
+
474
487
  return yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
475
488
  });
476
489
 
@@ -496,9 +509,11 @@ const makeJournal = (
496
509
  (record) => checkValueBound("append canonical record", record.recordJson),
497
510
  { discard: true },
498
511
  );
512
+
499
513
  return yield* withWriteTransaction("append transaction")(
500
514
  Effect.gen(function* () {
501
515
  const recordIds = request.records.map((record) => record.recordId);
516
+
502
517
  if (new Set(recordIds).size !== recordIds.length) {
503
518
  return yield* DoAppendConflict.make({
504
519
  message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
@@ -516,6 +531,7 @@ const makeJournal = (
516
531
  FROM effect_agent_threads
517
532
  WHERE thread_id = ${request.threadId}
518
533
  `.pipe(Effect.mapError(storageError("read append tail")));
534
+
519
535
  const thread = yield* decodeSingleRow(
520
536
  Schema.Array(ThreadRow),
521
537
  "effect_agent_threads",
@@ -544,6 +560,7 @@ const makeJournal = (
544
560
  WHERE thread_id = ${request.threadId}
545
561
  AND batch_id = ${request.batchId}
546
562
  `.pipe(Effect.mapError(storageError("read idempotent batch")));
563
+
547
564
  const batches = yield* decodeRows(
548
565
  Schema.Array(BatchRow),
549
566
  "effect_agent_canonical_batches",
@@ -560,12 +577,14 @@ const makeJournal = (
560
577
  }
561
578
  if (batches.length === 1) {
562
579
  const existing = batches[0];
580
+
563
581
  if (existing.batch_digest !== request.batchDigest) {
564
582
  return yield* DoAppendConflict.make({
565
583
  message: `Batch ${request.batchId} already exists with different canonical content.`,
566
584
  reason: "batch-digest",
567
585
  });
568
586
  }
587
+
569
588
  return RawAppendResult.make({
570
589
  firstSequence: existing.first_sequence,
571
590
  lastSequence: existing.last_sequence,
@@ -597,6 +616,7 @@ const makeJournal = (
597
616
  // Chunked to respect the Durable Object platform's 100-bound-parameter statement
598
617
  // limit: a batch may carry up to 256 records.
599
618
  const existingRecords: Array<RecordRow> = [];
619
+
600
620
  for (const chunk of chunked(recordIds, MAX_BOUND_PARAMETERS - 10)) {
601
621
  const existingRecordRows = yield* sql<Record<string, unknown>>`
602
622
  SELECT
@@ -610,6 +630,7 @@ const makeJournal = (
610
630
  AND record_id IN ${sql.in([...chunk])}
611
631
  ORDER BY sequence
612
632
  `.pipe(Effect.mapError(storageError("check canonical record identities")));
633
+
613
634
  existingRecords.push(
614
635
  ...(yield* decodeRows(
615
636
  Schema.Array(RecordRow),
@@ -637,6 +658,7 @@ const makeJournal = (
637
658
  }),
638
659
  ),
639
660
  );
661
+
640
662
  const lastSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(
641
663
  firstSequence + request.records.length - 1,
642
664
  ).pipe(
@@ -728,6 +750,7 @@ const makeJournal = (
728
750
  ORDER BY sequence
729
751
  LIMIT ${request.limit}
730
752
  `.pipe(Effect.mapError(storageError("read canonical records")));
753
+
731
754
  return yield* decodeRows(
732
755
  Schema.Array(RecordRow),
733
756
  "effect_agent_canonical_records",
@@ -750,13 +773,16 @@ const makeJournal = (
750
773
  FROM effect_agent_threads
751
774
  WHERE thread_id = ${threadId}
752
775
  `.pipe(Effect.mapError(storageError("export thread")));
776
+
753
777
  const thread = yield* decodeSingleRow(
754
778
  Schema.Array(ThreadRow),
755
779
  "effect_agent_threads",
756
780
  threadId,
757
781
  threadRows,
758
782
  );
783
+
759
784
  yield* failpoint("export:after-thread-read");
785
+
760
786
  const batchRows = yield* sql<Record<string, unknown>>`
761
787
  SELECT
762
788
  thread_id,
@@ -770,6 +796,7 @@ const makeJournal = (
770
796
  WHERE thread_id = ${threadId}
771
797
  ORDER BY first_sequence
772
798
  `.pipe(Effect.mapError(storageError("export canonical batches")));
799
+
773
800
  const recordRows = yield* sql<Record<string, unknown>>`
774
801
  SELECT
775
802
  thread_id,
@@ -781,6 +808,7 @@ const makeJournal = (
781
808
  WHERE thread_id = ${threadId}
782
809
  ORDER BY sequence
783
810
  `.pipe(Effect.mapError(storageError("export canonical records")));
811
+
784
812
  const checkpointRows = yield* sql<Record<string, unknown>>`
785
813
  SELECT
786
814
  thread_id,
@@ -844,12 +872,14 @@ const makeJournal = (
844
872
  FROM effect_agent_threads
845
873
  WHERE thread_id = ${checkpoint.threadId}
846
874
  `.pipe(Effect.mapError(storageError("read checkpoint tail")));
875
+
847
876
  const thread = yield* decodeSingleRow(
848
877
  Schema.Array(ThreadRow),
849
878
  "effect_agent_threads",
850
879
  checkpoint.threadId,
851
880
  threadRows,
852
881
  );
882
+
853
883
  if (checkpoint.throughSequence > thread.tail_sequence) {
854
884
  return yield* DoCheckpointConflict.make({
855
885
  message:
@@ -868,12 +898,14 @@ const makeJournal = (
868
898
  WHERE thread_id = ${checkpoint.threadId}
869
899
  AND through_sequence = ${checkpoint.throughSequence}
870
900
  `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
901
+
871
902
  const existing = yield* decodeRows(
872
903
  Schema.Array(CheckpointRow),
873
904
  "effect_agent_checkpoints",
874
905
  `${checkpoint.threadId}/${checkpoint.throughSequence}`,
875
906
  checkpointRows,
876
907
  );
908
+
877
909
  if (existing.length > 1) {
878
910
  return yield* DoStorageCorruptionError.make({
879
911
  table: "effect_agent_checkpoints",
@@ -890,6 +922,7 @@ const makeJournal = (
890
922
  message: "A different checkpoint already exists at this canonical sequence.",
891
923
  });
892
924
  }
925
+
893
926
  return;
894
927
  }
895
928
 
@@ -926,6 +959,7 @@ const makeJournal = (
926
959
  ORDER BY through_sequence DESC
927
960
  LIMIT 1
928
961
  `.pipe(Effect.mapError(storageError("load checkpoint")));
962
+
929
963
  return yield* decodeRows(
930
964
  Schema.Array(CheckpointRow),
931
965
  "effect_agent_checkpoints",
@@ -940,12 +974,14 @@ const makeJournal = (
940
974
  ) {
941
975
  if (sequence === 0) {
942
976
  const threads = yield* getThread(threadId);
977
+
943
978
  return threads.length === 0
944
979
  ? []
945
980
  : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : undefined].filter(
946
981
  (value): value is string => value !== undefined,
947
982
  );
948
983
  }
984
+
949
985
  const rows = yield* sql<Record<string, unknown>>`
950
986
  SELECT
951
987
  thread_id,
@@ -959,12 +995,14 @@ const makeJournal = (
959
995
  WHERE thread_id = ${threadId}
960
996
  AND last_sequence = ${sequence}
961
997
  `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
998
+
962
999
  const batches = yield* decodeRows(
963
1000
  Schema.Array(BatchRow),
964
1001
  "effect_agent_canonical_batches",
965
1002
  `${threadId}/${sequence}`,
966
1003
  rows,
967
1004
  );
1005
+
968
1006
  return batches.map((batch) => batch.tail_digest);
969
1007
  });
970
1008
 
@@ -982,6 +1020,7 @@ const makeJournal = (
982
1020
  FROM effect_agent_threads
983
1021
  ORDER BY thread_id
984
1022
  `.pipe(Effect.mapError(storageError("scan threads")));
1023
+
985
1024
  const batches = yield* sql<Record<string, unknown>>`
986
1025
  SELECT
987
1026
  thread_id,
@@ -994,6 +1033,7 @@ const makeJournal = (
994
1033
  FROM effect_agent_canonical_batches
995
1034
  ORDER BY thread_id, first_sequence
996
1035
  `.pipe(Effect.mapError(storageError("scan canonical batches")));
1036
+
997
1037
  const records = yield* sql<Record<string, unknown>>`
998
1038
  SELECT
999
1039
  thread_id,
@@ -1004,6 +1044,7 @@ const makeJournal = (
1004
1044
  FROM effect_agent_canonical_records
1005
1045
  ORDER BY thread_id, sequence
1006
1046
  `.pipe(Effect.mapError(storageError("scan canonical records")));
1047
+
1007
1048
  const checkpoints = yield* sql<Record<string, unknown>>`
1008
1049
  SELECT
1009
1050
  thread_id,
@@ -1013,6 +1054,7 @@ const makeJournal = (
1013
1054
  FROM effect_agent_checkpoints
1014
1055
  ORDER BY thread_id, through_sequence
1015
1056
  `.pipe(Effect.mapError(storageError("scan checkpoints")));
1057
+
1016
1058
  return {
1017
1059
  threads: yield* decodeRows(
1018
1060
  Schema.Array(ThreadRow),