@effect-agent/storage-sqlite 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.
@@ -131,10 +131,13 @@ const parseOffset = Effect.fn("SqliteThreadStore.parseOffset")(function* (
131
131
  offset: ObservationOffset | undefined,
132
132
  ): Effect.fn.Return<CanonicalSequence, ThreadStoreError> {
133
133
  if (offset === undefined) return ZERO_CANONICAL_SEQUENCE;
134
+
134
135
  const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(
135
136
  Effect.mapError((error) => schemaStoreError("decode observation offset", error)),
136
137
  );
138
+
137
139
  const threadPrefix = `${SQLITE_OFFSET_PREFIX}${encodeURIComponent(threadId)}:`;
140
+
138
141
  if (!text.startsWith(threadPrefix)) {
139
142
  return yield* ThreadStoreError.make({
140
143
  operation: "decode observation offset",
@@ -142,12 +145,14 @@ const parseOffset = Effect.fn("SqliteThreadStore.parseOffset")(function* (
142
145
  });
143
146
  }
144
147
  const sequenceText = text.slice(threadPrefix.length);
148
+
145
149
  if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) {
146
150
  return yield* ThreadStoreError.make({
147
151
  operation: "decode observation offset",
148
152
  message: "The observation offset is malformed.",
149
153
  });
150
154
  }
155
+
151
156
  return yield* Schema.decodeUnknownEffect(CanonicalSequence)(Number(sequenceText)).pipe(
152
157
  Effect.mapError((error) => schemaStoreError("decode observation offset", error)),
153
158
  );
@@ -200,13 +205,17 @@ const decodeEnvelope = Effect.fn("SqliteThreadStore.decodeEnvelope")(function* (
200
205
  }),
201
206
  ),
202
207
  );
208
+
203
209
  const threadId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.threadId)(
204
210
  row.thread_id,
205
211
  ).pipe(Effect.mapError((error) => schemaStoreError("decode thread identity", error)));
212
+
206
213
  const offset = yield* makeOffset(threadId, row.sequence);
214
+
207
215
  const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(
208
216
  row.batch_id,
209
217
  ).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
218
+
210
219
  return CanonicalRecordEnvelope.make({
211
220
  threadId,
212
221
  batchId,
@@ -231,9 +240,11 @@ const requireThread = Effect.fn("SqliteThreadStore.requireThread")(function* (
231
240
  const rows = yield* journal
232
241
  .getThread(threadId)
233
242
  .pipe(Effect.mapError((error) => storeError("read thread", error)));
243
+
234
244
  if (rows.length === 0) {
235
245
  return yield* ThreadNotMaterialized.make({ threadId });
236
246
  }
247
+
237
248
  return rows[0];
238
249
  });
239
250
 
@@ -243,15 +254,18 @@ const tailDigestAt = Effect.fn("SqliteThreadStore.tailDigestAt")(function* (
243
254
  sequence: CanonicalSequence,
244
255
  ) {
245
256
  if (sequence === 0) return EMPTY_TAIL_DIGEST;
257
+
246
258
  const digests = yield* journal
247
259
  .getTailDigestAt(threadId, sequence)
248
260
  .pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
261
+
249
262
  if (digests.length !== 1) {
250
263
  return yield* CheckpointRejected.make({
251
264
  threadId,
252
265
  reason: "digest-mismatch",
253
266
  });
254
267
  }
268
+
255
269
  return yield* Schema.decodeUnknownEffect(Digest)(digests[0]).pipe(
256
270
  Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)),
257
271
  );
@@ -262,14 +276,17 @@ const groupByKey = <A>(
262
276
  key: (row: A) => string,
263
277
  ): ReadonlyMap<string, ReadonlyArray<A>> => {
264
278
  const grouped = new Map<string, Array<A>>();
279
+
265
280
  for (const row of rows) {
266
281
  const existing = grouped.get(key(row));
282
+
267
283
  if (existing === undefined) {
268
284
  grouped.set(key(row), [row]);
269
285
  } else {
270
286
  existing.push(row);
271
287
  }
272
288
  }
289
+
273
290
  return grouped;
274
291
  };
275
292
 
@@ -283,6 +300,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
283
300
  crypto: Crypto.Crypto,
284
301
  ) {
285
302
  const stored = yield* journal.scanStoredPayloads();
303
+
286
304
  const batches = yield* Effect.forEach(stored.batches, (batch) =>
287
305
  Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(
288
306
  Effect.map((decoded) => ({ decoded, row: batch })),
@@ -295,6 +313,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
295
313
  ),
296
314
  ),
297
315
  );
316
+
298
317
  const records = yield* Effect.forEach(stored.records, (record) =>
299
318
  Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(
300
319
  Effect.map((decoded) => ({ decoded, row: record })),
@@ -307,6 +326,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
307
326
  ),
308
327
  ),
309
328
  );
