@yielded/agent-storage-cloudflare 0.1.0-beta.167 → 0.1.0-beta.169

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.
Files changed (71) hide show
  1. package/dist/DoMemoryStore.mjs +1 -1
  2. package/dist/DoMessageDeliveryStore.d.mts +1 -1
  3. package/dist/DoMessageDeliveryStore.mjs +3 -2
  4. package/dist/DoMessageDeliveryStore.mjs.map +1 -1
  5. package/dist/DoScheduleStore.mjs +16 -37
  6. package/dist/DoScheduleStore.mjs.map +1 -1
  7. package/dist/DoStorageConfig.d.mts +2 -10
  8. package/dist/DoStorageConfig.mjs +2 -11
  9. package/dist/DoStorageConfig.mjs.map +1 -1
  10. package/dist/{DoStorageError-CGuAfSwF.d.mts → DoStorageError-CxjMnzUA.d.mts} +7 -18
  11. package/dist/DoStorageError.d.mts +2 -2
  12. package/dist/DoStorageError.mjs +14 -30
  13. package/dist/DoStorageError.mjs.map +1 -1
  14. package/dist/DoStorageFailpoint.d.mts +1 -1
  15. package/dist/DoStorageFailpointTesting.d.mts +1 -1
  16. package/dist/{DoStorageVersion-iNSyi3cJ.d.mts → DoStorageVersion-DTFoLglB.d.mts} +3 -4
  17. package/dist/DoStorageVersion.d.mts +1 -1
  18. package/dist/DoStorageVersion.mjs +2 -2
  19. package/dist/DoStorageVersion.mjs.map +1 -1
  20. package/dist/DoSubmissionLedger.d.mts +7 -5
  21. package/dist/DoSubmissionLedger.mjs +378 -528
  22. package/dist/DoSubmissionLedger.mjs.map +1 -1
  23. package/dist/DoSubscriptionStore.mjs +5 -24
  24. package/dist/DoSubscriptionStore.mjs.map +1 -1
  25. package/dist/{DoThreadStore-D_83OjCH.d.mts → DoThreadStore-Bd4fTns3.d.mts} +17 -18
  26. package/dist/DoThreadStore-CwBvTLoq.mjs +468 -0
  27. package/dist/DoThreadStore-CwBvTLoq.mjs.map +1 -0
  28. package/dist/DoThreadStore.d.mts +2 -2
  29. package/dist/DoThreadStore.mjs +4 -529
  30. package/dist/MemoryProtocol.mjs +3 -3
  31. package/dist/MemoryProtocol.mjs.map +1 -1
  32. package/dist/PortProtocol.d.mts +11727 -3410
  33. package/dist/PortProtocol.mjs +103 -15
  34. package/dist/PortProtocol.mjs.map +1 -1
  35. package/dist/PortRouting.d.mts +14 -9
  36. package/dist/PortRouting.mjs +155 -29
  37. package/dist/PortRouting.mjs.map +1 -1
  38. package/dist/do-journal-Cawnsetj.mjs +761 -0
  39. package/dist/do-journal-Cawnsetj.mjs.map +1 -0
  40. package/dist/index.d.mts +3 -3
  41. package/dist/index.mjs +1 -1
  42. package/dist/migrations-D_EP3KYf.mjs +148 -0
  43. package/dist/migrations-D_EP3KYf.mjs.map +1 -0
  44. package/dist/{owned-state-DHm2aRXx.mjs → owned-state-BHzUi0eb.mjs} +50 -19
  45. package/dist/owned-state-BHzUi0eb.mjs.map +1 -0
  46. package/package.json +1 -1
  47. package/src/DoMessageDeliveryStore.ts +2 -1
  48. package/src/DoScheduleStore.ts +19 -59
  49. package/src/DoStorageConfig.ts +2 -11
  50. package/src/DoStorageError.ts +3 -26
  51. package/src/DoStorageVersion.ts +1 -1
  52. package/src/DoSubmissionLedger.ts +1806 -2030
  53. package/src/DoSubscriptionStore.ts +5 -47
  54. package/src/DoThreadStore.ts +276 -622
  55. package/src/MemoryProtocol.ts +3 -3
  56. package/src/PortProtocol.ts +198 -12
  57. package/src/PortRouting.ts +527 -108
  58. package/src/internal/canonical-append.ts +15 -0
  59. package/src/internal/do-journal.ts +548 -1440
  60. package/src/internal/migrations.ts +184 -287
  61. package/src/internal/owned-state.ts +81 -30
  62. package/src/internal/prompt-checkpoint.ts +158 -0
  63. package/src/internal/storage-span.ts +2 -5
  64. package/dist/DoThreadStore.mjs.map +0 -1
  65. package/dist/do-journal-DUhjZkOf.mjs +0 -1352
  66. package/dist/do-journal-DUhjZkOf.mjs.map +0 -1
  67. package/dist/migrations-DtKz_Gxr.mjs +0 -306
  68. package/dist/migrations-DtKz_Gxr.mjs.map +0 -1
  69. package/dist/owned-state-DHm2aRXx.mjs.map +0 -1
  70. package/src/internal/message-delivery-schema.ts +0 -24
  71. package/src/internal/recovery-checkpoint-schema.ts +0 -17
@@ -1,58 +1,46 @@
1
1
  import { BrowserCrypto } from "@effect/platform-browser";
2
2
  import { SqliteClient } from "@effect/sql-sqlite-do";
3
+ import { makeSqlThreadImport } from "@yielded/agent-storage-sql/sql-thread-import";
3
4
  import {
4
5
  makeSelectedReads,
5
6
  SelectedReadOwner,
6
7
  } from "@yielded/agent-storage-sql/sql-thread-native-reads";
7
- import { digestCanonicalBatch, EMPTY_TAIL_DIGEST } from "@yielded/agent/digest";
8
+ import { ExportRecord } from "@yielded/agent/record-format";
8
9
  import {
9
- CanonicalBatch,
10
- CanonicalRecord,
11
10
  CanonicalRecordEnvelope,
12
11
  CanonicalSequence,
13
- Digest,
14
12
  ObservationOffset,
15
13
  } from "@yielded/agent/records";
14
+ import { StorageOwner } from "@yielded/agent/run-storage";
16
15
  import { SqlStorageOwner } from "@yielded/agent/sql-memory-store";
17
- import { DEFAULT_OWNERSHIP_LEASE_DURATION } from "@yielded/agent/submission-ledger";
16
+ import { MAX_ARCHIVE_RANGE_PAGE } from "@yielded/agent/thread-archive-range";
17
+ import { ThreadImport } from "@yielded/agent/thread-import";
18
18
  import {
19
+ type ThreadExportRequest,
19
20
  AppendConflict,
20
21
  AppendResult,
21
22
  CheckpointRejected,
22
23
  ThreadCheckpoint,
23
- ThreadExport,
24
- ThreadExportRequest,
25
24
  ThreadMaterialization,
26
25
  ThreadNotMaterialized,
27
26
  ThreadObservation,
28
27
  ThreadReadRequest,
29
28
  ThreadStore,
29
+ ThreadReader,
30
30
  type ThreadCheckpoints,
31
31
  ThreadStoreError,
32
32
  ThreadStoreDiagnostic,
33
33
  ThreadTail,
34
34
  ThreadTailRequest,
35
- FenceRejected,
36
- FencedAppendRequest,
35
+ type AppendRequest,
36
+ PreparedAppend,
37
37
  LoadCheckpointRequest,
38
38
  SaveCheckpointRequest,
39
- SaveRecoveryCheckpointRequest,
40
- MAX_THREAD_EXPORT_RECORDS,
41
- type ThreadRecoveryCheckpoints,
42
39
  } from "@yielded/agent/thread-store";
