@effect-agent/storage-sqlite 0.1.0-beta.45 → 0.1.0-beta.46

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 (58) hide show
  1. package/dist/SqliteActivityStore.d.mts +14 -0
  2. package/dist/SqliteActivityStore.mjs +310 -0
  3. package/dist/SqliteActivityStore.mjs.map +1 -0
  4. package/dist/SqliteScheduleStore.d.mts +14 -0
  5. package/dist/SqliteScheduleStore.mjs +255 -0
  6. package/dist/SqliteScheduleStore.mjs.map +1 -0
  7. package/dist/SqliteStorageConfig.d.mts +34 -0
  8. package/dist/SqliteStorageConfig.mjs +39 -0
  9. package/dist/SqliteStorageConfig.mjs.map +1 -0
  10. package/dist/{sqlite-storage-failpoint-Cr0SXflP.d.mts → SqliteStorageError-W1oM5ka3.d.mts} +6 -15
  11. package/dist/SqliteStorageError.d.mts +2 -0
  12. package/dist/SqliteStorageError.mjs +144 -0
  13. package/dist/SqliteStorageError.mjs.map +1 -0
  14. package/dist/SqliteStorageFailpoint.d.mts +17 -0
  15. package/dist/{sqlite-storage-failpoint-DIwWk5dw.mjs → SqliteStorageFailpoint.mjs} +5 -3
  16. package/dist/SqliteStorageFailpoint.mjs.map +1 -0
  17. package/dist/{testing.d.mts → SqliteStorageFailpointTesting.d.mts} +3 -3
  18. package/dist/{testing.mjs → SqliteStorageFailpointTesting.mjs} +3 -3
  19. package/dist/SqliteStorageFailpointTesting.mjs.map +1 -0
  20. package/dist/SqliteStorageVersion-DwQbn4bw.d.mts +9 -0
  21. package/dist/SqliteStorageVersion.d.mts +2 -0
  22. package/dist/SqliteStorageVersion.mjs +8 -0
  23. package/dist/SqliteStorageVersion.mjs.map +1 -0
  24. package/dist/SqliteSubmissionLedger.d.mts +23 -0
  25. package/dist/SqliteSubmissionLedger.mjs +1656 -0
  26. package/dist/SqliteSubmissionLedger.mjs.map +1 -0
  27. package/dist/SqliteSubscriptionStore.d.mts +13 -0
  28. package/dist/SqliteSubscriptionStore.mjs +596 -0
  29. package/dist/SqliteSubscriptionStore.mjs.map +1 -0
  30. package/dist/SqliteThreadStore.d.mts +49 -0
  31. package/dist/SqliteThreadStore.mjs +422 -0
  32. package/dist/SqliteThreadStore.mjs.map +1 -0
  33. package/dist/index.d.mts +10 -108
  34. package/dist/index.mjs +10 -4265
  35. package/dist/migrations-B82C9A3r.mjs +293 -0
  36. package/dist/migrations-B82C9A3r.mjs.map +1 -0
  37. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  38. package/dist/sqlite-journal-C5SZnSdC.mjs +656 -0
  39. package/dist/sqlite-journal-C5SZnSdC.mjs.map +1 -0
  40. package/package.json +1 -1
  41. package/src/{sqlite-activity-store.ts → SqliteActivityStore.ts} +2 -2
  42. package/src/{sqlite-schedule-store.ts → SqliteScheduleStore.ts} +9 -7
  43. package/src/{errors.ts → SqliteStorageError.ts} +1 -1
  44. package/src/{sqlite-storage-failpoint.ts → SqliteStorageFailpoint.ts} +4 -1
  45. package/src/{sqlite-storage-failpoint-testing.ts → SqliteStorageFailpointTesting.ts} +1 -1
  46. package/src/SqliteStorageVersion.ts +2 -0
  47. package/src/{sqlite-ledger.ts → SqliteSubmissionLedger.ts} +17 -15
  48. package/src/{sqlite-subscription-store.ts → SqliteSubscriptionStore.ts} +14 -12
  49. package/src/{sqlite-thread-store.ts → SqliteThreadStore.ts} +19 -21
  50. package/src/index.ts +9 -10
  51. package/src/{sqlite-journal.ts → internal/sqlite-journal.ts} +5 -5
  52. package/dist/index.mjs.map +0 -1
  53. package/dist/sqlite-storage-failpoint-DIwWk5dw.mjs.map +0 -1
  54. package/dist/testing.mjs.map +0 -1
  55. package/src/sqlite-memory-store.ts +0 -7
  56. package/src/testing.ts +0 -2
  57. /package/src/{sqlite-storage-config.ts → SqliteStorageConfig.ts} +0 -0
  58. /package/src/{migrations.ts → internal/migrations.ts} +0 -0