329
+
310
330
  const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) =>
311
331
  Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(
312
332
  Effect.map((decoded) => ({ decoded, row: checkpoint })),
@@ -336,6 +356,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
336
356
 
337
357
  for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
338
358
  const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
359
+
339
360
  if (
340
361
  canonicalBatch.batchId !== batchRow.batch_id ||
341
362
  batchRow.first_sequence !== expectedSequence ||
@@ -358,6 +379,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
358
379
  }),
359
380
  ),
360
381
  );
382
+
361
383
  if (batchRow.batch_digest !== digest || batchRow.tail_digest !== digest) {
362
384
  return yield* SqliteStorageCorruptionError.make({
363
385
  table: "effect_agent_canonical_batches",
@@ -367,6 +389,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
367
389
  }
368
390
 
369
391
  const batchRecords = recordsByBatch.get(batchRow.batch_id) ?? [];
392
+
370
393
  if (batchRecords.length !== canonicalBatch.records.length) {
371
394
  return yield* SqliteStorageCorruptionError.make({
372
395
  table: "effect_agent_canonical_records",
@@ -377,6 +400,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
377
400
  for (let index = 0; index < canonicalBatch.records.length; index++) {
378
401
  const expectedRecord = canonicalBatch.records[index];
379
402
  const storedRecord = batchRecords[index];
403
+
380
404
  const expectedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(
381
405
  expectedRecord,
382
406
  ).pipe(
@@ -388,6 +412,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
388
412
  }),
389
413
  ),
390
414
  );
415
+
391
416
  const storedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(
392
417
  storedRecord.decoded,
393
418
  ).pipe(
@@ -399,6 +424,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
399
424
  }),
400
425
  ),
401
426
  );
427
+
402
428
  if (
403
429
  storedRecord.row.sequence !== batchRow.first_sequence + index ||
404
430
  storedRecord.row.record_id !== expectedRecord.recordId ||
@@ -463,12 +489,14 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
463
489
  const failpoint = yield* SqliteStorageFailpoint;
464
490
  const crypto = yield* Crypto.Crypto;
465
491
  const journal = yield* initializeSqliteJournal();
492
+
466
493
  if (config.verifyOnOpen) {
467
494
  yield* decodeStartupPayloads(journal, crypto);
468
495
  }
469
496
 
470
497
  const provideCrypto = <A, E>(effect: Effect.Effect<A, E, Crypto.Crypto>) =>
471
498
  Effect.provideService(effect, Crypto.Crypto, crypto);
499
+
472
500
  const hitFailpoint = Effect.fn("SqliteThreadStore.hitFailpoint")(
473
501
  (location: SqliteStorageFailpointLocation): Effect.Effect<void, ThreadStoreError> =>
474
502
  failpoint
@@ -482,7 +510,9 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
482
510
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadMaterialization))(
483
511
  request,
484
512
  ).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
513
+
485
514
  const now = yield* Clock.currentTimeMillis;
515
+
486
516
  yield* hitFailpoint("materialize:before");
487
517
  yield* journal