43
- import {
44
- Clock,
45
- Context,
46
- Crypto,
47
- Duration,
48
- Effect,
49
- Layer,
50
- Option,
51
- Ref,
52
- Schema,
53
- Stream,
54
- } from "effect";
40
+ import { checkExportSource } from "@yielded/agent/thread-transfer";
41
+ import { Clock, Context, Crypto, Effect, Layer, Option, Ref, Schema, Stream } from "effect";
55
42
  import * as SqlClientService from "effect/sql/SqlClient";
43
+ import { isSqlError } from "effect/sql/SqlError";
56
44
 
57
45
  import {
58
46
  DEFAULT_MAX_STORED_VALUE_BYTES,
@@ -63,20 +51,21 @@ import {
63
51
  type DoStorageCompatibilityError,
64
52
  DoAppendConflict,
65
53
  DoCheckpointConflict,
66
- DoFenceRejected,
67
54
  type DoStorageFailpointLocation,
68
55
  DoStorageCorruptionError,
69
56
  DoStorageError,
70
57
  } from "./DoStorageError.ts";
71
58
  import { DoStorageFailpoint, type DoStorageFailpointHandler } from "./DoStorageFailpoint.ts";
59
+ import { DO_OFFSET_PREFIX, prepareCanonicalAppend } from "./internal/canonical-append.ts";
72
60
  import {
73
61
  initializeDoJournal,
74
- RawAppendRequest,
75
62
  RawCheckpoint,
76
63
  RawReadRequest,
77
64
  type DoJournal,
78
65
  } from "./internal/do-journal.ts";
66
+ import { readDoStorageHeader } from "./internal/migrations.ts";
79
67
  import { invalidateOwnedState, ownedState } from "./internal/owned-state.ts";
68
+ import { makePromptCheckpoints, promptCheckpointPrefix } from "./internal/prompt-checkpoint.ts";
80
69
  import { isAppendContention, withStorageSpan } from "./internal/storage-span.ts";
81
70
 
82
71
  /**
@@ -87,20 +76,15 @@ import { isAppendContention, withStorageSpan } from "./internal/storage-span.ts"
87
76
  export interface DoStorageOptions {
88
77
  readonly storage: DurableObjectStorage;
89
78
  readonly observationPollInterval?: number | undefined;
90
- /**
91
- * Submission ownership lease duration in milliseconds (D5). Defaults to
92
- * `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@yielded/agent/submission-ledger`.
93
- */
94
- readonly ownershipLeaseDuration?: number | undefined;
95
79
  /**
96
80
  * Maximum bytes for any single stored value; must stay under the platform's 2 MB
97
81
  * per-value limit. Defaults to `DEFAULT_MAX_STORED_VALUE_BYTES`.
98
82
  */
99
83
  readonly maxStoredValueBytes?: number | undefined;
100
84
  /**
101
- * Re-verify every stored payload and digest chain while opening the store. Defaults to
102
- * off: per-operation Schema decoding and the digest chain already fail clearly on corrupt
103
- * rows without scanning the whole database on every open.
85
+ * Audit journal payloads, ownership, and batch coverage in bounded pages on open, including
86
+ * orphan rows and rows outside declared ranges. Defaults to false. Full reads validate
87
+ * canonical records; readPrompt trusts write-time validation of the omitted fields.
104
88
  */
105
89
  readonly verifyOnOpen?: boolean | undefined;
106
90
  readonly failpoint?: DoStorageFailpointHandler | undefined;
@@ -112,16 +96,12 @@ export type DoStorageInitializationError =
112
96
  | DoStorageError;
113
97
 
114
98
  const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
115
- const DO_OFFSET_PREFIX = "effect-agent-do@1:";
116
99
  const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
117
- const isDigest = Schema.is(Digest);
118
- const isDoFenceRejected = Schema.is(DoFenceRejected);
119
100
  const isDoAppendConflict = Schema.is(DoAppendConflict);
120
101
  const isDoCheckpointConflict = Schema.is(DoCheckpointConflict);
121
102
  // Reuse the same AST/parser across reads. Reconstructing a JSON codec per row
122
103
  // defeats Schema's parser cache and repeatedly walks the canonical union.
123
- const CanonicalRecordJson = Schema.fromJsonString(CanonicalRecord);
124
- const CanonicalBatchJson = Schema.fromJsonString(CanonicalBatch);
104
+ const CanonicalRecordJson = Schema.fromJsonString(ExportRecord);
125
105
  const ThreadCheckpointJson = Schema.fromJsonString(ThreadCheckpoint);
126
106
  const decodeCanonicalRecord = Schema.decodeEffect(CanonicalRecordJson);
127
107
 
@@ -147,11 +127,11 @@ const schemaStoreError = (operation: string, error: Schema.SchemaError) =>
147
127
  }),
148
128
  });
149
129
 
