@effect-agent/storage-cloudflare 0.1.0-beta.6 → 0.1.0-beta.60

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