488
518
  .materialize(
@@ -507,11 +537,15 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
507
537
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(
508
538
  request,
509
539
  ).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
540
+
510
541
  yield* requireThread(journal, validated.threadId);
542
+
511
543
  const tailDigest = yield* provideCrypto(
512
544
  digestCanonicalBatch(validated.expectedTailDigest, validated.batch),
513
545
  ).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
546
+
514
547
  const batchJson = yield* encodeCanonicalBatch(validated.batch);
548
+
515
549
  const rawRecords = yield* Effect.forEach(validated.batch.records, (record) =>
516
550
  encodeCanonicalRecord(record).pipe(
517
551
  Effect.map((recordJson) => ({
@@ -520,6 +554,7 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
520
554
  })),
521
555
  ),
522
556
  );
557
+
523
558
  const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
524
559
  threadId: validated.threadId,
525
560
  batchId: validated.batch.batchId,
@@ -531,7 +566,9 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
531
566
  records: rawRecords,
532
567
  tailDigest,
533
568
  }).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
569
+
534
570
  yield* hitFailpoint("append:before");
571
+
535
572
  const result = yield* journal.append(rawRequest).pipe(
536
573
  Effect.mapError((error) => {
537
574
  if (isSqliteFenceRejected(error)) {
@@ -552,6 +589,7 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
552
589
  reason: error.reason,
553
590
  });
554
591
  }
592
+
555
593
  return storeError("append canonical batch", error);
556
594
  }),
557
595
  Effect.flatMap((result) =>
@@ -560,7 +598,9 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
560
598
  ),
561
599
  ),
562
600
  );
601
+
563
602
  yield* hitFailpoint("append:after");
603
+
564
604
  return result;
565
605
  });
566
606
 
@@ -570,6 +610,7 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
570
610
  const rows = yield* journal
571
611
  .read(request)
572
612
  .pipe(Effect.mapError((error) => storeError("read canonical records", error)));
613
+
573
614
  return yield* Effect.forEach(rows, decodeEnvelope);
574
615
  });
575
616
 
@@ -577,7 +618,9 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
577
618
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadRead))(request).pipe(
578
619
  Effect.mapError((error) => schemaStoreError("validate thread read", error)),
579
620
  );
621
+
580
622
  yield* requireThread(journal, validated.threadId);
623
+
581
624
  const records = yield* loadRecords(
582
625
  RawReadRequest.make({
583
626
  threadId: validated.threadId,
@@ -585,8 +628,10 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
585
628
  limit: validated.limit,
586
629
  }),
587
630
  );
631
+
588
632
  return Stream.fromIterable(records);
589
633
  });
634
+
590
635
  const read: ThreadStore["Service"]["read"] = (request) => Stream.unwrap(readEffect(request));
591
636
 
592
637
  const observeEffect = Effect.fn("SqliteThreadStore.observe")(function* (
@@ -595,11 +640,14 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
595
640
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadObservation))(
596
641
  request,
597
642
  ).pipe(Effect.mapError((error) => schemaStoreError("validate thread observation", error)));
643
+
598
644
  yield* requireThread(journal, validated.threadId);
599
645
  const initialSequence = yield* parseOffset(validated.threadId, validated.afterOffset);
600
646
  const cursor = yield* Ref.make(initialSequence);
647
+
601
648
  const poll = Effect.fn("SqliteThreadStore.observePoll")(function* () {
602
649
  const fromSequenceExclusive = yield* Ref.get(cursor);
650
+
603
651
  const records = yield* loadRecords(
604
652
  RawReadRequest.make({
605
653
  threadId: validated.threadId,
@@ -607,15 +655,20 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
607
655
  limit: 1_024,
608
656
  }),
609
657
  );
658
+
610
659
  if (records.length === 0) {
611
660
  yield* Effect.sleep(config.observationPollInterval);
661
+
612
662
  return [];
613
663
  }
614
664
  yield* Ref.set(cursor, records[records.length - 1].sequence);
665
+
615
666
  return records;
616
667
  });
668
+
617
669
  return Stream.fromIterableEffectRepeat(poll());
618
670
  });
671
+
619
672
  const observe: ThreadStore["Service"]["observe"] = (request) =>
620
673
  Stream.unwrap(observeEffect(request));
621
674
 
