@effect-agent/storage-cloudflare 0.1.0-beta.11 → 0.1.0-beta.110

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 (74) hide show
  1. package/dist/DoMemoryStore.d.mts +29 -0
  2. package/dist/DoMemoryStore.mjs +55 -0
  3. package/dist/DoMemoryStore.mjs.map +1 -0
  4. package/dist/DoMessageDeliveryStore.d.mts +17 -0
  5. package/dist/DoMessageDeliveryStore.mjs +32 -0
  6. package/dist/DoMessageDeliveryStore.mjs.map +1 -0
  7. package/dist/DoScheduleStore.d.mts +36 -0
  8. package/dist/DoScheduleStore.mjs +388 -0
  9. package/dist/DoScheduleStore.mjs.map +1 -0
  10. package/dist/DoStorageConfig.d.mts +45 -0
  11. package/dist/DoStorageConfig.mjs +52 -0
  12. package/dist/DoStorageConfig.mjs.map +1 -0
  13. package/dist/DoStorageError-Cmhvl4TQ.d.mts +98 -0
  14. package/dist/DoStorageError.d.mts +2 -0
  15. package/dist/DoStorageError.mjs +164 -0
  16. package/dist/DoStorageError.mjs.map +1 -0
  17. package/dist/DoStorageFailpoint.d.mts +17 -0
  18. package/dist/DoStorageFailpoint.mjs +14 -0
  19. package/dist/DoStorageFailpoint.mjs.map +1 -0
  20. package/dist/DoStorageFailpointTesting.d.mts +33 -0
  21. package/dist/DoStorageFailpointTesting.mjs +35 -0
  22. package/dist/DoStorageFailpointTesting.mjs.map +1 -0
  23. package/dist/DoStorageVersion-Db6fjP-I.d.mts +12 -0
  24. package/dist/DoStorageVersion.d.mts +2 -0
  25. package/dist/DoStorageVersion.mjs +8 -0
  26. package/dist/DoStorageVersion.mjs.map +1 -0
  27. package/dist/DoSubmissionLedger.d.mts +23 -0
  28. package/dist/DoSubmissionLedger.mjs +1864 -0
  29. package/dist/DoSubmissionLedger.mjs.map +1 -0
  30. package/dist/DoSubscriptionStore.d.mts +25 -0
  31. package/dist/DoSubscriptionStore.mjs +183 -0
  32. package/dist/DoSubscriptionStore.mjs.map +1 -0
  33. package/dist/DoThreadStore.d.mts +59 -0
  34. package/dist/DoThreadStore.mjs +481 -0
  35. package/dist/DoThreadStore.mjs.map +1 -0
  36. package/dist/MemoryProtocol.d.mts +38160 -0
  37. package/dist/MemoryProtocol.mjs +219 -0
  38. package/dist/MemoryProtocol.mjs.map +1 -0
  39. package/dist/PortProtocol.d.mts +4933 -0
  40. package/dist/PortProtocol.mjs +193 -0
  41. package/dist/PortProtocol.mjs.map +1 -0
  42. package/dist/PortRouting.d.mts +86 -0
  43. package/dist/PortRouting.mjs +439 -0
  44. package/dist/PortRouting.mjs.map +1 -0
  45. package/dist/do-journal-THW8-ZeP.mjs +1041 -0
  46. package/dist/do-journal-THW8-ZeP.mjs.map +1 -0
  47. package/dist/index.d.mts +14 -1525
  48. package/dist/index.mjs +14 -3695
  49. package/dist/migrations-DZKTNU88.mjs +291 -0
  50. package/dist/migrations-DZKTNU88.mjs.map +1 -0
  51. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  52. package/package.json +1 -45
  53. package/src/DoMemoryStore.ts +56 -0
  54. package/src/DoMessageDeliveryStore.ts +53 -0
  55. package/src/DoScheduleStore.ts +622 -0
  56. package/src/{errors.ts → DoStorageError.ts} +10 -3
  57. package/src/DoStorageFailpoint.ts +20 -0
  58. package/src/{do-storage-failpoint.ts → DoStorageFailpointTesting.ts} +5 -18
  59. package/src/DoStorageVersion.ts +2 -0
  60. package/src/{do-ledger.ts → DoSubmissionLedger.ts} +960 -237
  61. package/src/DoSubscriptionStore.ts +329 -0
  62. package/src/DoThreadStore.ts +1039 -0
  63. package/src/MemoryProtocol.ts +351 -0
  64. package/src/{port-protocol.ts → PortProtocol.ts} +76 -38
  65. package/src/{routing.ts → PortRouting.ts} +374 -264
  66. package/src/index.ts +13 -32
  67. package/src/internal/do-journal.ts +1656 -0
  68. package/src/internal/message-delivery-schema.ts +24 -0
  69. package/src/{migrations.ts → internal/migrations.ts} +55 -35
  70. package/src/internal/recovery-checkpoint-schema.ts +17 -0
  71. package/dist/index.mjs.map +0 -1
  72. package/src/do-conversation-store.ts +0 -850
  73. package/src/do-journal.ts +0 -1078
  74. /package/src/{do-storage-config.ts → DoStorageConfig.ts} +0 -0