@@ -0,0 +1,656 @@
1
+ import { SqliteStorageConfig } from "./SqliteStorageConfig.mjs";
2
+ import { SqliteAppendConflict, SqliteCheckpointConflict, SqliteFenceRejected, SqliteStorageCompatibilityError, SqliteStorageCorruptionError, SqliteStorageError, SqliteWriteContention } from "./SqliteStorageError.mjs";
3
+ import { SqliteStorageFailpoint } from "./SqliteStorageFailpoint.mjs";
4
+ import { n as sqliteMigrations } from "./migrations-B82C9A3r.mjs";
5
+ import { CanonicalSequence, ProducerEpoch } from "@effect-agent/thread/Records";
6
+ import { Effect, Exit, Schema } from "effect";
7
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
8
+ import { SqliteMigrator } from "@effect/sql-sqlite-node";
9
+ //#region src/internal/sqlite-journal.ts
10
+ const BoundedStoredText = Schema.String.check(Schema.isMaxLength(16777216));
11
+ const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
12
+ const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
13
+ const MAX_RECORDS_PER_THREAD = 65536;
14
+ const MAX_STORED_TEXT_BYTES = 16777216;
15
+ const MAX_IDENTIFIER_LENGTH = 1024;
16
+ const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
17
+ var SqliteVersionRow = class extends Schema.Class("SqliteVersionRow")({ user_version: NonNegativeInt }) {};
18
+ var SqliteJournalModeRow = class extends Schema.Class("SqliteJournalModeRow")({ journal_mode: Schema.NonEmptyString.check(Schema.isMaxLength(32)) }) {};
19
+ var SqliteNameRow = class extends Schema.Class("SqliteNameRow")({ name: BoundedIdentifier }) {};
20
+ var ThreadRow = class extends Schema.Class("ThreadRow")({
21
+ thread_id: BoundedIdentifier,
22
+ created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
23
+ producer_epoch: ProducerEpoch,
24
+ tail_digest: BoundedStoredText,
25
+ tail_sequence: CanonicalSequence
26
+ }) {};
27
+ var BatchRow = class extends Schema.Class("BatchRow")({
28
+ batch_digest: BoundedStoredText,
29
+ batch_id: BoundedIdentifier,
30
+ batch_json: BoundedStoredText,
31
+ thread_id: BoundedIdentifier,
32
+ first_sequence: CanonicalSequence,
33
+ last_sequence: CanonicalSequence,
34
+ tail_digest: BoundedStoredText
35
+ }) {};
36
+ var RecordRow = class extends Schema.Class("RecordRow")({
37
+ batch_id: BoundedIdentifier,
38
+ thread_id: BoundedIdentifier,
39
+ record_id: BoundedIdentifier,
40
+ record_json: BoundedStoredText,
41
+ sequence: CanonicalSequence
42
+ }) {};
43
+ var CheckpointRow = class extends Schema.Class("CheckpointRow")({
44
+ checkpoint_json: BoundedStoredText,
45
+ thread_id: BoundedIdentifier,
46
+ tail_digest: BoundedStoredText,
47
+ through_sequence: CanonicalSequence
48
+ }) {};
49
+ var RawRecord = class extends Schema.Class("@effect-agent/storage-sqlite/RawRecord")({
50
+ recordId: BoundedIdentifier,
51
+ recordJson: BoundedStoredText
52
+ }) {};
53
+ var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-sqlite/RawAppendRequest")({
54
+ batchDigest: BoundedStoredText,
55
+ batchId: BoundedIdentifier,
56
+ batchJson: BoundedStoredText,
57
+ threadId: BoundedIdentifier,
58
+ expectedTailDigest: BoundedStoredText,
59
+ expectedTailSequence: CanonicalSequence,
60
+ producerEpoch: ProducerEpoch,
61
+ records: Schema.NonEmptyArray(RawRecord).check(Schema.isMaxLength(256)),
62
+ tailDigest: BoundedStoredText
63
+ }) {};
64
+ var RawAppendResult = class extends Schema.Class("@effect-agent/storage-sqlite/RawAppendResult")({
65
+ firstSequence: CanonicalSequence,
66
+ lastSequence: CanonicalSequence,
67
+ replayed: Schema.Boolean,
68
+ tailDigest: BoundedStoredText
69
+ }) {};
70
+ var RawReadRequest = class extends Schema.Class("@effect-agent/storage-sqlite/RawReadRequest")({
71
+ threadId: BoundedIdentifier,
72
+ fromSequenceExclusive: CanonicalSequence,
73
+ limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
74
+ }) {};
75
+ var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-sqlite/RawCheckpoint")({
76
+ checkpointJson: BoundedStoredText,
77
+ threadId: BoundedIdentifier,
78
+ tailDigest: BoundedStoredText,
79
+ throughSequence: CanonicalSequence
80
+ }) {};
81
+ var RawThreadExport = class extends Schema.Class("@effect-agent/storage-sqlite/RawThreadExport")({
82
+ batches: Schema.Array(BatchRow),
83
+ checkpoints: Schema.Array(CheckpointRow),
84
+ thread: ThreadRow,
85
+ records: Schema.Array(RecordRow)
86
+ }) {};
87
+ const storageError = (operation) => (error) => SqliteStorageError.make({
88
+ cause: error,
89
+ operation,
90
+ message: error.message
91
+ });
92
+ /** Decode raw SQLite rows against a Schema, reporting failures as typed corruption. */
93
+ const decodeRows = Effect.fn("SqliteJournal.decodeRows")((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
94
+ table,
95
+ rowKey,
96
+ message: String(error)
97
+ }))));
98
+ /** Decode exactly one raw SQLite row against a Schema, reporting failures as typed corruption. */
99
+ const decodeSingleRow = Effect.fn("SqliteJournal.decodeSingleRow")((schema, table, rowKey, rows) => decodeRows(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(SqliteStorageCorruptionError.make({
100
+ table,
101
+ rowKey,
102
+ message: `Expected exactly one row but found ${decoded.length}.`
103
+ })))));
104
+ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function* () {
105
+ const sql = yield* SqlClient.SqlClient;
106
+ const { hit: failpoint } = yield* SqliteStorageFailpoint;
107
+ const { busyTimeout } = yield* SqliteStorageConfig;
108
+ yield* sql`PRAGMA foreign_keys = ON`.pipe(Effect.mapError(storageError("enable foreign keys")));
109
+ yield* sql.unsafe(`PRAGMA busy_timeout = ${busyTimeout}`).pipe(Effect.mapError(storageError("configure busy timeout")));
110
+ const journalModeRows = yield* sql`PRAGMA journal_mode`.pipe(Effect.mapError(storageError("read journal mode")));
111
+ const journalMode = yield* decodeSingleRow(Schema.Array(SqliteJournalModeRow), "pragma_journal_mode", "singleton", journalModeRows);
112
+ if (journalMode.journal_mode.toLowerCase() !== "wal") return yield* SqliteStorageCompatibilityError.make({
113
+ actualVersion: 0,
114
+ supportedVersion: 7,
115
+ message: `SQLite WAL mode is required; the database reported ${journalMode.journal_mode}.`
116
+ });
117
+ const versionRows = yield* sql`PRAGMA user_version`.pipe(Effect.mapError(storageError("read storage version")));
118
+ const version = yield* decodeSingleRow(Schema.Array(SqliteVersionRow), "pragma_user_version", "singleton", versionRows);
119
+ if (version.user_version !== 0 && version.user_version !== 7) return yield* SqliteStorageCompatibilityError.make({
120
+ actualVersion: version.user_version,
121
+ supportedVersion: 7,
122
+ message: `The SQLite file uses private-development storage version ${version.user_version}; this build supports exactly version 7. Reset the database file explicitly; automatic stored-data migrations are not provided during private development.`
123
+ });
124
+ if (version.user_version === 0) {
125
+ const existingRows = yield* sql`
126
+ SELECT name
127
+ FROM sqlite_master
128
+ WHERE type = 'table'
129
+ AND name LIKE 'effect_agent_%'
130
+ ORDER BY name
131
+ `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
132
+ if ((yield* decodeRows(Schema.Array(SqliteNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* SqliteStorageCompatibilityError.make({
133
+ actualVersion: 0,
134
+ supportedVersion: 7,
135
+ message: "The SQLite file contains unversioned Effect Agent tables. Reset it explicitly; refusing to mutate ambiguous stored data."
136
+ });
137
+ yield* SqliteMigrator.run({ loader: sqliteMigrations }).pipe(Effect.mapError((error) => SqliteStorageError.make({
138
+ cause: error,
139
+ operation: "initialize current storage",
140
+ message: error.message
141
+ })));
142
+ }
143
+ const requiredRows = yield* sql`
144
+ SELECT name
145
+ FROM sqlite_master
146
+ WHERE type = 'table'
147
+ AND name IN (
148
+ 'effect_agent_threads',
149
+ 'effect_agent_canonical_batches',
150
+ 'effect_agent_canonical_records',
151
+ 'effect_agent_checkpoints',
152
+ 'effect_agent_submissions',
153
+ 'effect_agent_submission_ownership',
154
+ 'effect_agent_attempts',
155
+ 'effect_agent_settlement_reservations',
156
+ 'effect_agent_abort_intents',
157
+ 'effect_agent_approval_decisions',
158
+ 'effect_agent_unknown_resolutions',
159
+ 'effect_agent_schedules'
160
+ )
161
+ ORDER BY name
162
+ `.pipe(Effect.mapError(storageError("verify storage tables")));
163
+ if ((yield* decodeRows(Schema.Array(SqliteNameRow), "sqlite_master", "required_tables", requiredRows)).length !== 12) return yield* SqliteStorageCompatibilityError.make({
164
+ actualVersion: 7,
165
+ supportedVersion: 7,
166
+ message: "The SQLite file claims the current format but is missing required tables. Reset the corrupt private-development data."
167
+ });
168
+ const classifyWriteFailure = (operation) => (error) => error.reason._tag === "LockTimeoutError" ? SqliteWriteContention.make({
169
+ cause: error,
170
+ operation,
171
+ message: `Another producer holds the SQLite write lock; ${operation} is safe to retry.`
172
+ }) : storageError(operation)(error);
173
+ /**
174
+ * Runs one journal write transaction under `BEGIN IMMEDIATE`. SQLite's deferred `BEGIN`
175
+ * would let a read-then-write transaction start as a reader and fail with
176
+ * SQLITE_BUSY_SNAPSHOT on upgrade, which `busy_timeout` never retries. Taking the write
177
+ * lock up front keeps cross-owner contention inside the bounded busy retry; a lock
178
+ * timeout is classified as the retryable SqliteWriteContention. A failed `BEGIN` leaves
179
+ * no transaction, so no rollback is attempted for it.
180
+ *
181
+ * Journal write transactions are always top level. Nesting one inside another would
182
+ * deadlock the single-connection client, so new journal operations must not wrap this
183
+ * helper inside another transaction.
184
+ */
185
+ const withWriteTransaction = (operation) => (effect) => Effect.uninterruptibleMask((restore) => Effect.scoped(Effect.gen(function* () {
186
+ const connection = yield* sql.reserve.pipe(Effect.mapError(classifyWriteFailure(operation)));
187
+ yield* connection.executeUnprepared("BEGIN IMMEDIATE", [], void 0).pipe(Effect.mapError(classifyWriteFailure(operation)));
188
+ const exit = yield* restore(Effect.provideService(effect, sql.transactionService, [connection, 0])).pipe(Effect.exit);
189
+ if (Exit.isSuccess(exit)) {
190
+ yield* connection.executeUnprepared("COMMIT", [], void 0).pipe(Effect.mapError(classifyWriteFailure(operation)));
191
+ return exit.value;
192
+ }
193
+ yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], void 0));
194
+ return yield* exit;
195
+ })).pipe(Effect.withSpan("SqliteJournal.withWriteTransaction", { attributes: { operation } })));
196
+ /**
197
+ * Runs a read-only snapshot under a deferred transaction. Effect's SQLite client now
198
+ * starts every writable-client `withTransaction` with `BEGIN IMMEDIATE`, which is the
199
+ * right default for mutations but would make exports take the write lock and block a
200
+ * concurrent append. Reserving the connection and beginning explicitly preserves the
201
+ * adapter's snapshot-with-concurrent-writer contract. As with the write helper, a failed
202
+ * `BEGIN` is reported directly because there is no transaction to roll back.
203
+ */
204
+ const withReadTransaction = (operation) => (effect) => Effect.uninterruptibleMask((restore) => Effect.scoped(Effect.gen(function* () {
205
+ const connection = yield* sql.reserve.pipe(Effect.mapError(storageError(operation)));
206
+ yield* connection.executeUnprepared("BEGIN", [], void 0).pipe(Effect.mapError(storageError(operation)));
207
+ const exit = yield* restore(Effect.provideService(effect, sql.transactionService, [connection, 0])).pipe(Effect.exit);
208
+ if (Exit.isSuccess(exit)) {
209
+ yield* connection.executeUnprepared("COMMIT", [], void 0).pipe(Effect.mapError(storageError(operation)));
210
+ return exit.value;
211
+ }
212
+ yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], void 0));
213
+ return yield* exit;
214
+ })).pipe(Effect.withSpan("SqliteJournal.withReadTransaction", { attributes: { operation } })));
215
+ const materialize = Effect.fn("SqliteJournal.materialize")(function* (threadId, createdAt, emptyTailDigest, producerEpoch) {
216
+ if (threadId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(emptyTailDigest) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
217
+ operation: "materialize thread",
218
+ message: "Thread identity or initial digest exceeds the SQLite storage bounds."
219
+ });
220
+ yield* withWriteTransaction("materialize transaction")(Effect.gen(function* () {
221
+ const existingRows = yield* sql`
222
+ SELECT
223
+ thread_id,
224
+ created_at,
225
+ tail_sequence,
226
+ tail_digest,
227
+ producer_epoch
228
+ FROM effect_agent_threads
229
+ WHERE thread_id = ${threadId}
230
+ `.pipe(Effect.mapError(storageError("read materialized thread")));
231
+ const existing = yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, existingRows);
232
+ if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
233
+ table: "effect_agent_threads",
234
+ rowKey: threadId,
235
+ message: "A thread primary key returned more than one row."
236
+ });
237
+ if (existing.length === 0) {
238
+ yield* sql`
239
+ INSERT INTO effect_agent_threads (
240
+ thread_id,
241
+ created_at,
242
+ tail_sequence,
243
+ tail_digest,
244
+ producer_epoch
245
+ ) VALUES (
246
+ ${threadId},
247
+ ${createdAt},
248
+ 0,
249
+ ${emptyTailDigest},
250
+ ${producerEpoch}
251
+ )
252
+ `.pipe(Effect.mapError(storageError("materialize thread")));
253
+ return;
254
+ }
255
+ if (producerEpoch < existing[0].producer_epoch) return yield* SqliteFenceRejected.make({
256
+ producerEpoch,
257
+ actualEpoch: existing[0].producer_epoch,
258
+ message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
259
+ });
260
+ if (producerEpoch > existing[0].producer_epoch) yield* sql`
261
+ UPDATE effect_agent_threads
262
+ SET producer_epoch = ${producerEpoch}
263
+ WHERE thread_id = ${threadId}
264
+ `.pipe(Effect.mapError(storageError("advance materialization epoch")));
265
+ }));
266
+ });
267
+ const getThread = Effect.fn("SqliteJournal.getThread")(function* (threadId) {
268
+ const rows = yield* sql`
269
+ SELECT
270
+ thread_id,
271
+ created_at,
272
+ tail_sequence,
273
+ tail_digest,
274
+ producer_epoch
275
+ FROM effect_agent_threads
276
+ WHERE thread_id = ${threadId}
277
+ `.pipe(Effect.mapError(storageError("read thread")));
278
+ return yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
279
+ });
280
+ const append = Effect.fn("SqliteJournal.append")(function* (request) {
281
+ if (request.threadId.length > MAX_IDENTIFIER_LENGTH || request.batchId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(request.batchJson) > MAX_STORED_TEXT_BYTES || storedTextBytes(request.batchDigest) > MAX_STORED_TEXT_BYTES || storedTextBytes(request.tailDigest) > MAX_STORED_TEXT_BYTES || request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(record.recordJson) > MAX_STORED_TEXT_BYTES)) return yield* SqliteStorageError.make({
282
+ operation: "append canonical batch",
283
+ message: "Canonical identifiers or encoded JSON exceed the SQLite storage bounds."
284
+ });
285
+ return yield* withWriteTransaction("append transaction")(Effect.gen(function* () {
286
+ const recordIds = request.records.map((record) => record.recordId);
287
+ if (new Set(recordIds).size !== recordIds.length) return yield* SqliteAppendConflict.make({
288
+ message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
289
+ reason: "record-identity"
290
+ });
291
+ const threadRows = yield* sql`
292
+ SELECT
293
+ thread_id,
294
+ created_at,
295
+ tail_sequence,
296
+ tail_digest,
297
+ producer_epoch
298
+ FROM effect_agent_threads
299
+ WHERE thread_id = ${request.threadId}
300
+ `.pipe(Effect.mapError(storageError("read append tail")));
301
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", request.threadId, threadRows);
302
+ if (request.producerEpoch !== thread.producer_epoch) return yield* SqliteFenceRejected.make({
303
+ producerEpoch: request.producerEpoch,
304
+ actualEpoch: thread.producer_epoch,
305
+ message: `Producer epoch ${request.producerEpoch} is not the current epoch ${thread.producer_epoch}.`
306
+ });
307
+ const batchRows = yield* sql`
308
+ SELECT
309
+ thread_id,
310
+ batch_id,
311
+ first_sequence,
312
+ last_sequence,
313
+ batch_digest,
314
+ tail_digest,
315
+ batch_json
316
+ FROM effect_agent_canonical_batches
317
+ WHERE thread_id = ${request.threadId}
318
+ AND batch_id = ${request.batchId}
319
+ `.pipe(Effect.mapError(storageError("read idempotent batch")));
320
+ const batches = yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.threadId}/${request.batchId}`, batchRows);
321
+ if (batches.length > 1) return yield* SqliteStorageCorruptionError.make({
322
+ table: "effect_agent_canonical_batches",
323
+ rowKey: `${request.threadId}/${request.batchId}`,
324
+ message: "A canonical batch primary key returned more than one row."
325
+ });
326
+ if (batches.length === 1) {
327
+ const existing = batches[0];
328
+ if (existing.batch_digest !== request.batchDigest) return yield* SqliteAppendConflict.make({
329
+ message: `Batch ${request.batchId} already exists with different canonical content.`,
330
+ reason: "batch-digest"
331
+ });
332
+ return RawAppendResult.make({
333
+ firstSequence: existing.first_sequence,
334
+ lastSequence: existing.last_sequence,
335
+ replayed: true,
336
+ tailDigest: existing.tail_digest
337
+ });
338
+ }
339
+ if (request.expectedTailSequence !== thread.tail_sequence || request.expectedTailDigest !== thread.tail_digest) return yield* SqliteAppendConflict.make({
340
+ message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${thread.tail_sequence}/${thread.tail_digest}.`,
341
+ reason: "tail",
342
+ actualTailSequence: thread.tail_sequence,
343
+ actualTailDigest: thread.tail_digest
344
+ });
345
+ if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) return yield* SqliteStorageError.make({
346
+ operation: "append canonical batch",
347
+ message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`
348
+ });
349
+ const existingRecordRows = yield* sql`
350
+ SELECT
351
+ thread_id,
352
+ sequence,
353
+ record_id,
354
+ batch_id,
355
+ record_json
356
+ FROM effect_agent_canonical_records
357
+ WHERE thread_id = ${request.threadId}
358
+ AND record_id IN ${sql.in(recordIds)}
359
+ ORDER BY sequence
360
+ `.pipe(Effect.mapError(storageError("check canonical record identities")));
361
+ const existingRecords = yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/record_ids`, existingRecordRows);
362
+ if (existingRecords.length > 0) return yield* SqliteAppendConflict.make({
363
+ message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
364
+ reason: "record-identity"
365
+ });
366
+ const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(thread.tail_sequence + 1).pipe(Effect.mapError((error) => SqliteStorageError.make({
367
+ cause: error,
368
+ operation: "append canonical batch",
369
+ message: error.message
370
+ })));
371
+ const lastSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(firstSequence + request.records.length - 1).pipe(Effect.mapError((error) => SqliteStorageError.make({
372
+ cause: error,
373
+ operation: "append canonical batch",
374
+ message: error.message
375
+ })));
376
+ yield* sql`
377
+ INSERT INTO effect_agent_canonical_batches (
378
+ thread_id,
379
+ batch_id,
380
+ first_sequence,
381
+ last_sequence,
382
+ batch_digest,
383
+ tail_digest,
384
+ batch_json
385
+ ) VALUES (
386
+ ${request.threadId},
387
+ ${request.batchId},
388
+ ${firstSequence},
389
+ ${lastSequence},
390
+ ${request.batchDigest},
391
+ ${request.tailDigest},
392
+ ${request.batchJson}
393
+ )
394
+ `.pipe(Effect.mapError(storageError("insert canonical batch")));
395
+ yield* failpoint("append:after-batch-insert");
396
+ yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
397
+ yield* sql`
398
+ INSERT INTO effect_agent_canonical_records (
399
+ thread_id,
400
+ sequence,
401
+ record_id,
402
+ batch_id,
403
+ record_json
404
+ ) VALUES (
405
+ ${request.threadId},
406
+ ${firstSequence + index},
407
+ ${record.recordId},
408
+ ${request.batchId},
409
+ ${record.recordJson}
410
+ )
411
+ `.pipe(Effect.mapError(storageError("insert canonical record")));
412
+ yield* failpoint("append:after-record-insert");
413
+ }), { discard: true });
414
+ yield* sql`
415
+ UPDATE effect_agent_threads
416
+ SET
417
+ tail_sequence = ${lastSequence},
418
+ tail_digest = ${request.tailDigest},
419
+ producer_epoch = ${request.producerEpoch}
420
+ WHERE thread_id = ${request.threadId}
421
+ `.pipe(Effect.mapError(storageError("advance thread tail")));
422
+ yield* failpoint("append:after-tail-update");
423
+ return RawAppendResult.make({
424
+ firstSequence,
425
+ lastSequence,
426
+ replayed: false,
427
+ tailDigest: request.tailDigest
428
+ });
429
+ }));
430
+ });
431
+ const read = Effect.fn("SqliteJournal.read")(function* (request) {
432
+ const rows = yield* sql`
433
+ SELECT
434
+ thread_id,
435
+ sequence,
436
+ record_id,
437
+ batch_id,
438
+ record_json
439
+ FROM effect_agent_canonical_records
440
+ WHERE thread_id = ${request.threadId}
441
+ AND sequence > ${request.fromSequenceExclusive}
442
+ ORDER BY sequence
443
+ LIMIT ${request.limit}
444
+ `.pipe(Effect.mapError(storageError("read canonical records")));
445
+ return yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}>${request.fromSequenceExclusive}`, rows);
446
+ });
447
+ const exportThread = Effect.fn("SqliteJournal.exportThread")(function* (threadId) {
448
+ return yield* withReadTransaction("export transaction")(Effect.gen(function* () {
449
+ const threadRows = yield* sql`
450
+ SELECT
451
+ thread_id,
452
+ created_at,
453
+ tail_sequence,
454
+ tail_digest,
455
+ producer_epoch
456
+ FROM effect_agent_threads
457
+ WHERE thread_id = ${threadId}
458
+ `.pipe(Effect.mapError(storageError("export thread")));
459
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", threadId, threadRows);
460
+ yield* failpoint("export:after-thread-read");
461
+ const batchRows = yield* sql`
462
+ SELECT
463
+ thread_id,
464
+ batch_id,
465
+ first_sequence,
466
+ last_sequence,
467
+ batch_digest,
468
+ tail_digest,
469
+ batch_json
470
+ FROM effect_agent_canonical_batches
471
+ WHERE thread_id = ${threadId}
472
+ ORDER BY first_sequence
473
+ `.pipe(Effect.mapError(storageError("export canonical batches")));
474
+ const recordRows = yield* sql`
475
+ SELECT
476
+ thread_id,
477
+ sequence,
478
+ record_id,
479
+ batch_id,
480
+ record_json
481
+ FROM effect_agent_canonical_records
482
+ WHERE thread_id = ${threadId}
483
+ ORDER BY sequence
484
+ `.pipe(Effect.mapError(storageError("export canonical records")));
485
+ const checkpointRows = yield* sql`
486
+ SELECT
487
+ thread_id,
488
+ through_sequence,
489
+ tail_digest,
490
+ checkpoint_json
491
+ FROM effect_agent_checkpoints
492
+ WHERE thread_id = ${threadId}
493
+ ORDER BY through_sequence
494
+ `.pipe(Effect.mapError(storageError("export checkpoints")));
495
+ return RawThreadExport.make({
496
+ thread,
497
+ batches: yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", threadId, batchRows),
498
+ records: yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", threadId, recordRows),
499
+ checkpoints: yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", threadId, checkpointRows)
500
+ });
501
+ }));
502
+ });
503
+ const saveCheckpoint = Effect.fn("SqliteJournal.saveCheckpoint")(function* (checkpoint) {
504
+ if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(checkpoint.checkpointJson) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
505
+ operation: "save checkpoint",
506
+ message: "Checkpoint identity or encoded JSON exceeds the SQLite storage bounds."
507
+ });
508
+ yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
509
+ const threadRows = yield* sql`
510
+ SELECT
511
+ thread_id,
512
+ created_at,
513
+ tail_sequence,
514
+ tail_digest,
515
+ producer_epoch
516
+ FROM effect_agent_threads
517
+ WHERE thread_id = ${checkpoint.threadId}
518
+ `.pipe(Effect.mapError(storageError("read checkpoint tail")));
519
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", checkpoint.threadId, threadRows);
520
+ if (checkpoint.throughSequence > thread.tail_sequence) return yield* SqliteCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${thread.tail_sequence}.` });
521
+ const checkpointRows = yield* sql`
522
+ SELECT
523
+ thread_id,
524
+ through_sequence,
525
+ tail_digest,
526
+ checkpoint_json
527
+ FROM effect_agent_checkpoints
528
+ WHERE thread_id = ${checkpoint.threadId}
529
+ AND through_sequence = ${checkpoint.throughSequence}
530
+ `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
531
+ const existing = yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.threadId}/${checkpoint.throughSequence}`, checkpointRows);
532
+ if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
533
+ table: "effect_agent_checkpoints",
534
+ rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
535
+ message: "A checkpoint primary key returned more than one row."
536
+ });
537
+ if (existing.length === 1) {
538
+ if (existing[0].tail_digest !== checkpoint.tailDigest || existing[0].checkpoint_json !== checkpoint.checkpointJson) return yield* SqliteCheckpointConflict.make({ message: "A different checkpoint already exists at this canonical sequence." });
539
+ return;
540
+ }
541
+ yield* sql`
542
+ INSERT INTO effect_agent_checkpoints (
543
+ thread_id,
544
+ through_sequence,
545
+ tail_digest,
546
+ checkpoint_json
547
+ ) VALUES (
548
+ ${checkpoint.threadId},
549
+ ${checkpoint.throughSequence},
550
+ ${checkpoint.tailDigest},
551
+ ${checkpoint.checkpointJson}
552
+ )
553
+ `.pipe(Effect.mapError(storageError("insert checkpoint")));
554
+ }));
555
+ });
556
+ const loadCheckpoint = Effect.fn("SqliteJournal.loadCheckpoint")(function* (threadId, atOrBeforeSequence) {
557
+ const rows = yield* sql`
558
+ SELECT
559
+ thread_id,
560
+ through_sequence,
561
+ tail_digest,
562
+ checkpoint_json
563
+ FROM effect_agent_checkpoints
564
+ WHERE thread_id = ${threadId}
565
+ AND through_sequence <= ${atOrBeforeSequence}
566
+ ORDER BY through_sequence DESC
567
+ LIMIT 1
568
+ `.pipe(Effect.mapError(storageError("load checkpoint")));
569
+ return yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${threadId}<=${atOrBeforeSequence}`, rows);
570
+ });
571
+ return {
572
+ append,
573
+ exportThread,
574
+ getThread,
575
+ getTailDigestAt: Effect.fn("SqliteJournal.getTailDigestAt")(function* (threadId, sequence) {
576
+ if (sequence === 0) {
577
+ const threads = yield* getThread(threadId);
578
+ return threads.length === 0 ? [] : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : void 0].filter((value) => value !== void 0);
579
+ }
580
+ const rows = yield* sql`
581
+ SELECT
582
+ thread_id,
583
+ batch_id,
584
+ first_sequence,
585
+ last_sequence,
586
+ batch_digest,
587
+ tail_digest,
588
+ batch_json
589
+ FROM effect_agent_canonical_batches
590
+ WHERE thread_id = ${threadId}
591
+ AND last_sequence = ${sequence}
592
+ `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
593
+ return (yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${threadId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
594
+ }),
595
+ loadCheckpoint,
596
+ materialize,
597
+ read,
598
+ saveCheckpoint,
599
+ scanStoredPayloads: Effect.fn("SqliteJournal.scanStoredPayloads")(function* () {
600
+ return yield* withReadTransaction("startup scan transaction")(Effect.gen(function* () {
601
+ const threads = yield* sql`
602
+ SELECT
603
+ thread_id,
604
+ created_at,
605
+ tail_sequence,
606
+ tail_digest,
607
+ producer_epoch
608
+ FROM effect_agent_threads
609
+ ORDER BY thread_id
610
+ `.pipe(Effect.mapError(storageError("scan threads")));
611
+ const batches = yield* sql`
612
+ SELECT
613
+ thread_id,
614
+ batch_id,
615
+ first_sequence,
616
+ last_sequence,
617
+ batch_digest,
618
+ tail_digest,
619
+ batch_json
620
+ FROM effect_agent_canonical_batches
621
+ ORDER BY thread_id, first_sequence
622
+ `.pipe(Effect.mapError(storageError("scan canonical batches")));
623
+ const records = yield* sql`
624
+ SELECT
625
+ thread_id,
626
+ sequence,
627
+ record_id,
628
+ batch_id,
629
+ record_json
630
+ FROM effect_agent_canonical_records
631
+ ORDER BY thread_id, sequence
632
+ `.pipe(Effect.mapError(storageError("scan canonical records")));
633
+ const checkpoints = yield* sql`
634
+ SELECT
635
+ thread_id,
636
+ through_sequence,
637
+ tail_digest,
638
+ checkpoint_json
639
+ FROM effect_agent_checkpoints
640
+ ORDER BY thread_id, through_sequence
641
+ `.pipe(Effect.mapError(storageError("scan checkpoints")));
642
+ return {
643
+ threads: yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", "startup_scan", threads),
644
+ batches: yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
645
+ records: yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
646
+ checkpoints: yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
647
+ };
648
+ }));
649
+ }),
650
+ withWriteTransaction
651
+ };
652
+ });
653
+ //#endregion
654
+ export { initializeSqliteJournal as a, decodeRows as i, RawCheckpoint as n, RawReadRequest as r, RawAppendRequest as t };
655
+
656
+ //# sourceMappingURL=sqlite-journal-C5SZnSdC.mjs.map