@@ -624,20 +677,26 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
624
677
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadExportRequest))(
625
678
  request,
626
679
  ).pipe(Effect.mapError((error) => schemaStoreError("validate thread export", error)));
680
+
627
681
  yield* requireThread(journal, validated.threadId);
682
+
628
683
  const exported = yield* journal
629
684
  .exportThread(validated.threadId)
630
685
  .pipe(Effect.mapError((error) => storeError("export thread", error)));
686
+
631
687
  const records = yield* Effect.forEach(exported.records, decodeEnvelope);
688
+
632
689
  if (records.length > 65_536) {
633
690
  return yield* ThreadStoreError.make({
634
691
  operation: "decode thread export",
635
692
  message: "The thread exceeds the current export record limit.",
636
693
  });
637
694
  }
695
+
638
696
  const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(
639
697
  exported.thread.tail_digest,
640
698
  ).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
699
+
641
700
  return ThreadExport.make({
642
701
  format: "effect-agent/thread@1",
643
702
  threadId: validated.threadId,
@@ -654,10 +713,13 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
654
713
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadTailRequest))(
655
714
  request,
656
715
  ).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
716
+
657
717
  const thread = yield* requireThread(journal, validated.threadId);
718
+
658
719
  const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(thread.tail_digest).pipe(
659
720
  Effect.mapError((error) => schemaStoreError("decode tail digest", error)),
660
721
  );
722
+
661
723
  return ThreadTail.make({
662
724
  threadId: validated.threadId,
663
725
  tailSequence: thread.tail_sequence,
@@ -671,18 +733,22 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
671
733
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(
672
734
  request,
673
735
  ).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
736
+
674
737
  const thread = yield* requireThread(journal, validated.checkpoint.threadId);
738
+
675
739
  if (validated.checkpoint.throughSequence > thread.tail_sequence) {
676
740
  return yield* CheckpointRejected.make({
677
741
  threadId: validated.checkpoint.threadId,
678
742
  reason: "ahead-of-tail",
679
743
  });
680
744
  }
745
+
681
746
  const canonicalDigest = yield* tailDigestAt(
682
747
  journal,
683
748
  validated.checkpoint.threadId,
684
749
  validated.checkpoint.throughSequence,
685
750
  );
751
+
686
752
  if (canonicalDigest !== validated.checkpoint.tailDigest) {
687
753
  return yield* CheckpointRejected.make({
688
754
  threadId: validated.checkpoint.threadId,
@@ -690,12 +756,14 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
690
756
  });
691
757
  }
692
758
  const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
759
+
693
760
  const raw = RawCheckpoint.make({
694
761
  threadId: validated.checkpoint.threadId,
695
762
  throughSequence: validated.checkpoint.throughSequence,
696
763
  tailDigest: validated.checkpoint.tailDigest,
697
764
  checkpointJson,
698
765
  });
766
+
699
767
  yield* hitFailpoint("save-checkpoint:before");
700
768
  yield* journal.saveCheckpoint(raw).pipe(
701
769
  Effect.mapError((error) =>
@@ -716,10 +784,13 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
716
784
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(
717
785
  request,
718
786
  ).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
787
+
719
788
  const thread = yield* requireThread(journal, validated.threadId);
789
+
720
790
  const rows = yield* journal
721
791
  .loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence)
722
792
  .pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
793
+
723
794
  if (rows.length === 0) return Option.none();
724
795
  if (rows.length !== 1) {
725
796
  return yield* ThreadStoreError.make({
@@ -728,17 +799,20 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
728
799
  });
729
800
  }
730
801
  const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
802
+
731
803
  const canonicalDigest = yield* tailDigestAt(
732
804
  journal,
733
805
  checkpoint.threadId,
734
806
  checkpoint.throughSequence,
735
807
  );
808
+
736
809
  if (canonicalDigest !== checkpoint.tailDigest) {
737
810
  return yield* CheckpointRejected.make({
738
811
  threadId: checkpoint.threadId,
739
812
  reason: "digest-mismatch",
740
813
  });
741
814
  }
815
+
742
816
  return Option.some(checkpoint);
743
817
  },
744
818
  );