@@ -0,0 +1,1041 @@
1
+ import { DoAppendConflict, DoCheckpointConflict, DoFenceRejected, DoStorageCompatibilityError, DoStorageCorruptionError, DoStorageError, DoValueBoundExceeded } from "./DoStorageError.mjs";
2
+ import { a as createMessageDeliveryTables, i as createRecoveryCheckpointTable, n as createNonterminalIndex, r as doMigrations } from "./migrations-DZKTNU88.mjs";
3
+ import { Effect, Schema, Stream } from "effect";
4
+ import { createMessageDeliveryPendingIndex } from "effect-agent/sql-message-delivery-store";
5
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
6
+ import { SqliteMigrator } from "@effect/sql-sqlite-do";
7
+ import { EMPTY_TAIL_DIGEST } from "effect-agent/digest";
8
+ import { CanonicalRecord, CanonicalSequence, ProducerEpoch } from "effect-agent/records";
9
+ import { checkV2ThreadLayout } from "effect-agent/sql-storage-v2-upgrade";
10
+ import { createNativeReadIndexes, indexCanonicalRecord, seedNativeReadIndexes } from "effect-agent/sql-thread-native-reads";
11
+ import { CheckpointRejected, FenceRejected, MAX_THREAD_EXPORT_RECORDS, ThreadNotMaterialized } from "effect-agent/thread-store";
12
+ import { SqlError } from "effect/unstable/sql/SqlError";
13
+ //#region src/internal/do-journal.ts
14
+ /**
15
+ * Static schema ceiling for stored text columns. Writes are bounded in BYTES by the
16
+ * configured `maxStoredValueBytes` (always ≤ 2,000,000); UTF-8 byte length is never smaller
17
+ * than UTF-16 string length, so any value that passed the byte bound also passes this
18
+ * decode-side character ceiling.
19
+ */
20
+ const BoundedStoredText = Schema.String.check(Schema.isMaxLength(2e6));
21
+ const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
22
+ const MAX_RECORDS_PER_THREAD = MAX_THREAD_EXPORT_RECORDS;
23
+ const ZERO_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
24
+ const MAX_IDENTIFIER_LENGTH = 1024;
25
+ const MAX_READ_PAGE_JSON_BYTES = 4194304;
26
+ const isSqlError = Schema.is(SqlError);
27
+ const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
28
+ const chunked = (values, size) => {
29
+ const chunks = [];
30
+ for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
31
+ return chunks;
32
+ };
33
+ var DoMetaRow = class extends Schema.Class("DoMetaRow")({ value: Schema.NonEmptyString.check(Schema.isMaxLength(128)) }) {};
34
+ var DoNameRow = class extends Schema.Class("DoNameRow")({ name: BoundedIdentifier }) {};
35
+ var ThreadRow = class extends Schema.Class("ThreadRow")({
36
+ thread_id: BoundedIdentifier,
37
+ created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
38
+ producer_epoch: ProducerEpoch,
39
+ tail_digest: BoundedStoredText,
40
+ tail_sequence: CanonicalSequence
41
+ }) {};
42
+ var BatchRow = class extends Schema.Class("BatchRow")({
43
+ batch_digest: BoundedStoredText,
44
+ batch_id: BoundedIdentifier,
45
+ batch_json: BoundedStoredText,
46
+ thread_id: BoundedIdentifier,
47
+ first_sequence: CanonicalSequence,
48
+ last_sequence: CanonicalSequence,
49
+ tail_digest: BoundedStoredText
50
+ }) {};
51
+ var RecordRow = class extends Schema.Class("RecordRow")({
52
+ batch_id: BoundedIdentifier,
53
+ thread_id: BoundedIdentifier,
54
+ record_id: BoundedIdentifier,
55
+ record_json: BoundedStoredText,
56
+ sequence: CanonicalSequence
57
+ }) {};
58
+ const ReadPlanRow = Schema.Struct({
59
+ sequence: CanonicalSequence,
60
+ record_json_bytes: Schema.Natural.check(Schema.isLessThanOrEqualTo(MAX_READ_PAGE_JSON_BYTES))
61
+ });
62
+ var CheckpointRow = class extends Schema.Class("CheckpointRow")({
63
+ checkpoint_json: BoundedStoredText,
64
+ thread_id: BoundedIdentifier,
65
+ tail_digest: BoundedStoredText,
66
+ through_sequence: CanonicalSequence
67
+ }) {};
68
+ var RawRecord = class extends Schema.Class("@effect-agent/storage-cloudflare/RawRecord")({
69
+ recordId: BoundedIdentifier,
70
+ recordJson: BoundedStoredText
71
+ }) {};
72
+ var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawAppendRequest")({
73
+ batchDigest: BoundedStoredText,
74
+ batchId: BoundedIdentifier,
75
+ batchJson: BoundedStoredText,
76
+ threadId: BoundedIdentifier,
77
+ expectedTailDigest: BoundedStoredText,
78
+ expectedTailSequence: CanonicalSequence,
79
+ producerEpoch: ProducerEpoch,
80
+ records: Schema.NonEmptyArray(RawRecord).check(Schema.isMaxLength(256)),
81
+ tailDigest: BoundedStoredText
82
+ }) {};
83
+ var RawAppendResult = class extends Schema.Class("@effect-agent/storage-cloudflare/RawAppendResult")({
84
+ firstSequence: CanonicalSequence,
85
+ lastSequence: CanonicalSequence,
86
+ replayed: Schema.Boolean,
87
+ tailDigest: BoundedStoredText
88
+ }) {};
89
+ var RawReadRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawReadRequest")({
90
+ threadId: BoundedIdentifier,
91
+ fromSequenceExclusive: CanonicalSequence,
92
+ limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
93
+ }) {};
94
+ var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-cloudflare/RawCheckpoint")({
95
+ checkpointJson: BoundedStoredText,
96
+ threadId: BoundedIdentifier,
97
+ tailDigest: BoundedStoredText,
98
+ throughSequence: CanonicalSequence
99
+ }) {};
100
+ var RawThreadExport = class extends Schema.Class("@effect-agent/storage-cloudflare/RawThreadExport")({
101
+ thread: ThreadRow,
102
+ records: Schema.Array(RecordRow)
103
+ }) {};
104
+ const noFailpoint = () => Effect.void;
105
+ const storageError = (operation) => (error) => DoStorageError.make({
106
+ cause: error,
107
+ operation,
108
+ message: error.message
109
+ });
110
+ /** Decode raw Durable Object SQLite rows against a Schema, reporting failures as typed corruption. */
111
+ const decodeRows = Effect.fn((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
112
+ table,
113
+ rowKey,
114
+ message: String(error)
115
+ }))));
116
+ /** Decode exactly one raw row against a Schema, reporting failures as typed corruption. */
117
+ const decodeSingleRow = Effect.fn((schema, table, rowKey, rows) => decodeRows(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(DoStorageCorruptionError.make({
118
+ table,
119
+ rowKey,
120
+ message: `Expected exactly one row but found ${decoded.length}.`
121
+ })))));
122
+ /** Column inventory of the supported v3 predecessor, independent of physical column order. */
123
+ const predecessorColumns = {
124
+ effect_agent_threads: [
125
+ "thread_id",
126
+ "created_at",
127
+ "tail_sequence",
128
+ "tail_digest",
129
+ "producer_epoch"
130
+ ],
131
+ effect_agent_canonical_batches: [
132
+ "thread_id",
133
+ "batch_id",
134
+ "first_sequence",
135
+ "last_sequence",
136
+ "batch_digest",
137
+ "tail_digest",
138
+ "batch_json"
139
+ ],
140
+ effect_agent_canonical_records: [
141
+ "thread_id",
142
+ "sequence",
143
+ "record_id",
144
+ "batch_id",
145
+ "record_json"
146
+ ],
147
+ effect_agent_checkpoints: [
148
+ "thread_id",
149
+ "through_sequence",
150
+ "tail_digest",
151
+ "checkpoint_json"
152
+ ],
153
+ effect_agent_submissions: [
154
+ "submission_id",
155
+ "thread_id",
156
+ "queue_sequence",
157
+ "principal",
158
+ "idempotency_key",
159
+ "agent_id",
160
+ "agent_digests_json",
161
+ "deployment_id",
162
+ "input_json",
163
+ "input_digest",
164
+ "receipt_id",
165
+ "state",
166
+ "settled_outcome",
167
+ "created_at",
168
+ "ready_at",
169
+ "input_applied_record_id",
170
+ "input_applied_sequence",
171
+ "joined_host_submission_id",
172
+ "suspended_reason_json",
173
+ "suspended_at",
174
+ "unknown_reason",
175
+ "unknown_tool_call_ids_json",
176
+ "parent_submission_id",
177
+ "parent_tool_call_id",
178
+ "admission_group",
179
+ "admission_fence_json"
180
+ ],
181
+ effect_agent_submission_ownership: [
182
+ "submission_id",
183
+ "attempt_id",
184
+ "ownership_token",
185
+ "producer_epoch",
186
+ "owner_producer_id",
187
+ "lease_expires_at"
188
+ ],
189
+ effect_agent_attempts: [
190
+ "attempt_id",
191
+ "submission_id",
192
+ "thread_id",
193
+ "owner_producer_id",
194
+ "producer_epoch",
195
+ "claimed_at"
196
+ ],
197
+ effect_agent_settlement_reservations: [
198
+ "submission_id",
199
+ "settlement_id",
200
+ "outcome",
201
+ "record_id",
202
+ "record_json",
203
+ "record_digest",
204
+ "reserved_at",
205
+ "finalized_at"
206
+ ],
207
+ effect_agent_abort_intents: [
208
+ "submission_id",
209
+ "author",
210
+ "reason",
211
+ "requested_at",
212
+ "canonical_record_id"
213
+ ],
214
+ effect_agent_approval_decisions: [
215
+ "submission_id",
216
+ "tool_call_id",
217
+ "decision",
218
+ "resolver",
219
+ "reason",
220
+ "decided_at"
221
+ ],
222
+ effect_agent_unknown_resolutions: [
223
+ "submission_id",
224
+ "tool_call_id",
225
+ "author",
226
+ "reason",
227
+ "resolution_json",
228
+ "resolved_at"
229
+ ],
230
+ effect_agent_child_reservations: [
231
+ "reservation_id",
232
+ "parent_submission_id",
233
+ "parent_tool_call_id",
234
+ "child_submission_id",
235
+ "status",
236
+ "allocation_json",
237
+ "allocation_digest",
238
+ "accounting_json",
239
+ "reserved_at",
240
+ "release_began_at",
241
+ "released_at"
242
+ ],
243
+ effect_agent_meta: ["key", "value"],
244
+ effect_agent_child_settlements: [
245
+ "parent_submission_id",
246
+ "child_submission_id",
247
+ "child_outcome",
248
+ "recorded_at"
249
+ ]
250
+ };
251
+ const checkPredecessorLayout = Effect.fn("DoJournal.checkPredecessorLayout")(function* (version) {
252
+ const sql = yield* SqlClient.SqlClient;
253
+ const expectedColumns = {
254
+ ...version === 3 ? predecessorColumns : {
255
+ ...predecessorColumns,
256
+ effect_agent_submissions: [
257
+ ...predecessorColumns.effect_agent_submissions,
258
+ "worker_admission_json",
259
+ "message_admission_json"
260
+ ],
261
+ effect_agent_message_deliveries: [
262
+ "owner_thread_id",
263
+ "message_id",
264
+ "version",
265
+ "state",
266
+ "deadline_at_millis",
267
+ "record_json"
268
+ ]
269
+ },
270
+ ...version === 5 ? { effect_agent_recovery_checkpoints: [
271
+ "thread_id",
272
+ "through_sequence",
273
+ "tail_digest",
274
+ "checkpoint_json"
275
+ ] } : {}
276
+ };
277
+ for (const [table, expected] of Object.entries(expectedColumns)) {
278
+ const columns = yield* decodeRows(Schema.Array(Schema.Struct({ name: BoundedIdentifier })), table, "schema", yield* sql.unsafe(`PRAGMA table_info(${table})`));
279
+ const names = new Set(expected);
280
+ if (columns.length !== names.size || columns.some((column) => !names.has(column.name))) return yield* DoStorageCompatibilityError.make({
281
+ actualVersion: version,
282
+ supportedVersion: 7,
283
+ message: `The v${version} ${table} columns do not match the supported predecessor; no upgrade was committed.`
284
+ });
285
+ }
286
+ });
287
+ const REQUIRED_TABLES = [
288
+ "effect_agent_abort_intents",
289
+ "effect_agent_approval_decisions",
290
+ "effect_agent_attempts",
291
+ "effect_agent_canonical_batches",
292
+ "effect_agent_canonical_records",
293
+ "effect_agent_checkpoints",
294
+ "effect_agent_child_reservations",
295
+ "effect_agent_child_settlements",
296
+ "effect_agent_threads",
297
+ "effect_agent_meta",
298
+ "effect_agent_settlement_reservations",
299
+ "effect_agent_submission_ownership",
300
+ "effect_agent_submissions",
301
+ "effect_agent_unknown_resolutions"
302
+ ];
303
+ /**
304
+ * Supported-predecessor or fresh storage gate (DEPLOY-008) over `effect_agent_meta` instead of
305
+ * `PRAGMA user_version` (unverified on Durable Object SQL storage; a meta table is portable
306
+ * regardless). No WAL check (Durable Object storage owns durability and confirms writes
307
+ * through output gates) and no busy timeout (a Durable Object has exactly one writer): the
308
+ * Node machinery those served has no DC analogue and is deliberately absent.
309
+ */
310
+ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint, maxStoredValueBytes) {
311
+ const metaTableRows = yield* sql`
312
+ SELECT name
313
+ FROM sqlite_master
314
+ WHERE type = 'table'
315
+ AND name = 'effect_agent_meta'
316
+ `.pipe(Effect.mapError(storageError("read storage version table")));
317
+ if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_meta", metaTableRows)).length === 0) {
318
+ const existingRows = yield* sql`
319
+ SELECT name
320
+ FROM sqlite_master
321
+ WHERE type = 'table'
322
+ AND name LIKE 'effect_agent_%'
323
+ ORDER BY name
324
+ `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
325
+ if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* DoStorageCompatibilityError.make({
326
+ actualVersion: 0,
327
+ supportedVersion: 7,
328
+ message: "The Durable Object contains unversioned Effect Agent tables. Refusing to mutate ambiguous stored data; retain it for inspection with its original writer."
329
+ });
330
+ yield* SqliteMigrator.run({
331
+ loader: doMigrations,
332
+ table: "effect_sql_migrations_agent_threads"
333
+ }).pipe(Effect.provideService(SqlClient.SqlClient, sql), Effect.mapError((error) => DoStorageError.make({
334
+ cause: error,
335
+ operation: "initialize current storage",
336
+ message: error.message
337
+ })));
338
+ } else {
339
+ const versionRows = yield* sql`
340
+ SELECT value
341
+ FROM effect_agent_meta
342
+ WHERE key = 'storage_version'
343
+ `.pipe(Effect.mapError(storageError("read storage version")));
344
+ const version = yield* decodeSingleRow(Schema.Array(DoMetaRow), "effect_agent_meta", "storage_version", versionRows);
345
+ if (version.value === "2" || version.value === "3" || version.value === "4" || version.value === "5") yield* sql.withTransaction(Effect.gen(function* () {
346
+ const current = yield* sql`SELECT value FROM effect_agent_meta WHERE key='storage_version'`;
347
+ if (current.length === 1 && current[0].value === String(7)) return;
348
+ if (current.length !== 1 || current[0].value !== "2" && current[0].value !== "3" && current[0].value !== "4" && current[0].value !== "5") return yield* DoStorageCompatibilityError.make({
349
+ actualVersion: -1,
350
+ supportedVersion: 7,
351
+ message: "Storage version changed while acquiring the upgrade transaction."
352
+ });
353
+ if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "required_tables", yield* sql`SELECT name FROM sqlite_master WHERE type='table' AND name IN ${sql.in([...REQUIRED_TABLES])}`)).length !== REQUIRED_TABLES.length) return yield* DoStorageCompatibilityError.make({
354
+ actualVersion: Number(current[0].value),
355
+ supportedVersion: 7,
356
+ message: "The predecessor store is missing required tables. Retain the original store for inspection; no upgrade was committed."
357
+ });
358
+ if ((yield* sql`SELECT name FROM sqlite_master WHERE type='table' AND name='effect_agent_recovery_checkpoints'`).length !== (current[0].value === "5" ? 1 : 0)) return yield* DoStorageCompatibilityError.make({
359
+ actualVersion: Number(current[0].value),
360
+ supportedVersion: 7,
361
+ message: "The predecessor recovery checkpoint storage does not match its version; refusing ambiguous data without mutation."
362
+ });
363
+ if ((yield* sql`SELECT name FROM sqlite_master WHERE name='effect_agent_submissions_nonterminal'`).length !== 0) return yield* DoStorageCompatibilityError.make({
364
+ actualVersion: Number(current[0].value),
365
+ supportedVersion: 7,
366
+ message: "The predecessor already contains the nonterminal index; refusing ambiguous storage without mutation."
367
+ });
368
+ if (current[0].value === "2") {
369
+ yield* checkV2ThreadLayout();
370
+ for (const statement of [
371
+ sql`ALTER TABLE effect_agent_submissions ADD COLUMN admission_group TEXT`,
372
+ sql`ALTER TABLE effect_agent_submissions ADD COLUMN admission_fence_json TEXT`,
373
+ sql`CREATE INDEX effect_agent_submissions_group ON effect_agent_submissions (thread_id, admission_group, state)`
374
+ ]) {
375
+ yield* failpoint("upgrade:before-mutation");
376
+ yield* statement;
377
+ yield* failpoint("upgrade:after-mutation");
378
+ }
379
+ }
380
+ if (current[0].value === "3") yield* checkPredecessorLayout(3);
381
+ if (current[0].value === "4") yield* checkPredecessorLayout(4);
382
+ if (current[0].value === "5") yield* checkPredecessorLayout(5);
383
+ if (current[0].value === "2" || current[0].value === "3") {
384
+ yield* failpoint("upgrade:before-mutation");
385
+ yield* sql`ALTER TABLE effect_agent_submissions ADD COLUMN worker_admission_json TEXT`;
386
+ yield* failpoint("upgrade:after-mutation");
387
+ yield* failpoint("upgrade:before-mutation");
388
+ yield* sql`ALTER TABLE effect_agent_submissions ADD COLUMN message_admission_json TEXT`;
389
+ yield* failpoint("upgrade:after-mutation");
390
+ yield* failpoint("upgrade:before-mutation");
391
+ yield* createMessageDeliveryTables;
392
+ yield* failpoint("upgrade:after-mutation");
393
+ }
394
+ if (current[0].value !== "5") {
395
+ yield* failpoint("upgrade:before-mutation");
396
+ yield* createRecoveryCheckpointTable;
397
+ yield* failpoint("upgrade:after-mutation");
398
+ }
399
+ yield* failpoint("upgrade:before-mutation");
400
+ yield* createNonterminalIndex;
401
+ yield* failpoint("upgrade:after-mutation");
402
+ yield* failpoint("upgrade:before-version");
403
+ yield* createNativeReadIndexes;
404
+ yield* createMessageDeliveryPendingIndex;
405
+ yield* seedNativeReadIndexes.pipe(Effect.catchTag("ThreadStoreError", (error) => DoStorageCorruptionError.make({
406
+ table: "effect_agent_canonical_records",
407
+ rowKey: "upgrade",
408
+ message: error.message
409
+ })));
410
+ yield* sql`UPDATE effect_agent_meta SET value='7' WHERE key='storage_version'`;
411
+ yield* failpoint("upgrade:after-version");
412
+ })).pipe(Effect.catchTag("DoStorageFailpointError", (error) => DoStorageError.make({
413
+ cause: error,
414
+ operation: "upgrade storage",
415
+ message: error.message
416
+ })), Effect.catchTag("StorageUpgradeError", (error) => DoStorageCorruptionError.make({
417
+ table: error.table,
418
+ rowKey: error.rowKey,
419
+ message: error.message
420
+ })), Effect.catchTag("SchemaError", (error) => DoStorageCorruptionError.make({
421
+ table: "effect_agent_canonical_records",
422
+ rowKey: "upgrade",
423
+ message: error.message
424
+ })), Effect.catchTag("SqlError", storageError("upgrade supported thread storage")));
425
+ else if (version.value === "6") yield* sql.withTransaction(Effect.gen(function* () {
426
+ const current = yield* sql`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
427
+ if (current[0]?.value === "7") return;
428
+ if (current[0]?.value !== "6") return yield* DoStorageCompatibilityError.make({
429
+ actualVersion: -1,
430
+ supportedVersion: 7,
431
+ message: "Storage version changed during native index upgrade"
432
+ });
433
+ yield* checkPredecessorLayout(5);
434
+ if ((yield* sql`SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'effect_agent_submissions_nonterminal'`).length !== 1) return yield* DoStorageCompatibilityError.make({
435
+ actualVersion: 6,
436
+ supportedVersion: 7,
437
+ message: "Predecessor storage is missing its required nonterminal index"
438
+ });
439
+ yield* failpoint("upgrade:before-mutation");
440
+ yield* createNativeReadIndexes;
441
+ yield* createMessageDeliveryPendingIndex;
442
+ yield* seedNativeReadIndexes.pipe(Effect.catchTag("ThreadStoreError", (error) => DoStorageCorruptionError.make({
443
+ table: "effect_agent_canonical_records",
444
+ rowKey: "upgrade",
445
+ message: error.message
446
+ })));
447
+ yield* failpoint("upgrade:after-mutation");
448
+ yield* failpoint("upgrade:before-version");
449
+ yield* sql`UPDATE effect_agent_meta SET value='7' WHERE key='storage_version'`;
450
+ yield* failpoint("upgrade:after-version");
451
+ })).pipe(Effect.mapError((error) => DoStorageError.make({
452
+ operation: "upgrade native indexes",
453
+ message: error.message,
454
+ cause: error
455
+ })));
456
+ else if (version.value !== String(7)) {
457
+ const actualVersion = Number.parseInt(version.value, 10);
458
+ return yield* DoStorageCompatibilityError.make({
459
+ actualVersion: Number.isSafeInteger(actualVersion) ? actualVersion : -1,
460
+ supportedVersion: 7,
461
+ message: `The Durable Object uses unsupported storage version ${version.value}; this build supports exactly version 7. Only supported v2, v3, v4, v5 and v6 can be upgraded automatically. Keep the original store and use a compatible library version.`
462
+ });
463
+ }
464
+ }
465
+ const requiredRows = yield* sql`
466
+ SELECT name
467
+ FROM sqlite_master
468
+ WHERE (type = 'table'
469
+ AND name IN ${sql.in([
470
+ ...REQUIRED_TABLES,
471
+ "effect_agent_message_deliveries",
472
+ "effect_agent_recovery_checkpoints"
473
+ ])}
474
+ ) OR (type = 'index' AND name IN ('effect_agent_submissions_nonterminal', 'effect_agent_records_subtree', 'effect_agent_message_deliveries_pending', 'effect_agent_records_outstanding', 'effect_agent_records_call', 'effect_agent_records_run_input', 'effect_agent_records_worker_input'))
475
+ ORDER BY name
476
+ `.pipe(Effect.mapError(storageError("verify storage tables")));
477
+ if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "required_tables", requiredRows)).length !== REQUIRED_TABLES.length + 9) return yield* DoStorageCompatibilityError.make({
478
+ actualVersion: 7,
479
+ supportedVersion: 7,
480
+ message: "The Durable Object claims the current format but is missing required tables or its nonterminal index. Retain the original store for inspection."
481
+ });
482
+ return makeJournal(sql, failpoint, maxStoredValueBytes);
483
+ });
484
+ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
485
+ /** Typed pre-write refusal for any single value over the configured byte bound. */
486
+ const checkValueBound = (operation, value) => {
487
+ const actualBytes = storedTextBytes(value);
488
+ return actualBytes > maxStoredValueBytes ? Effect.fail(DoValueBoundExceeded.make({
489
+ actualBytes,
490
+ maxBytes: maxStoredValueBytes,
491
+ operation
492
+ })) : Effect.void;
493
+ };
494
+ /**
495
+ * Runs one journal write transaction on the Durable Object storage-backed
496
+ * `withTransaction` (`ctx.storage.transaction()` under the hood). Within one Durable
497
+ * Object there is exactly ONE writer, so the Node `BEGIN IMMEDIATE` + busy-retry +
498
+ * `SqliteWriteContention` machinery has no analogue here and is deliberately absent.
499
+ * Ownership-token and epoch checks still run INSIDE the transaction, so fencing atomicity
500
+ * (DUR-006) is preserved identically.
501
+ *
502
+ * Journal write transactions are always top level: the Durable Object client rejects
503
+ * nested transactions, so new journal operations must not wrap this helper inside another
504
+ * transaction.
505
+ */
506
+ const withWriteTransaction = (operation) => (effect) => sql.withTransaction(effect).pipe(Effect.mapError((error) => isSqlError(error) ? storageError(operation)(error) : error), Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }));
507
+ const materialize = Effect.fn("DoJournal.materialize")(function* (threadId, createdAt, emptyTailDigest, producerEpoch) {
508
+ if (threadId.length > MAX_IDENTIFIER_LENGTH) return yield* DoStorageError.make({
509
+ operation: "materialize thread",
510
+ message: "Thread identity exceeds the Durable Object storage bounds."
511
+ });
512
+ yield* checkValueBound("materialize thread", emptyTailDigest);
513
+ yield* withWriteTransaction("materialize transaction")(Effect.gen(function* () {
514
+ const existingRows = yield* sql`
515
+ SELECT
516
+ thread_id,
517
+ created_at,
518
+ tail_sequence,
519
+ tail_digest,
520
+ producer_epoch
521
+ FROM effect_agent_threads
522
+ WHERE thread_id = ${threadId}
523
+ `.pipe(Effect.mapError(storageError("read materialized thread")));
524
+ const existing = yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, existingRows);
525
+ if (existing.length > 1) return yield* DoStorageCorruptionError.make({
526
+ table: "effect_agent_threads",
527
+ rowKey: threadId,
528
+ message: "A thread primary key returned more than one row."
529
+ });
530
+ if (existing.length === 0) {
531
+ yield* sql`
532
+ INSERT INTO effect_agent_threads (
533
+ thread_id,
534
+ created_at,
535
+ tail_sequence,
536
+ tail_digest,
537
+ producer_epoch
538
+ ) VALUES (
539
+ ${threadId},
540
+ ${createdAt},
541
+ 0,
542
+ ${emptyTailDigest},
543
+ ${producerEpoch}
544
+ )
545
+ `.pipe(Effect.mapError(storageError("materialize thread")));
546
+ return;
547
+ }
548
+ if (producerEpoch < existing[0].producer_epoch) return yield* DoFenceRejected.make({
549
+ producerEpoch,
550
+ actualEpoch: existing[0].producer_epoch,
551
+ message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
552
+ });
553
+ if (producerEpoch > existing[0].producer_epoch) yield* sql`
554
+ UPDATE effect_agent_threads
555
+ SET producer_epoch = ${producerEpoch}
556
+ WHERE thread_id = ${threadId}
557
+ `.pipe(Effect.mapError(storageError("advance materialization epoch")));
558
+ }));
559
+ });
560
+ const getThread = Effect.fn("DoJournal.getThread")(function* (threadId) {
561
+ const rows = yield* sql`
562
+ SELECT
563
+ thread_id,
564
+ created_at,
565
+ tail_sequence,
566
+ tail_digest,
567
+ producer_epoch
568
+ FROM effect_agent_threads
569
+ WHERE thread_id = ${threadId}
570
+ `.pipe(Effect.mapError(storageError("read thread")));
571
+ return yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
572
+ });
573
+ const append = Effect.fn("DoJournal.append")(function* (request) {
574
+ if (request.threadId.length > MAX_IDENTIFIER_LENGTH || request.batchId.length > MAX_IDENTIFIER_LENGTH || request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH)) return yield* DoStorageError.make({
575
+ operation: "append canonical batch",
576
+ message: "Canonical identifiers exceed the Durable Object storage bounds."
577
+ });
578
+ yield* checkValueBound("append canonical batch", request.batchJson);
579
+ yield* checkValueBound("append canonical batch", request.batchDigest);
580
+ yield* checkValueBound("append canonical batch", request.tailDigest);
581
+ yield* Effect.forEach(request.records, (record) => checkValueBound("append canonical record", record.recordJson), { discard: true });
582
+ return yield* withWriteTransaction("append transaction")(Effect.gen(function* () {
583
+ const recordIds = request.records.map((record) => record.recordId);
584
+ if (new Set(recordIds).size !== recordIds.length) return yield* DoAppendConflict.make({
585
+ message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
586
+ reason: "record-identity"
587
+ });
588
+ const threadRows = yield* sql`
589
+ SELECT
590
+ thread_id,
591
+ created_at,
592
+ tail_sequence,
593
+ tail_digest,
594
+ producer_epoch
595
+ FROM effect_agent_threads
596
+ WHERE thread_id = ${request.threadId}
597
+ `.pipe(Effect.mapError(storageError("read append tail")));
598
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", request.threadId, threadRows);
599
+ if (request.producerEpoch !== thread.producer_epoch) return yield* DoFenceRejected.make({
600
+ producerEpoch: request.producerEpoch,
601
+ actualEpoch: thread.producer_epoch,
602
+ message: `Producer epoch ${request.producerEpoch} is not the current epoch ${thread.producer_epoch}.`
603
+ });
604
+ const batchRows = yield* sql`
605
+ SELECT
606
+ thread_id,
607
+ batch_id,
608
+ first_sequence,
609
+ last_sequence,
610
+ batch_digest,
611
+ tail_digest,
612
+ batch_json
613
+ FROM effect_agent_canonical_batches
614
+ WHERE thread_id = ${request.threadId}
615
+ AND batch_id = ${request.batchId}
616
+ `.pipe(Effect.mapError(storageError("read idempotent batch")));
617
+ const batches = yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.threadId}/${request.batchId}`, batchRows);
618
+ if (batches.length > 1) return yield* DoStorageCorruptionError.make({
619
+ table: "effect_agent_canonical_batches",
620
+ rowKey: `${request.threadId}/${request.batchId}`,
621
+ message: "A canonical batch primary key returned more than one row."
622
+ });
623
+ if (batches.length === 1) {
624
+ const existing = batches[0];
625
+ if (existing.batch_digest !== request.batchDigest) return yield* DoAppendConflict.make({
626
+ message: `Batch ${request.batchId} already exists with different canonical content.`,
627
+ reason: "batch-digest"
628
+ });
629
+ return RawAppendResult.make({
630
+ firstSequence: existing.first_sequence,
631
+ lastSequence: existing.last_sequence,
632
+ replayed: true,
633
+ tailDigest: existing.tail_digest
634
+ });
635
+ }
636
+ if (request.expectedTailSequence !== thread.tail_sequence || request.expectedTailDigest !== thread.tail_digest) return yield* DoAppendConflict.make({
637
+ message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${thread.tail_sequence}/${thread.tail_digest}.`,
638
+ reason: "tail",
639
+ actualTailSequence: thread.tail_sequence,
640
+ actualTailDigest: thread.tail_digest
641
+ });
642
+ if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) return yield* DoStorageError.make({
643
+ operation: "append canonical batch",
644
+ message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`
645
+ });
646
+ const existingRecords = [];
647
+ for (const chunk of chunked(recordIds, 90)) {
648
+ const existingRecordRows = yield* sql`
649
+ SELECT
650
+ thread_id,
651
+ sequence,
652
+ record_id,
653
+ batch_id,
654
+ record_json
655
+ FROM effect_agent_canonical_records
656
+ WHERE thread_id = ${request.threadId}
657
+ AND record_id IN ${sql.in([...chunk])}
658
+ ORDER BY sequence
659
+ `.pipe(Effect.mapError(storageError("check canonical record identities")));
660
+ existingRecords.push(...yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/record_ids`, existingRecordRows));
661
+ }
662
+ if (existingRecords.length > 0) return yield* DoAppendConflict.make({
663
+ message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
664
+ reason: "record-identity"
665
+ });
666
+ const firstSequence = yield* Schema.decodeEffect(CanonicalSequence)(thread.tail_sequence + 1).pipe(Effect.mapError((error) => DoStorageError.make({
667
+ cause: error,
668
+ operation: "append canonical batch",
669
+ message: error.message
670
+ })));
671
+ const lastSequence = yield* Schema.decodeEffect(CanonicalSequence)(firstSequence + request.records.length - 1).pipe(Effect.mapError((error) => DoStorageError.make({
672
+ cause: error,
673
+ operation: "append canonical batch",
674
+ message: error.message
675
+ })));
676
+ yield* sql`
677
+ INSERT INTO effect_agent_canonical_batches (
678
+ thread_id,
679
+ batch_id,
680
+ first_sequence,
681
+ last_sequence,
682
+ batch_digest,
683
+ tail_digest,
684
+ batch_json
685
+ ) VALUES (
686
+ ${request.threadId},
687
+ ${request.batchId},
688
+ ${firstSequence},
689
+ ${lastSequence},
690
+ ${request.batchDigest},
691
+ ${request.tailDigest},
692
+ ${request.batchJson}
693
+ )
694
+ `.pipe(Effect.mapError(storageError("insert canonical batch")));
695
+ yield* failpoint("append:after-batch-insert");
696
+ yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
697
+ yield* sql`
698
+ INSERT INTO effect_agent_canonical_records (
699
+ thread_id,
700
+ sequence,
701
+ record_id,
702
+ batch_id,
703
+ record_json
704
+ ) VALUES (
705
+ ${request.threadId},
706
+ ${firstSequence + index},
707
+ ${record.recordId},
708
+ ${request.batchId},
709
+ ${record.recordJson}
710
+ )
711
+ `.pipe(Effect.mapError(storageError("insert canonical record")));
712
+ const canonical = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.recordJson).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
713
+ table: "effect_agent_canonical_records",
714
+ rowKey: record.recordId,
715
+ message: error.message
716
+ })));
717
+ yield* indexCanonicalRecord(request.threadId, canonical).pipe(Effect.provideService(SqlClient.SqlClient, sql), Effect.mapError(storageError("index canonical record")));
718
+ yield* failpoint("append:after-record-insert");
719
+ }), { discard: true });
720
+ yield* sql`
721
+ UPDATE effect_agent_threads
722
+ SET
723
+ tail_sequence = ${lastSequence},
724
+ tail_digest = ${request.tailDigest},
725
+ producer_epoch = ${request.producerEpoch}
726
+ WHERE thread_id = ${request.threadId}
727
+ `.pipe(Effect.mapError(storageError("advance thread tail")));
728
+ yield* failpoint("append:after-tail-update");
729
+ return RawAppendResult.make({
730
+ firstSequence,
731
+ lastSequence,
732
+ replayed: false,
733
+ tailDigest: request.tailDigest
734
+ });
735
+ }));
736
+ });
737
+ const read = Effect.fnUntraced(function* (request) {
738
+ const planRows = yield* sql`
739
+ SELECT
740
+ sequence,
741
+ length(CAST(record_json AS BLOB)) AS record_json_bytes
742
+ FROM effect_agent_canonical_records
743
+ WHERE thread_id = ${request.threadId}
744
+ AND sequence > ${request.fromSequenceExclusive}
745
+ ORDER BY sequence
746
+ LIMIT ${request.limit}
747
+ `.pipe(Effect.mapError(storageError("read canonical records")));
748
+ const plan = yield* decodeRows(Schema.Array(ReadPlanRow), "effect_agent_canonical_records", `${request.threadId}>${request.fromSequenceExclusive}`, planRows);
749
+ const mismatch = () => DoStorageCorruptionError.make({
750
+ table: "effect_agent_canonical_records",
751
+ rowKey: `${request.threadId}>${request.fromSequenceExclusive}`,
752
+ message: "Canonical read membership, sequence, or payload size changed from its read plan."
753
+ });
754
+ if (plan.length > request.limit) return yield* mismatch();
755
+ const pages = [];
756
+ let page;
757
+ let pageBytes = 0;
758
+ let previousSequence = request.fromSequenceExclusive;
759
+ for (const row of plan) {
760
+ if (row.sequence !== previousSequence + 1) return yield* mismatch();
761
+ previousSequence = row.sequence;
762
+ if (page === void 0 || pageBytes + row.record_json_bytes > MAX_READ_PAGE_JSON_BYTES) {
763
+ page = [row];
764
+ pages.push(page);
765
+ pageBytes = row.record_json_bytes;
766
+ } else {
767
+ page.push(row);
768
+ pageBytes += row.record_json_bytes;
769
+ }
770
+ }
771
+ const readPage = Effect.fn("DoJournal.readPage")(function* (page) {
772
+ const rows = yield* sql`
773
+ SELECT thread_id, sequence, record_id, batch_id, record_json
774
+ FROM effect_agent_canonical_records
775
+ WHERE thread_id = ${request.threadId}
776
+ AND sequence >= ${page[0].sequence}
777
+ AND sequence <= ${page[page.length - 1].sequence}
778
+ ORDER BY sequence
779
+ `.pipe(Effect.mapError(storageError("read canonical records")));
780
+ const decoded = yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/${page[0].sequence}`, rows);
781
+ if (decoded.length !== page.length || decoded.some((row, index) => row.thread_id !== request.threadId || row.sequence !== page[index].sequence || storedTextBytes(row.record_json) !== page[index].record_json_bytes)) return yield* mismatch();
782
+ return decoded;
783
+ });
784
+ return {
785
+ count: plan.length,
786
+ records: Stream.fromIterable(pages).pipe(Stream.flatMap((page) => Stream.fromIterableEffect(readPage(page))))
787
+ };
788
+ });
789
+ const exportThread = Effect.fn("DoJournal.exportThread")(function* (threadId) {
790
+ return yield* sql.withTransaction(Effect.gen(function* () {
791
+ const threadRows = yield* sql`
792
+ SELECT
793
+ thread_id,
794
+ created_at,
795
+ tail_sequence,
796
+ tail_digest,
797
+ producer_epoch
798
+ FROM effect_agent_threads
799
+ WHERE thread_id = ${threadId}
800
+ `.pipe(Effect.mapError(storageError("export thread")));
801
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", threadId, threadRows);
802
+ yield* failpoint("export:after-thread-read");
803
+ if (thread.tail_sequence > MAX_RECORDS_PER_THREAD) return yield* DoStorageError.make({
804
+ operation: "export thread",
805
+ message: "The thread exceeds the current export record limit."
806
+ });
807
+ const records = [];
808
+ let afterSequence = ZERO_SEQUENCE;
809
+ while (afterSequence < thread.tail_sequence) {
810
+ const limit = Math.min(1024, thread.tail_sequence - afterSequence);
811
+ const request = RawReadRequest.make({
812
+ threadId,
813
+ fromSequenceExclusive: afterSequence,
814
+ limit
815
+ });
816
+ const plan = yield* read(request);
817
+ const page = yield* Stream.runCollect(plan.records);
818
+ if (page.length !== limit || page.some((record, index) => record.sequence !== afterSequence + index + 1)) return yield* DoStorageCorruptionError.make({
819
+ table: "effect_agent_canonical_records",
820
+ rowKey: threadId,
821
+ message: "The exported canonical prefix is not contiguous through its captured tail."
822
+ });
823
+ records.push(...page);
824
+ afterSequence = page[page.length - 1].sequence;
825
+ }
826
+ if ((yield* sql`SELECT sequence FROM effect_agent_canonical_records WHERE thread_id=${threadId} AND sequence > ${thread.tail_sequence} LIMIT 1`.pipe(Effect.mapError(storageError("verify export tail")))).length !== 0) return yield* DoStorageCorruptionError.make({
827
+ table: "effect_agent_canonical_records",
828
+ rowKey: threadId,
829
+ message: "Canonical records exist beyond the captured thread tail."
830
+ });
831
+ return RawThreadExport.make({
832
+ thread,
833
+ records
834
+ });
835
+ })).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(storageError("export transaction")(error))));
836
+ });
837
+ const saveCheckpoint = Effect.fn("DoJournal.saveCheckpoint")(function* (checkpoint) {
838
+ if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH) return yield* DoStorageError.make({
839
+ operation: "save checkpoint",
840
+ message: "Checkpoint identity exceeds the Durable Object storage bounds."
841
+ });
842
+ yield* checkValueBound("save checkpoint", checkpoint.checkpointJson);
843
+ yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
844
+ const threadRows = yield* sql`
845
+ SELECT
846
+ thread_id,
847
+ created_at,
848
+ tail_sequence,
849
+ tail_digest,
850
+ producer_epoch
851
+ FROM effect_agent_threads
852
+ WHERE thread_id = ${checkpoint.threadId}
853
+ `.pipe(Effect.mapError(storageError("read checkpoint tail")));
854
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", checkpoint.threadId, threadRows);
855
+ if (checkpoint.throughSequence > thread.tail_sequence) return yield* DoCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${thread.tail_sequence}.` });
856
+ const checkpointRows = yield* sql`
857
+ SELECT
858
+ thread_id,
859
+ through_sequence,
860
+ tail_digest,
861
+ checkpoint_json
862
+ FROM effect_agent_checkpoints
863
+ WHERE thread_id = ${checkpoint.threadId}
864
+ AND through_sequence = ${checkpoint.throughSequence}
865
+ `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
866
+ const existing = yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.threadId}/${checkpoint.throughSequence}`, checkpointRows);
867
+ if (existing.length > 1) return yield* DoStorageCorruptionError.make({
868
+ table: "effect_agent_checkpoints",
869
+ rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
870
+ message: "A checkpoint primary key returned more than one row."
871
+ });
872
+ if (existing.length === 1) {
873
+ if (existing[0].tail_digest !== checkpoint.tailDigest || existing[0].checkpoint_json !== checkpoint.checkpointJson) return yield* DoCheckpointConflict.make({ message: "A different checkpoint already exists at this canonical sequence." });
874
+ return;
875
+ }
876
+ yield* sql`
877
+ INSERT INTO effect_agent_checkpoints (
878
+ thread_id,
879
+ through_sequence,
880
+ tail_digest,
881
+ checkpoint_json
882
+ ) VALUES (
883
+ ${checkpoint.threadId},
884
+ ${checkpoint.throughSequence},
885
+ ${checkpoint.tailDigest},
886
+ ${checkpoint.checkpointJson}
887
+ )
888
+ `.pipe(Effect.mapError(storageError("insert checkpoint")));
889
+ }));
890
+ });
891
+ const saveRecoveryCheckpoint = Effect.fn("DoJournal.saveRecoveryCheckpoint")(function* (request, checkpointJson) {
892
+ const { checkpoint } = request;
893
+ if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH) return yield* DoStorageError.make({
894
+ operation: "save recovery checkpoint",
895
+ message: "Checkpoint identity exceeds the Durable Object storage bounds."
896
+ });
897
+ yield* checkValueBound("save recovery checkpoint", checkpointJson);
898
+ yield* failpoint("save-recovery-checkpoint:before");
899
+ yield* withWriteTransaction("recovery checkpoint transaction")(Effect.gen(function* () {
900
+ const thread = (yield* getThread(checkpoint.threadId))[0];
901
+ if (thread === void 0) return yield* ThreadNotMaterialized.make({ threadId: checkpoint.threadId });
902
+ if (request.producerEpoch !== thread.producer_epoch) return yield* FenceRejected.make({
903
+ threadId: checkpoint.threadId,
904
+ actualEpoch: thread.producer_epoch,
905
+ attemptedEpoch: request.producerEpoch
906
+ });
907
+ if (checkpoint.throughSequence > thread.tail_sequence) return yield* CheckpointRejected.make({
908
+ threadId: checkpoint.threadId,
909
+ reason: "ahead-of-tail"
910
+ });
911
+ const digests = checkpoint.throughSequence === 0 ? [EMPTY_TAIL_DIGEST] : yield* getTailDigestAt(checkpoint.threadId, checkpoint.throughSequence);
912
+ if (digests.length !== 1 || digests[0] !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
913
+ threadId: checkpoint.threadId,
914
+ reason: "digest-mismatch"
915
+ });
916
+ yield* sql`
917
+ INSERT INTO effect_agent_recovery_checkpoints (thread_id, through_sequence, tail_digest, checkpoint_json)
918
+ VALUES (${checkpoint.threadId}, ${checkpoint.throughSequence}, ${checkpoint.tailDigest}, ${checkpointJson})
919
+ ON CONFLICT (thread_id) DO UPDATE SET
920
+ through_sequence = excluded.through_sequence,
921
+ tail_digest = excluded.tail_digest,
922
+ checkpoint_json = excluded.checkpoint_json
923
+ WHERE excluded.through_sequence >= effect_agent_recovery_checkpoints.through_sequence
924
+ `.pipe(Effect.mapError(storageError("save recovery checkpoint")));
925
+ }));
926
+ yield* failpoint("save-recovery-checkpoint:after");
927
+ });
928
+ const loadRecoveryCheckpoint = Effect.fn("DoJournal.loadRecoveryCheckpoint")(function* (threadId) {
929
+ const rows = yield* sql`
930
+ SELECT thread_id, through_sequence, tail_digest, checkpoint_json
931
+ FROM effect_agent_recovery_checkpoints
932
+ WHERE thread_id = ${threadId}
933
+ `.pipe(Effect.mapError(storageError("load recovery checkpoint")));
934
+ return yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_recovery_checkpoints", threadId, rows);
935
+ });
936
+ const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (threadId, atOrBeforeSequence) {
937
+ const rows = yield* sql`
938
+ SELECT
939
+ thread_id,
940
+ through_sequence,
941
+ tail_digest,
942
+ checkpoint_json
943
+ FROM effect_agent_checkpoints
944
+ WHERE thread_id = ${threadId}
945
+ AND through_sequence <= ${atOrBeforeSequence}
946
+ ORDER BY through_sequence DESC
947
+ LIMIT 1
948
+ `.pipe(Effect.mapError(storageError("load checkpoint")));
949
+ return yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${threadId}<=${atOrBeforeSequence}`, rows);
950
+ });
951
+ const getTailDigestAt = Effect.fn("DoJournal.getTailDigestAt")(function* (threadId, sequence) {
952
+ if (sequence === 0) {
953
+ const threads = yield* getThread(threadId);
954
+ return threads.length === 0 ? [] : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : void 0].filter((value) => value !== void 0);
955
+ }
956
+ const rows = yield* sql`
957
+ SELECT
958
+ thread_id,
959
+ batch_id,
960
+ first_sequence,
961
+ last_sequence,
962
+ batch_digest,
963
+ tail_digest,
964
+ batch_json
965
+ FROM effect_agent_canonical_batches
966
+ WHERE thread_id = ${threadId}
967
+ AND last_sequence = ${sequence}
968
+ `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
969
+ return (yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${threadId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
970
+ });
971
+ return {
972
+ append,
973
+ checkValueBound,
974
+ exportThread,
975
+ getThread,
976
+ getTailDigestAt,
977
+ loadCheckpoint,
978
+ loadRecoveryCheckpoint,
979
+ saveRecoveryCheckpoint,
980
+ materialize,
981
+ read,
982
+ saveCheckpoint,
983
+ scanStoredPayloads: Effect.fn("DoJournal.scanStoredPayloads")(function* () {
984
+ return yield* sql.withTransaction(Effect.gen(function* () {
985
+ const threads = yield* sql`
986
+ SELECT
987
+ thread_id,
988
+ created_at,
989
+ tail_sequence,
990
+ tail_digest,
991
+ producer_epoch
992
+ FROM effect_agent_threads
993
+ ORDER BY thread_id
994
+ `.pipe(Effect.mapError(storageError("scan threads")));
995
+ const batches = yield* sql`
996
+ SELECT
997
+ thread_id,
998
+ batch_id,
999
+ first_sequence,
1000
+ last_sequence,
1001
+ batch_digest,
1002
+ tail_digest,
1003
+ batch_json
1004
+ FROM effect_agent_canonical_batches
1005
+ ORDER BY thread_id, first_sequence
1006
+ `.pipe(Effect.mapError(storageError("scan canonical batches")));
1007
+ const records = yield* sql`
1008
+ SELECT
1009
+ thread_id,
1010
+ sequence,
1011
+ record_id,
1012
+ batch_id,
1013
+ record_json
1014
+ FROM effect_agent_canonical_records
1015
+ ORDER BY thread_id, sequence
1016
+ `.pipe(Effect.mapError(storageError("scan canonical records")));
1017
+ const checkpoints = yield* sql`
1018
+ SELECT
1019
+ thread_id,
1020
+ through_sequence,
1021
+ tail_digest,
1022
+ checkpoint_json
1023
+ FROM effect_agent_checkpoints
1024
+ ORDER BY thread_id, through_sequence
1025
+ `.pipe(Effect.mapError(storageError("scan checkpoints")));
1026
+ return {
1027
+ threads: yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", "startup_scan", threads),
1028
+ batches: yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
1029
+ records: yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
1030
+ checkpoints: yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
1031
+ };
1032
+ })).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(storageError("startup scan transaction")(error))));
1033
+ }),
1034
+ withWriteTransaction
1035
+ };
1036
+ };
1037
+ const initializeDoJournal = ensureCurrentStorage;
1038
+ //#endregion
1039
+ export { initializeDoJournal as a, decodeRows as i, RawCheckpoint as n, RawReadRequest as r, RawAppendRequest as t };
1040
+
1041
+ //# sourceMappingURL=do-journal-THW8-ZeP.mjs.map