150
- const makeOffset = Effect.fn(function* (
130
+ const makeOffset = (
151
131
  threadId: ThreadMaterialization["threadId"],
152
132
  sequence: number,
153
- ): Effect.fn.Return<ObservationOffset, ThreadStoreError> {
154
- return yield* Schema.decodeEffect(CanonicalSequence)(sequence).pipe(
133
+ ): Effect.Effect<ObservationOffset, ThreadStoreError> =>
134
+ Schema.decodeEffect(CanonicalSequence)(sequence).pipe(
155
135
  Effect.flatMap((validatedSequence) =>
156
136
  Schema.decodeEffect(ObservationOffset)(
157
137
  `${DO_OFFSET_PREFIX}${encodeURIComponent(threadId)}:${validatedSequence}`,
@@ -159,9 +139,8 @@ const makeOffset = Effect.fn(function* (
159
139
  ),
160
140
  Effect.mapError((error) => schemaStoreError("encode observation offset", error)),
161
141
  );
162
- });
163
142
 
164
- const parseOffset = Effect.fn(function* (
143
+ const parseOffset = Effect.fnUntraced(function* (
165
144
  threadId: ThreadMaterialization["threadId"],
166
145
  offset: ObservationOffset | undefined,
167
146
  ): Effect.fn.Return<CanonicalSequence, ThreadStoreError> {
@@ -193,36 +172,10 @@ const parseOffset = Effect.fn(function* (
193
172
  );
194
173
  });
195
174
 
196
- const mapFence = (threadId: ThreadMaterialization["threadId"], error: DoFenceRejected) =>
197
- FenceRejected.make({
198
- threadId,
199
- actualEpoch: error.actualEpoch,
200
- attemptedEpoch: error.producerEpoch,
201
- });
202
-
203
- const encodeCanonicalRecord = Effect.fn(function* (
204
- record: CanonicalRecord,
205
- ): Effect.fn.Return<string, ThreadStoreError> {
206
- return yield* Schema.encodeEffect(CanonicalRecordJson)(record).pipe(
207
- Effect.mapError((error) => schemaStoreError("encode canonical record", error)),
208
- );
209
- });
210
-
211
- const encodeCanonicalBatch = Effect.fn(function* (
212
- batch: CanonicalBatch,
213
- ): Effect.fn.Return<string, ThreadStoreError> {
214
- return yield* Schema.encodeEffect(CanonicalBatchJson)(batch).pipe(
215
- Effect.mapError((error) => schemaStoreError("encode canonical batch", error)),
216
- );
217
- });
218
-
219
- const encodeCheckpoint = Effect.fn(function* (
220
- checkpoint: ThreadCheckpoint,
221
- ): Effect.fn.Return<string, ThreadStoreError> {
222
- return yield* Schema.encodeEffect(ThreadCheckpointJson)(checkpoint).pipe(
175
+ const encodeCheckpoint = (checkpoint: ThreadCheckpoint): Effect.Effect<string, ThreadStoreError> =>
176
+ Schema.encodeEffect(ThreadCheckpointJson)(checkpoint).pipe(
223
177
  Effect.mapError((error) => schemaStoreError("encode checkpoint", error)),
224
178
  );
225
- });
226
179
 
227
180
  const envelopes = new WeakMap<object, CanonicalRecordEnvelope>();
228
181
 
@@ -271,28 +224,24 @@ const decodeEnvelope = Effect.fnUntraced(function* (row: {
271
224
  row.batch_id,
272
225
  ).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
273
226
 
274
- const envelope = CanonicalRecordEnvelope.make({
275
- threadId,
276
- batchId,
277
- sequence: row.sequence,
278
- offset,
279
- record,
280
- });
227
+ const envelope = new CanonicalRecordEnvelope(
228
+ { threadId, batchId, sequence: row.sequence, offset, record },
229
+ { disableChecks: true },
230
+ );
281
231
 
282
232
  envelopes.set(row, envelope);
283
233
 
284
234
  return envelope;
285
235
  });
286
236
 
287
- const decodeCheckpoint = Effect.fn(function* (
237
+ const decodeCheckpoint = (
288
238
  checkpointJson: string,
289
- ): Effect.fn.Return<ThreadCheckpoint, ThreadStoreError> {
290
- return yield* Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpointJson).pipe(
239
+ ): Effect.Effect<ThreadCheckpoint, ThreadStoreError> =>
240
+ Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpointJson).pipe(
291
241
  Effect.mapError((error) => schemaStoreError("decode checkpoint", error)),
292
242
  );
293
- });
294
243
 
295
- const requireThread = Effect.fn("DoThreadStore.requireThread")(function* (
244
+ const requireThread = Effect.fnUntraced(function* (
296
245
  journal: DoJournal,
297
246
  threadId: ThreadMaterialization["threadId"],
298
247
  ) {
@@ -307,243 +256,40 @@ const requireThread = Effect.fn("DoThreadStore.requireThread")(function* (
307
256
  return rows[0];
308
257
  });
309
258
 
310
- const tailDigestAt = Effect.fn("DoThreadStore.tailDigestAt")(function* (
259
+ const requireBatchBoundary = Effect.fnUntraced(function* (
311
260
  journal: DoJournal,
312
261
  threadId: ThreadMaterialization["threadId"],
313
262
  sequence: CanonicalSequence,
314
263
  ) {
315
- if (sequence === 0) return EMPTY_TAIL_DIGEST;
264
+ const valid = yield* journal
265
+ .isBatchBoundary(threadId, sequence)
266
+ .pipe(Effect.mapError((error) => storeError("read checkpoint boundary", error)));
316
267
 
317
- const digests = yield* journal
318
- .getTailDigestAt(threadId, sequence)
319
- .pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
320
-
321
- if (digests.length !== 1) {
268
+ if (!valid) {
322
269
  return yield* CheckpointRejected.make({
323
270
  threadId,
324
- reason: "digest-mismatch",
271
+ reason: "not-batch-boundary",
325
272
  });
326
273
  }
327
-
328
- return yield* Schema.decodeEffect(Digest)(digests[0]).pipe(
329
- Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)),
330
- );
331
274
  });
332
275
 
333
- const groupByKey = <A>(
334
- rows: ReadonlyArray<A>,
335
- key: (row: A) => string,
336
- ): ReadonlyMap<string, ReadonlyArray<A>> => {
337
- const grouped = new Map<string, Array<A>>();
276
+ /** Audit excluded rows before paging owners and bounded ranges; never collect Thread payloads. */
277
+ const decodeStartupPayloads = Effect.fnUntraced(function* (journal: DoJournal) {
278
+ yield* journal.archiveRanges.verifyCoverage;
279
+ let afterThreadId: string | undefined;
338
280
 
339
- for (const row of rows) {
340
- const existing = grouped.get(key(row));
281
+ while (true) {
282
+ const owners = yield* journal.archiveRanges.threadPage(afterThreadId);
341
283
 
342
- if (existing === undefined) {
343
- grouped.set(key(row), [row]);
344
- } else {
345
- existing.push(row);
284
+ for (const owner of owners) {
285
+ yield* journal.archiveRanges.verifyThread(owner.thread_id);
286
+ afterThreadId = owner.thread_id;
346
287
  }
347
- }
348
-
349
- return grouped;
350
- };
351
-
352
- /**
353
- * Opt-in integrity audit (`verifyOnOpen`) of canonical payloads, their digest chains and generic
354
- * projection checkpoints. Disposable recovery checkpoints are validated when loaded. Routine opens
355
- * skip this scan: per-operation Schema decoding fails clearly on corrupt canonical rows.
356
- */
357
- const decodeStartupPayloads = Effect.fn("DoThreadStore.decodeStartupPayloads")(function* (
358
- journal: DoJournal,
359
- crypto: Crypto.Crypto,
360
- ) {
361
- const stored = yield* journal.scanStoredPayloads();
362
-
363
- const batches = yield* Effect.forEach(stored.batches, (batch) =>
364
- Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(
365
- Effect.map((decoded) => ({ decoded, row: batch })),
366
- Effect.mapError((error) =>
367
- DoStorageCorruptionError.make({
368
- table: "effect_agent_canonical_batches",
369
- rowKey: `${batch.thread_id}/${batch.batch_id}`,
370
- message: error.message,
371
- }),
372
- ),
373
- ),
374
- );
375
-
376
- const records = yield* Effect.forEach(stored.records, (record) =>
377
- Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(
378
- Effect.map((decoded) => ({ decoded, row: record })),
379
- Effect.mapError((error) =>
380
- DoStorageCorruptionError.make({
381
- table: "effect_agent_canonical_records",
382
- rowKey: `${record.thread_id}/${record.sequence}`,
383
- message: error.message,
384
- }),
385
- ),
386
- ),
387
- );
388
-
389
- const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) =>
390
- Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(
391
- Effect.map((decoded) => ({ decoded, row: checkpoint })),
392
- Effect.mapError((error) =>
393
- DoStorageCorruptionError.make({
394
- table: "effect_agent_checkpoints",
395
- rowKey: `${checkpoint.thread_id}/${checkpoint.through_sequence}`,
396
- message: error.message,
397
- }),
398
- ),
399
- ),
400
- );
401
-
402
- const batchesByThread = groupByKey(batches, ({ row }) => row.thread_id);
403
- const recordsByThread = groupByKey(records, ({ row }) => row.thread_id);
404
- const checkpointsByThread = groupByKey(checkpoints, ({ row }) => row.thread_id);
405
- const materializedIds = new Set(stored.threads.map((thread) => thread.thread_id));
406
-
407
- for (const thread of stored.threads) {
408
- const threadBatches = batchesByThread.get(thread.thread_id) ?? [];
409
- const threadRecords = recordsByThread.get(thread.thread_id) ?? [];
410
- const threadCheckpoints = checkpointsByThread.get(thread.thread_id) ?? [];
411
- const recordsByBatch = groupByKey(threadRecords, ({ row }) => row.batch_id);
412
- let previousDigest = EMPTY_TAIL_DIGEST;
413
- let expectedSequence = 1;
414
- const tailDigests = new Map<number, string>([[0, EMPTY_TAIL_DIGEST]]);
415
-
416
- for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
417
- const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
418
-
419
- if (
420
- canonicalBatch.batchId !== batchRow.batch_id ||
421
- batchRow.first_sequence !== expectedSequence ||
422
- batchRow.last_sequence !== batchRow.first_sequence + canonicalBatch.records.length - 1
423
- ) {
424
- return yield* DoStorageCorruptionError.make({
425
- table: "effect_agent_canonical_batches",
426
- rowKey: key,
427
- message: "Canonical batch identity, sequence, or record count is inconsistent.",
428
- });
429
- }
430
-
431
- const digest = yield* digestCanonicalBatch(previousDigest, canonicalBatch).pipe(
432
- Effect.provideService(Crypto.Crypto, crypto),
433
- Effect.mapError((error) =>
434
- DoStorageCorruptionError.make({
435
- table: "effect_agent_canonical_batches",
436
- rowKey: key,
437
- message: error.message,
438
- }),
439
- ),
440
- );
441
-
442
- if (batchRow.batch_digest !== digest || batchRow.tail_digest !== digest) {
443
- return yield* DoStorageCorruptionError.make({
444
- table: "effect_agent_canonical_batches",
445
- rowKey: key,
446
- message: "Canonical batch digest does not match its decoded content and prior tail.",
447
- });
448
- }
449
-
450
- const batchRecords = recordsByBatch.get(batchRow.batch_id) ?? [];
451
-
452
- if (batchRecords.length !== canonicalBatch.records.length) {
453
- return yield* DoStorageCorruptionError.make({
454
- table: "effect_agent_canonical_records",
455
- rowKey: key,
456
- message: "Canonical batch and record-table counts differ.",
457
- });
458
- }
459
- for (let index = 0; index < canonicalBatch.records.length; index++) {
460
- const expectedRecord = canonicalBatch.records[index];
461
- const storedRecord = batchRecords[index];
462
-
463
- const expectedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(
464
- expectedRecord,
465
- ).pipe(
466
- Effect.mapError((error) =>
467
- DoStorageCorruptionError.make({
468
- table: "effect_agent_canonical_batches",
469
- rowKey: key,
470
- message: error.message,
471
- }),
472
- ),
473
- );
474
-
475
- const storedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(
476
- storedRecord.decoded,
477
- ).pipe(
478
- Effect.mapError((error) =>
479
- DoStorageCorruptionError.make({
480
- table: "effect_agent_canonical_records",
481
- rowKey: `${key}/${storedRecord.row.sequence}`,
482
- message: error.message,
483
- }),
484
- ),
485
- );
486
-
487
- if (
488
- storedRecord.row.sequence !== batchRow.first_sequence + index ||
489
- storedRecord.row.record_id !== expectedRecord.recordId ||
490
- expectedJson !== storedJson
491
- ) {
492
- return yield* DoStorageCorruptionError.make({
493
- table: "effect_agent_canonical_records",
494
- rowKey: `${key}/${storedRecord.row.sequence}`,
495
- message: "Canonical record identity, sequence, or payload differs from its batch.",
496
- });
497
- }
498
- }
499
-
500
- previousDigest = digest;
501
- expectedSequence = batchRow.last_sequence + 1;
502
- tailDigests.set(batchRow.last_sequence, digest);
503
- }
504
-
505
- if (
506
- threadRecords.length !== thread.tail_sequence ||
507
- thread.tail_sequence !== expectedSequence - 1 ||
508
- thread.tail_digest !== previousDigest
509
- ) {
510
- return yield* DoStorageCorruptionError.make({
511
- table: "effect_agent_threads",
512
- rowKey: thread.thread_id,
513
- message: "Thread tail does not match its canonical batch chain.",
514
- });
515
- }
516
-
517
- for (const checkpoint of threadCheckpoints) {
518
- if (
519
- checkpoint.decoded.threadId !== thread.thread_id ||
520
- checkpoint.decoded.throughSequence !== checkpoint.row.through_sequence ||
521
- checkpoint.decoded.tailDigest !== checkpoint.row.tail_digest ||
522
- tailDigests.get(checkpoint.row.through_sequence) !== checkpoint.row.tail_digest
523
- ) {
524
- return yield* DoStorageCorruptionError.make({
525
- table: "effect_agent_checkpoints",
526
- rowKey: `${thread.thread_id}/${checkpoint.row.through_sequence}`,
527
- message: "Checkpoint identity or digest is not bound to a canonical batch tail.",
528
- });
529
- }
530
- }
531
- }
532
-
533
- if (
534
- batches.some(({ row }) => !materializedIds.has(row.thread_id)) ||
535
- records.some(({ row }) => !materializedIds.has(row.thread_id)) ||
536
- checkpoints.some(({ row }) => !materializedIds.has(row.thread_id))
537
- ) {
538
- return yield* DoStorageCorruptionError.make({
539
- table: "effect_agent_threads",
540
- rowKey: "startup_scan",
541
- message: "Canonical rows exist without a materialized Thread.",
542
- });
288
+ if (owners.length < MAX_ARCHIVE_RANGE_PAGE) break;
543
289
  }
544
290
  });
545
291
 
546
- const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
292
+ const makeServices = Effect.fnUntraced(function* () {
547
293
  const config = yield* DoStorageConfig;
548
294
  const failpoint = yield* DoStorageFailpoint;
549
295
  const sql = yield* SqlClientService.SqlClient;
@@ -551,113 +297,78 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
551
297
  const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
552
298
 
553
299
  if (config.verifyOnOpen) {
554
- yield* decodeStartupPayloads(journal, crypto);
300
+ yield* journal.state.read(decodeStartupPayloads(journal)).pipe(
301
+ Effect.mapError((cause) =>
302
+ DoStorageCorruptionError.make({
303
+ table: "effect_agent_journal_ranges",
304
+ rowKey: "verification",
305
+ message: cause.message,
306
+ }),
307
+ ),
308
+ );
555
309
  }
556
310
 
557
- const provideCrypto = <A, E>(effect: Effect.Effect<A, E, Crypto.Crypto>) =>
558
- Effect.provideService(effect, Crypto.Crypto, crypto);
311
+ const hitFailpoint = (
312
+ location: DoStorageFailpointLocation,
313
+ ): Effect.Effect<void, ThreadStoreError> =>
314
+ failpoint
315
+ .hit(location)
316
+ .pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error)));
559
317
 
560
- const hitFailpoint = Effect.fn(
561
- (location: DoStorageFailpointLocation): Effect.Effect<void, ThreadStoreError> =>
562
- failpoint
563
- .hit(location)
564
- .pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))),
565
- );
318
+ const materialize: ThreadStore["Service"]["materialize"] = Effect.fnUntraced(function* (
319
+ request: ThreadMaterialization,
320
+ ) {
321
+ const validated = yield* Schema.decodeEffect(Schema.toType(ThreadMaterialization))(
322
+ request,
323
+ ).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
566
324
 
567
- const materialize: ThreadStore["Service"]["materialize"] = Effect.fnUntraced(
568
- function* (request: ThreadMaterialization) {
569
- const validated = yield* Schema.decodeEffect(Schema.toType(ThreadMaterialization))(
570
- request,
571
- ).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
572
-
573
- const now = yield* Clock.currentTimeMillis;
574
-
575
- yield* hitFailpoint("materialize:before");
576
- yield* journal
577
- .materialize(
578
- validated.threadId,
579
- new Date(now).toISOString(),
580
- EMPTY_TAIL_DIGEST,
581
- validated.producerEpoch,
582
- )
583
- .pipe(
584
- Effect.mapError((error) =>
585
- error._tag === "DoFenceRejected"
586
- ? mapFence(validated.threadId, error)
587
- : storeError("materialize thread", error),
588
- ),
589
- );
590
- yield* hitFailpoint("materialize:after");
591
- },
592
- withStorageSpan("DoThreadStore.materialize", (error) => error._tag === "FenceRejected"),
593
- );
325
+ const now = yield* Clock.currentTimeMillis;
594
326
 
595
- const append: ThreadStore["Service"]["append"] = Effect.fnUntraced(
596
- function* (request: FencedAppendRequest) {
597
- const validated = yield* Schema.decodeEffect(Schema.toType(FencedAppendRequest))(
598
- request,
599
- ).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
327
+ yield* hitFailpoint("materialize:before");
328
+ yield* journal
329
+ .materialize(validated.threadId, new Date(now).toISOString())
330
+ .pipe(Effect.mapError((error) => storeError("materialize thread", error)));
331
+ yield* hitFailpoint("materialize:after");
332
+ }, withStorageSpan("DoThreadStore.materialize"));
600
333
 
334
+ const append: ThreadStore["Service"]["append"] = Effect.fnUntraced(
335
+ function* (request: AppendRequest) {
336
+ const validated = yield* PreparedAppend.capture(request);
601
337
  const observed = yield* requireThread(journal, validated.threadId);
602
338
 
603
- const tailDigest = yield* provideCrypto(
604
- digestCanonicalBatch(validated.expectedTailDigest, validated.batch),
605
- ).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
606
-
607
- const batchJson = yield* encodeCanonicalBatch(validated.batch);
608
-
609
- const rawRecords = yield* Effect.forEach(validated.batch.records, (record) =>
610
- encodeCanonicalRecord(record).pipe(
611
- Effect.map((recordJson) => ({
612
- recordId: record.recordId,
613
- recordJson,
614
- })),
615
- ),
339
+ const { raw: rawRequest } = yield* prepareCanonicalAppend(validated).pipe(
340
+ Effect.provideService(Crypto.Crypto, crypto),
616
341
  );
617
342
 
618
- const rawRequest = yield* Schema.decodeEffect(RawAppendRequest)({
619
- threadId: validated.threadId,
620
- batchId: validated.batch.batchId,
621
- batchDigest: tailDigest,
622
- batchJson,
623
- expectedTailSequence: validated.expectedTailSequence,
624
- expectedTailDigest: validated.expectedTailDigest,
625
- producerEpoch: validated.producerEpoch,
626
- records: rawRecords,
627
- tailDigest,
628
- }).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
629
-
630
343
  yield* hitFailpoint("append:before");
631
344
 
632
- const result = yield* journal.append(rawRequest, observed).pipe(
633
- Effect.mapError((error) => {
634
- if (isDoFenceRejected(error)) {
635
- return mapFence(validated.threadId, error);
636
- }
637
- if (isDoAppendConflict(error)) {
638
- return error.actualTailSequence !== undefined && isDigest(error.actualTailDigest)
639
- ? AppendConflict.make({
640
- threadId: validated.threadId,
641
- batchId: validated.batch.batchId,
642
- reason: error.reason,
643
- actualTailSequence: error.actualTailSequence,
644
- actualTailDigest: error.actualTailDigest,
645
- })
646
- : AppendConflict.make({
647
- threadId: validated.threadId,
648
- batchId: validated.batch.batchId,
649
- reason: error.reason,
650
- });
651
- }
652
-
653
- return storeError("append canonical batch", error);
654
- }),
655
- Effect.flatMap((result) =>
656
- Schema.decodeEffect(AppendResult)(result).pipe(
657
- Effect.mapError((error) => schemaStoreError("decode append result", error)),
345
+ const result = yield* journal.state.owner
346
+ .withProgress(rawRequest.threadId, journal.append(rawRequest, observed))
347
+ .pipe(
348
+ Effect.mapError((error) => {
349
+ if (isDoAppendConflict(error)) {
350
+ return error.actualTailSequence !== undefined
351
+ ? AppendConflict.make({
352
+ threadId: validated.threadId,
353
+ batchId: validated.batch.batchId,
354
+ reason: error.reason,
355
+ actualTailSequence: error.actualTailSequence,
356
+ })
357
+ : AppendConflict.make({
358
+ threadId: validated.threadId,
359
+ batchId: validated.batch.batchId,
360
+ reason: error.reason,
361
+ });
362
+ }
363
+
364
+ return storeError("append canonical batch", error);
365
+ }),
366
+ Effect.flatMap((result) =>
367
+ Schema.decodeEffect(AppendResult)(result).pipe(
368
+ Effect.mapError((error) => schemaStoreError("decode append result", error)),
369
+ ),
658
370
  ),
659
- ),
660
- );
371
+ );
661
372
 
662
373
  yield* hitFailpoint("append:after");
663
374
 
@@ -680,7 +391,7 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
680
391
  };
681
392
  });
682
393
 
683
- const readEffect = Effect.fn("DoThreadStore.read")(function* (request: ThreadReadRequest) {
394
+ const readEffect = Effect.fnUntraced(function* (request: ThreadReadRequest) {
684
395
  const validated = yield* Schema.decodeEffect(Schema.toType(ThreadReadRequest))(request).pipe(
685
396
  Effect.mapError((error) => schemaStoreError("validate thread read", error)),
686
397
  );
@@ -758,244 +469,128 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
758
469
  const observe: ThreadStore["Service"]["observe"] = (request) =>
759
470
  Stream.unwrap(observeEffect(request)).pipe(Stream.withSpan("DoThreadStore.observe"));
760
471
 
761
- const exportThread: ThreadStore["Service"]["export"] = Effect.fn("DoThreadStore.export")(
762
- function* (request: ThreadExportRequest) {
763
- const validated = yield* Schema.decodeEffect(Schema.toType(ThreadExportRequest))(
764
- request,
765
- ).pipe(Effect.mapError((error) => schemaStoreError("validate thread export", error)));
766
-
767
- yield* requireThread(journal, validated.threadId);
768
-
769
- const exported = yield* journal
770
- .exportThread(validated.threadId)
771
- .pipe(Effect.mapError((error) => storeError("export thread", error)));
772
-
773
- const records = yield* Effect.forEach(exported.records, decodeEnvelope);
774
-
775
- if (records.length > MAX_THREAD_EXPORT_RECORDS) {
776
- return yield* ThreadStoreError.make({
777
- operation: "decode thread export",
778
- message: "The thread exceeds the current export record limit.",
779
- });
780
- }
781
-
782
- const tailDigest = yield* Schema.decodeEffect(Digest)(exported.thread.tail_digest).pipe(
783
- Effect.mapError((error) => schemaStoreError("decode export tail digest", error)),
784
- );
785
-
786
- return ThreadExport.make({
787
- format: "effect-agent/thread@1",
788
- threadId: validated.threadId,
789
- tailSequence: exported.thread.tail_sequence,
790
- tailDigest,
791
- records,
792
- });
793
- },
794
- );
795
-
796
- const inspectTail: ThreadStore["Service"]["inspectTail"] = Effect.fn("DoThreadStore.inspectTail")(
797
- function* (request: ThreadTailRequest) {
798
- const validated = yield* Schema.decodeEffect(Schema.toType(ThreadTailRequest))(request).pipe(
799
- Effect.mapError((error) => schemaStoreError("validate tail inspection", error)),
800
- );
801
-
802
- const thread = yield* requireThread(journal, validated.threadId);
803
-
804
- const tailDigest = yield* Schema.decodeEffect(Digest)(thread.tail_digest).pipe(
805
- Effect.mapError((error) => schemaStoreError("decode tail digest", error)),
806
- );
807
-
808
- return ThreadTail.make({
809
- threadId: validated.threadId,
810
- tailSequence: thread.tail_sequence,
811
- tailDigest,
812
- producerEpoch: thread.producer_epoch,
813
- });
814
- },
815
- );
472
+ const transfer = yield* makeSqlThreadImport({
473
+ owner: journal.state.owner,
474
+ offsetPrefix: DO_OFFSET_PREFIX,
475
+ maxValueBytes: config.maxStoredValueBytes,
476
+ // Import establishes a new sequence space; discard derivatives in this same transaction.
477
+ afterImport: ({ result }) =>
478
+ Effect.gen(function* () {
479
+ const prefix = promptCheckpointPrefix(result.threadId);
480
+
481
+ yield* sql`DELETE FROM effect_agent_meta WHERE key >= ${prefix} AND key < ${prefix + "~"}`;
482
+ // Closed imported history must not emit historical lifecycle notifications again.
483
+ if (journal.lifecycle !== undefined)
484
+ yield* sql`INSERT INTO effect_agent_lifecycle_cursors(thread_id, through_sequence) VALUES (${result.threadId}, ${result.tailSequence})`;
485
+ }).pipe(
486
+ Effect.catchTag("SqlError", (cause) => storeError("rebuild imported derivatives", cause)),
487
+ ),
488
+ afterThreadRead: hitFailpoint("export:after-thread-read"),
489
+ read: (body) =>
490
+ journal.owner.transaction(body).pipe(
491
+ Effect.provideService(SqlClientService.SqlClient, sql),
492
+ Effect.catchIf(isSqlError, (cause) => storeError("export transaction", cause)),
493
+ ),
494
+ write: (body) =>
495
+ journal.owner.transaction(body.pipe(Effect.tap(() => journal.state.invalidate))).pipe(
496
+ Effect.provideService(SqlClientService.SqlClient, sql),
497
+ Effect.catchIf(isSqlError, (cause) => storeError("import transaction", cause)),
498
+ ),
499
+ });
816
500
 
817
- const saveCheckpoint: ThreadCheckpoints["save"] = Effect.fn("DoThreadStore.saveCheckpoint")(
818
- function* (request: SaveCheckpointRequest) {
819
- const validated = yield* Schema.decodeEffect(Schema.toType(SaveCheckpointRequest))(
820
- request,
821
- ).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
501
+ const inspectTail: ThreadStore["Service"]["inspectTail"] = Effect.fnUntraced(function* (
502
+ request: ThreadTailRequest,
503
+ ) {
504
+ const validated = yield* Schema.decodeEffect(Schema.toType(ThreadTailRequest))(request).pipe(
505
+ Effect.mapError((error) => schemaStoreError("validate tail inspection", error)),
506
+ );
822
507
 
823
- const thread = yield* requireThread(journal, validated.checkpoint.threadId);
508
+ const thread = yield* requireThread(journal, validated.threadId);
824
509
 
825
- if (validated.checkpoint.throughSequence > thread.tail_sequence) {
826
- return yield* CheckpointRejected.make({
827
- threadId: validated.checkpoint.threadId,
828
- reason: "ahead-of-tail",
829
- });
830
- }
510
+ return ThreadTail.make({
511
+ threadId: validated.threadId,
512
+ tailSequence: thread.tail_sequence,
513
+ });
514
+ });
831
515
 
832
- const canonicalDigest = yield* tailDigestAt(
833
- journal,
834
- validated.checkpoint.threadId,
835
- validated.checkpoint.throughSequence,
836
- );
516
+ const saveCheckpoint: ThreadCheckpoints["save"] = Effect.fnUntraced(function* (
517
+ request: SaveCheckpointRequest,
518
+ ) {
519
+ const validated = yield* Schema.decodeEffect(Schema.toType(SaveCheckpointRequest))(
520
+ request,
521
+ ).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
837
522
 
838
- if (canonicalDigest !== validated.checkpoint.tailDigest) {
839
- return yield* CheckpointRejected.make({
840
- threadId: validated.checkpoint.threadId,
841
- reason: "digest-mismatch",
842
- });
843
- }
844
- const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
523
+ const thread = yield* requireThread(journal, validated.checkpoint.threadId);
845
524
 
846
- const raw = RawCheckpoint.make({
525
+ if (validated.checkpoint.throughSequence > thread.tail_sequence) {
526
+ return yield* CheckpointRejected.make({
847
527
  threadId: validated.checkpoint.threadId,
848
- throughSequence: validated.checkpoint.throughSequence,
849
- tailDigest: validated.checkpoint.tailDigest,
850
- checkpointJson,
528
+ reason: "ahead-of-tail",
851
529
  });
530
+ }
852
531
 
853
- yield* hitFailpoint("save-checkpoint:before");
854
- yield* journal.saveCheckpoint(raw).pipe(
855
- Effect.mapError((error) =>
856
- isDoCheckpointConflict(error)
857
- ? CheckpointRejected.make({
858
- threadId: validated.checkpoint.threadId,
859
- reason: "digest-mismatch",
860
- })
861
- : storeError("save checkpoint", error),
862
- ),
863
- );
864
- yield* hitFailpoint("save-checkpoint:after");
865
- },
866
- );
867
-
868
- const loadCheckpoint: ThreadCheckpoints["load"] = Effect.fn("DoThreadStore.loadCheckpoint")(
869
- function* (request: LoadCheckpointRequest) {
870
- const validated = yield* Schema.decodeEffect(Schema.toType(LoadCheckpointRequest))(
871
- request,
872
- ).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
873
-
874
- const thread = yield* requireThread(journal, validated.threadId);
875
-
876
- const rows = yield* journal
877
- .loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence)
878
- .pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
879
-
880
- if (rows.length === 0) return Option.none();
881
- if (rows.length !== 1) {
882
- return yield* ThreadStoreError.make({
883
- operation: "load checkpoint",
884
- message: `Expected at most one checkpoint row but found ${rows.length}.`,
885
- });
886
- }
887
- const row = rows[0];
888
- const checkpoint = yield* decodeCheckpoint(row.checkpoint_json);
889
-
890
- if (
891
- row.thread_id !== validated.threadId ||
892
- checkpoint.threadId !== row.thread_id ||
893
- checkpoint.throughSequence !== row.through_sequence ||
894
- checkpoint.tailDigest !== row.tail_digest
895
- ) {
896
- return yield* ThreadStoreError.make({
897
- operation: "load checkpoint",
898
- message: "Stored checkpoint metadata does not match its canonical row.",
899
- });
900
- }
901
-
902
- const canonicalDigest = yield* tailDigestAt(
903
- journal,
904
- checkpoint.threadId,
905
- checkpoint.throughSequence,
906
- );
907
-
908
- if (canonicalDigest !== checkpoint.tailDigest) {
909
- return yield* CheckpointRejected.make({
910
- threadId: checkpoint.threadId,
911
- reason: "digest-mismatch",
912
- });
913
- }
914
-
915
- return Option.some(checkpoint);
916
- },
917
- );
918
-
919
- const saveRecoveryCheckpoint: ThreadRecoveryCheckpoints["save"] = Effect.fn(
920
- "DoThreadStore.saveRecoveryCheckpoint",
921
- )(function* (request) {
922
- const validated = yield* Schema.decodeEffect(Schema.toType(SaveRecoveryCheckpointRequest))(
923
- request,
924
- ).pipe(Effect.mapError((error) => schemaStoreError("validate recovery checkpoint", error)));
925
-
532
+ yield* requireBatchBoundary(
533
+ journal,
534
+ validated.checkpoint.threadId,
535
+ validated.checkpoint.throughSequence,
536
+ );
926
537
  const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
927
538
 
928
- yield* journal
929
- .saveRecoveryCheckpoint(validated, checkpointJson)
930
- .pipe(
931
- Effect.mapError((error) =>
932
- error._tag === "CheckpointRejected" ||
933
- error._tag === "FenceRejected" ||
934
- error._tag === "ThreadNotMaterialized"
935
- ? error
936
- : storeError("save recovery checkpoint", error),
937
- ),
938
- );
539
+ const raw = RawCheckpoint.make({
540
+ threadId: validated.checkpoint.threadId,
541
+ throughSequence: validated.checkpoint.throughSequence,
542
+ checkpointJson,
543
+ });
544
+
545
+ yield* hitFailpoint("save-checkpoint:before");
546
+ yield* journal.saveCheckpoint(raw).pipe(
547
+ Effect.mapError((error) =>
548
+ isDoCheckpointConflict(error)
549
+ ? CheckpointRejected.make({
550
+ threadId: validated.checkpoint.threadId,
551
+ reason: error.reason,
552
+ })
553
+ : storeError("save checkpoint", error),
554
+ ),
555
+ );
556
+ yield* hitFailpoint("save-checkpoint:after");
939
557
  });
940
558
 
941
- const loadRecoveryCheckpoint: ThreadRecoveryCheckpoints["load"] = Effect.fn(
942
- "DoThreadStore.loadRecoveryCheckpoint",
943
- )(function* (request) {
559
+ const loadCheckpoint: ThreadCheckpoints["load"] = Effect.fnUntraced(function* (
560
+ request: LoadCheckpointRequest,
561
+ ) {
944
562
  const validated = yield* Schema.decodeEffect(Schema.toType(LoadCheckpointRequest))(
945
563
  request,
946
- ).pipe(
947
- Effect.mapError((error) => schemaStoreError("validate recovery checkpoint lookup", error)),
948
- );
564
+ ).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
949
565
 
950
566
  const thread = yield* requireThread(journal, validated.threadId);
951
567
 
952
- const corrupt = () =>
953
- CheckpointRejected.make({ threadId: validated.threadId, reason: "corrupt" });
954
-
955
568
  const rows = yield* journal
956
- .loadRecoveryCheckpoint(validated.threadId)
957
- .pipe(
958
- Effect.mapError((error) =>
959
- error._tag === "DoStorageCorruptionError"
960
- ? corrupt()
961
- : storeError("load recovery checkpoint", error),
962
- ),
963
- );
569
+ .loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence)
570
+ .pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
964
571
 
965
572
  if (rows.length === 0) return Option.none();
966
- if (rows.length !== 1) return yield* corrupt();
573
+ if (rows.length !== 1) {
574
+ return yield* ThreadStoreError.make({
575
+ operation: "load checkpoint",
576
+ message: `Expected at most one checkpoint row but found ${rows.length}.`,
577
+ });
578
+ }
967
579
  const row = rows[0];
968
-
969
- const checkpoint = yield* Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(
970
- row.checkpoint_json,
971
- ).pipe(Effect.mapError(corrupt));
580
+ const checkpoint = yield* decodeCheckpoint(row.checkpoint_json);
972
581
 
973
582
  if (
974
583
  row.thread_id !== validated.threadId ||
975
584
  checkpoint.threadId !== row.thread_id ||
976
- checkpoint.throughSequence !== row.through_sequence ||
977
- checkpoint.tailDigest !== row.tail_digest
978
- )
979
- return yield* corrupt();
980
- if (checkpoint.throughSequence > thread.tail_sequence)
981
- return yield* CheckpointRejected.make({
982
- threadId: validated.threadId,
983
- reason: "ahead-of-tail",
585
+ checkpoint.throughSequence !== row.through_sequence
586
+ ) {
587
+ return yield* ThreadStoreError.make({
588
+ operation: "load checkpoint",
589
+ message: "Stored checkpoint metadata does not match its canonical row.",
984
590
  });
985
- if (checkpoint.throughSequence > (validated.atOrBeforeSequence ?? thread.tail_sequence))
986
- return Option.none();
987
-
988
- const canonicalDigest = yield* tailDigestAt(
989
- journal,
990
- checkpoint.threadId,
991
- checkpoint.throughSequence,
992
- );
591
+ }
993
592
 
994
- if (canonicalDigest !== checkpoint.tailDigest)
995
- return yield* CheckpointRejected.make({
996
- threadId: validated.threadId,
997
- reason: "digest-mismatch",
998
- });
593
+ yield* requireBatchBoundary(journal, checkpoint.threadId, checkpoint.throughSequence);
999
594
 
1000
595
  return Option.some(checkpoint);
1001
596
  });
@@ -1010,8 +605,6 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
1010
605
  Schema.UndefinedOr(
1011
606
  Schema.Struct({
1012
607
  tail_sequence: CanonicalSequence,
1013
- tail_digest: Digest,
1014
- producer_epoch: ThreadMaterialization.fields.producerEpoch,
1015
608
  }),
1016
609
  ),
1017
610
  )(rows[0]),
@@ -1022,22 +615,36 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
1022
615
  );
1023
616
 
1024
617
  const threadStore = ThreadStore.of({
618
+ archives: journal.archives,
619
+ work: journal.work.storage,
1025
620
  ...(journal.lifecycle === undefined
1026
621
  ? {}
1027
622
  : { lifecyclePublications: journal.lifecycle.storage }),
1028
623
  readIdentity: selectedReads.readIdentity,
624
+ readExecution: (request) =>
625
+ journal.state.read(
626
+ requireThread(journal, request.threadId).pipe(
627
+ Effect.andThen(journal.readExecution(request)),
628
+ ),
629
+ ),
630
+ readWorkerCapacity: selectedReads.readWorkerCapacity,
631
+ verification: transfer.verification,
1029
632
  countPeerMessages: selectedReads.countPeerMessages,
1030
633
  append,
1031
- export: exportThread,
634
+ export: transfer.export,
1032
635
  inspectTail,
1033
636
  materialize,
1034
637
  observe,
1035
638
  read,
639
+ readPrompt: (request) => Stream.fromIterableEffect(selectedReads.readPrompt(request)),
1036
640
  checkpoints: { save: saveCheckpoint, load: loadCheckpoint },
1037
- recoveryCheckpoints: { save: saveRecoveryCheckpoint, load: loadRecoveryCheckpoint },
641
+ promptCheckpoints: makePromptCheckpoints(sql, journal, config.maxStoredValueBytes),
1038
642
  });
1039
643
 
1040
- return Context.make(ThreadStore, threadStore);
644
+ return Context.make(ThreadStore, threadStore).pipe(
645
+ Context.add(ThreadImport, transfer.importer),
646
+ Context.add(StorageOwner, journal.state.owner),
647
+ );
1041
648
  });
1042
649
 
1043
650
  /**
@@ -1045,10 +652,10 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
1045
652
  * Crypto authority kept visible in its input channel.
1046
653
  */
1047
654
  export const threadStoreLayer: Layer.Layer<
1048
- ThreadStore,
655
+ ThreadStore | ThreadReader | ThreadImport | StorageOwner,
1049
656
  DoStorageInitializationError,
1050
657
  DoStorageConfig | DoStorageFailpoint | SqlClientService.SqlClient | Crypto.Crypto
1051
- > = Layer.effectContext(makeServices());
658
+ > = ThreadReader.layer().pipe(Layer.provideMerge(Layer.effectContext(makeServices())));
1052
659
 
1053
660
  /**
1054
661
  * Validated Durable Object storage configuration Layer with the documented defaults applied.
@@ -1061,8 +668,6 @@ export const storageConfigLayer = (
1061
668
  Layer.effect(DoStorageConfig)(
1062
669
  Schema.decodeEffect(DoStorageConfigValue)({
1063
670
  observationPollInterval: options.observationPollInterval ?? 25,
1064
- ownershipLeaseDuration:
1065
- options.ownershipLeaseDuration ?? Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION),
1066
671
  maxStoredValueBytes: options.maxStoredValueBytes ?? DEFAULT_MAX_STORED_VALUE_BYTES,
1067
672
  verifyOnOpen: options.verifyOnOpen ?? false,
1068
673
  }).pipe(
@@ -1087,12 +692,15 @@ export const storageFailpointLayer = (
1087
692
  /**
1088
693
  * A composition-root convenience Layer for canonical Threads inside one Durable Object,
1089
694
  * built over `ctx.storage`. Durable accepted work is served by the separate SubmissionLedger
1090
- * port; point both at the SAME `ctx.storage` so claims fence the same producer epochs
695
+ * port; point both at the SAME `ctx.storage` so both share the same scoped storage owner
1091
696
  * (ADR-0011 D7's "same file" rule, transposed to one object's private database).
1092
697
  */
1093
698
  export const layer = (
1094
699
  options: DoStorageOptions,
1095
- ): Layer.Layer<ThreadStore, DoStorageInitializationError> =>
700
+ ): Layer.Layer<
701
+ ThreadStore | ThreadReader | ThreadImport | StorageOwner,
702
+ DoStorageInitializationError
703
+ > =>
1096
704
  Layer.unwrap(
1097
705
  Effect.map(DoStorageConfig, (config) =>
1098
706
  threadStoreLayer.pipe(
@@ -1108,6 +716,48 @@ export const layer = (
1108
716
  ),
1109
717
  ).pipe(Layer.provide(storageConfigLayer(options)));
1110
718
 
719
+ /** Read a quiesced current-layout Object without initializing its layout or ownership. */
720
+ export const exportThread = Effect.fn("DoThreadStore.exportThread")(function* (
721
+ options: Pick<DoStorageOptions, "storage">,
722
+ request: ThreadExportRequest,
723
+ ) {
724
+ yield* checkExportSource(request.threadId);
725
+
726
+ return yield* Effect.gen(function* () {
727
+ const sql = yield* SqlClientService.SqlClient;
728
+ const state = yield* ownedState(sql);
729
+
730
+ return yield* state
731
+ .transaction(
732
+ Effect.gen(function* () {
733
+ yield* readDoStorageHeader();
734
+
735
+ const transfer = yield* makeSqlThreadImport({
736
+ owner: state.owner,
737
+ offsetPrefix: DO_OFFSET_PREFIX,
738
+ read: (body) => body,
739
+ write: () => Effect.die("A read-only exporter cannot import"),
740
+ });
741
+
742
+ return yield* transfer.export(request);
743
+ }),
744
+ )
745
+ .pipe(
746
+ Effect.catchTag("SqlError", (cause) =>
747
+ DoStorageError.make({
748
+ operation: "export Thread snapshot",
749
+ message: cause.message,
750
+ cause,
751
+ }),
752
+ ),
753
+ );
754
+ }).pipe(
755
+ Effect.provide(
756
+ Layer.merge(SqliteClient.layer({ storage: options.storage }), BrowserCrypto.layer),
757
+ ),
758
+ );
759
+ });
760
+
1111
761
  /**
1112
762
  * Discard the Object's derived thread and ledger state after direct SQL maintenance.
1113
763
  * Quiesce port operations during the raw write and invalidation, and enroll the write with
@@ -1116,6 +766,10 @@ export const layer = (
1116
766
  export const invalidate = invalidateOwnedState;
1117
767
 
1118
768
  /** Share the Object's transaction gate with owner-local SQL Memory compositions. */
1119
- export const sqlOwnerLayer = Layer.effect(SqlStorageOwner)(
1120
- Effect.flatMap(SqlClientService.SqlClient, ownedState),
769
+ export const sqlOwnerLayer = Layer.effectContext(
770
+ Effect.flatMap(SqlClientService.SqlClient, ownedState).pipe(
771
+ Effect.map((state) =>
772
+ Context.make(SqlStorageOwner, state).pipe(Context.add(StorageOwner, state.owner)),
773
+ ),
774
+ ),
1121
775
  );