@effect-agent/storage-sqlite 0.1.0-beta.9 → 0.1.0-beta.91

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 (61) 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/SqliteMessageDeliveryStore.d.mts +14 -0
  5. package/dist/SqliteMessageDeliveryStore.mjs +24 -0
  6. package/dist/SqliteMessageDeliveryStore.mjs.map +1 -0
  7. package/dist/SqliteScheduleStore.d.mts +14 -0
  8. package/dist/SqliteScheduleStore.mjs +255 -0
  9. package/dist/SqliteScheduleStore.mjs.map +1 -0
  10. package/dist/SqliteStorageConfig.d.mts +34 -0
  11. package/dist/SqliteStorageConfig.mjs +39 -0
  12. package/dist/SqliteStorageConfig.mjs.map +1 -0
  13. package/dist/SqliteStorageError-DVWeaWLm.d.mts +86 -0
  14. package/dist/SqliteStorageError.d.mts +2 -0
  15. package/dist/SqliteStorageError.mjs +150 -0
  16. package/dist/SqliteStorageError.mjs.map +1 -0
  17. package/dist/SqliteStorageFailpoint.d.mts +17 -0
  18. package/dist/SqliteStorageFailpoint.mjs +14 -0
  19. package/dist/SqliteStorageFailpoint.mjs.map +1 -0
  20. package/dist/SqliteStorageFailpointTesting.d.mts +15 -0
  21. package/dist/SqliteStorageFailpointTesting.mjs +19 -0
  22. package/dist/SqliteStorageFailpointTesting.mjs.map +1 -0
  23. package/dist/SqliteStorageVersion-BqmqX0CI.d.mts +11 -0
  24. package/dist/SqliteStorageVersion.d.mts +2 -0
  25. package/dist/SqliteStorageVersion.mjs +8 -0
  26. package/dist/SqliteStorageVersion.mjs.map +1 -0
  27. package/dist/SqliteSubmissionLedger.d.mts +23 -0
  28. package/dist/SqliteSubmissionLedger.mjs +1788 -0
  29. package/dist/SqliteSubmissionLedger.mjs.map +1 -0
  30. package/dist/SqliteSubscriptionStore.d.mts +13 -0
  31. package/dist/SqliteSubscriptionStore.mjs +28 -0
  32. package/dist/SqliteSubscriptionStore.mjs.map +1 -0
  33. package/dist/SqliteThreadStore.d.mts +49 -0
  34. package/dist/SqliteThreadStore.mjs +460 -0
  35. package/dist/SqliteThreadStore.mjs.map +1 -0
  36. package/dist/index.d.mts +11 -193
  37. package/dist/index.mjs +11 -3082
  38. package/dist/migrations-Dy5v0HGe.mjs +346 -0
  39. package/dist/migrations-Dy5v0HGe.mjs.map +1 -0
  40. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  41. package/dist/sqlite-journal-CBOQDySe.mjs +998 -0
  42. package/dist/sqlite-journal-CBOQDySe.mjs.map +1 -0
  43. package/package.json +1 -41
  44. package/src/SqliteActivityStore.ts +556 -0
  45. package/src/SqliteMessageDeliveryStore.ts +42 -0
  46. package/src/SqliteScheduleStore.ts +404 -0
  47. package/src/{sqlite-storage-config.ts → SqliteStorageConfig.ts} +1 -1
  48. package/src/{errors.ts → SqliteStorageError.ts} +10 -3
  49. package/src/SqliteStorageFailpoint.ts +23 -0
  50. package/src/{sqlite-storage-failpoint.ts → SqliteStorageFailpointTesting.ts} +7 -18
  51. package/src/SqliteStorageVersion.ts +2 -0
  52. package/src/{sqlite-ledger.ts → SqliteSubmissionLedger.ts} +912 -212
  53. package/src/SqliteSubscriptionStore.ts +55 -0
  54. package/src/SqliteThreadStore.ts +999 -0
  55. package/src/index.ts +10 -6
  56. package/src/internal/message-delivery-schema.ts +24 -0
  57. package/src/{migrations.ts → internal/migrations.ts} +156 -79
  58. package/src/internal/recovery-checkpoint-schema.ts +17 -0
  59. package/src/{sqlite-journal.ts → internal/sqlite-journal.ts} +751 -241
  60. package/dist/index.mjs.map +0 -1
  61. package/src/sqlite-conversation-store.ts +0 -841
package/dist/index.mjs CHANGED
@@ -1,3082 +1,11 @@
1
- import { AbortCommand, AbortIntent, AdmissionAdmitted, AdmissionConflict, AdmissionNotAdmitted, AdmissionRequest, AdmissionResult, AppendConflict, AppendResult, ApprovalConflict, ApprovalDecision, ApprovalDecisionCommand, ApprovalDecisionIntent, AttachChildToReservationRequest, BeginChildBudgetReleaseRequest, CanonicalBatch, CanonicalRecord, CanonicalRecordEnvelope, CanonicalSequence, CheckpointRejected, ChildAttachmentSnapshot, ChildBudgetReservationRequest, ChildBudgetReservationSnapshot, ChildReservationConflict, ChildReservationStatus, ChildSettledNotification, Claim, ClaimJoiningRequest, ClaimRequest, ConversationCheckpoint, ConversationExport, ConversationExportRequest, ConversationMaterialization, ConversationNotMaterialized, ConversationObservation, ConversationRead, ConversationStore, ConversationStoreError, ConversationTail, ConversationTailRequest, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, Digest, EMPTY_TAIL_DIGEST, FenceRejected, FencedAppendRequest, InputAppliedMarker, JoinSnapshot, JoinedToHost, JoiningClaim, LedgerCapabilities, LedgerError, LoadCheckpointRequest, MarkInputAppliedRequest, MarkJoinedRequest, MarkReadyRequest, MarkUnknownRequest, ObservationOffset, OwnershipLost, OwnershipRenewal, OwnershipSnapshot, ParentLinkage, PersistedJson, ProducerEpoch, QueueSequence, RecordEnvelope, RecoverySnapshot, RecoverySnapshotRequest, ReleaseChildBudgetRequest, ReleaseOwnershipRequest, RenewOwnershipRequest, ReservedChildBudget, ReservedSettlement, RevertJoiningRequest, SaveCheckpointRequest, Settlement, SettlementConflict, SettlementFinalization, SettlementOutcome, SettlementReservation, SettlementReservationSnapshot, SubmissionLedger, SubmissionLookup, SubmissionLookupByKey, SubmissionSnapshot, SubmissionState, SuspendRequest, SuspensionReason, SuspensionSnapshot, UnknownResolution, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, digestCanonicalBatch, submissionAbortRecordId } from "@effect-agent/session";
2
- import { Clock, Context, Crypto, DateTime, Duration, Effect, Exit, Layer, Option, Ref, Schema, Stream } from "effect";
3
- import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node";
4
- import * as SqlClient from "effect/unstable/sql/SqlClient";
5
- import { NodeCrypto } from "@effect/platform-node";
6
- //#region src/errors.ts
7
- /** The SQLite file uses a private-development format this adapter cannot read. */
8
- var SqliteStorageCompatibilityError = class extends Schema.TaggedError()("SqliteStorageCompatibilityError", {
9
- actualVersion: Schema.Int,
10
- message: Schema.String,
11
- supportedVersion: Schema.Int
12
- }) {};
13
- /** Stored bytes failed the current Schema and cannot be used as recovery truth. */
14
- var SqliteStorageCorruptionError = class extends Schema.TaggedError()("SqliteStorageCorruptionError", {
15
- message: Schema.String,
16
- rowKey: Schema.String,
17
- table: Schema.String
18
- }) {};
19
- /** SQLite infrastructure failed while opening or operating the store. */
20
- var SqliteStorageError = class extends Schema.TaggedError()("SqliteStorageError", {
21
- cause: Schema.optionalKey(Schema.Defect()),
22
- message: Schema.String,
23
- operation: Schema.String
24
- }) {};
25
- /**
26
- * SQLite infrastructure failed while operating the Submission Ledger. Surfaces at the
27
- * SubmissionLedger port as the typed `LedgerError` with this error preserved as its cause,
28
- * so the adapter-level tag is never erased.
29
- */
30
- var SqliteLedgerError = class extends Schema.TaggedError()("SqliteLedgerError", {
31
- cause: Schema.optionalKey(Schema.Defect()),
32
- message: Schema.String,
33
- operation: Schema.String
34
- }) {};
35
- /**
36
- * A canonical batch retry conflicts with existing append state. Tail conflicts carry the
37
- * actual committed tail as a diagnostic resume hint.
38
- */
39
- var SqliteAppendConflict = class extends Schema.TaggedError()("SqliteAppendConflict", {
40
- message: Schema.String,
41
- reason: Schema.Literals([
42
- "batch-digest",
43
- "record-identity",
44
- "tail"
45
- ]),
46
- actualTailSequence: Schema.optionalKey(CanonicalSequence),
47
- actualTailDigest: Schema.optionalKey(Schema.String)
48
- }) {};
49
- /**
50
- * A producer epoch does not match the Conversation's current writer registration. Appends
51
- * require the exact registered epoch, so both older and newer unregistered epochs are fenced;
52
- * a newer epoch takes over by materializing first.
53
- */
54
- var SqliteFenceRejected = class extends Schema.TaggedError()("SqliteFenceRejected", {
55
- actualEpoch: ProducerEpoch,
56
- message: Schema.String,
57
- producerEpoch: ProducerEpoch
58
- }) {};
59
- /**
60
- * A write transaction could not acquire the SQLite write lock within the configured busy
61
- * timeout (SQLITE_BUSY / SQLITE_LOCKED). Another transiently coexisting owner is writing;
62
- * the operation did not mutate canonical state and is safe to retry.
63
- */
64
- var SqliteWriteContention = class extends Schema.TaggedError()("SqliteWriteContention", {
65
- cause: Schema.optionalKey(Schema.Defect()),
66
- message: Schema.String,
67
- operation: Schema.String
68
- }) {};
69
- /** A checkpoint conflicts with a previously stored checkpoint at the same offset. */
70
- var SqliteCheckpointConflict = class extends Schema.TaggedError()("SqliteCheckpointConflict", { message: Schema.String }) {};
71
- const SqliteStorageFailpointLocation = Schema.Literals([
72
- "materialize:before",
73
- "materialize:after",
74
- "append:before",
75
- "append:after-batch-insert",
76
- "append:after-record-insert",
77
- "append:after-tail-update",
78
- "append:after",
79
- "export:after-conversation-read",
80
- "save-checkpoint:before",
81
- "save-checkpoint:after",
82
- "ledger:admit:before",
83
- "ledger:admit:after",
84
- "ledger:mark-ready:before",
85
- "ledger:mark-ready:after",
86
- "ledger:claim:before",
87
- "ledger:claim:after",
88
- "ledger:mark-input-applied:before",
89
- "ledger:mark-input-applied:after",
90
- "ledger:renew:before",
91
- "ledger:renew:after",
92
- "ledger:reserve-settlement:before",
93
- "ledger:reserve-settlement:after",
94
- "ledger:finalize-settlement:before",
95
- "ledger:finalize-settlement:after",
96
- "ledger:request-abort:before",
97
- "ledger:request-abort:after",
98
- "ledger:release:before",
99
- "ledger:release:after",
100
- "ledger:claim-joining:before",
101
- "ledger:claim-joining:after",
102
- "ledger:mark-joined:before",
103
- "ledger:mark-joined:after",
104
- "ledger:revert-joining:before",
105
- "ledger:revert-joining:after",
106
- "ledger:suspend:before",
107
- "ledger:suspend:after",
108
- "ledger:approval-decision:before",
109
- "ledger:approval-decision:after",
110
- "ledger:mark-unknown:before",
111
- "ledger:mark-unknown:after",
112
- "ledger:unknown-resolution:before",
113
- "ledger:unknown-resolution:after",
114
- "ledger:child-reservation:before",
115
- "ledger:child-reservation:after",
116
- "ledger:child-attach:before",
117
- "ledger:child-attach:after",
118
- "ledger:child-release-pending:before",
119
- "ledger:child-release-pending:after",
120
- "ledger:child-release:before",
121
- "ledger:child-release:after",
122
- "ledger:child-settled:before",
123
- "ledger:child-settled:after"
124
- ]);
125
- /** Deterministic test-only fault or pause injected at a SQLite operation boundary. */
126
- var SqliteStorageFailpointError = class extends Schema.TaggedError()("SqliteStorageFailpointError", { location: SqliteStorageFailpointLocation }) {
127
- get message() {
128
- return `Injected SQLite storage failure at ${this.location}.`;
129
- }
130
- };
131
- //#endregion
132
- //#region src/migrations.ts
133
- const CurrentSqliteStorageVersion = 4;
134
- const sqliteMigrations = SqliteMigrator.fromRecord({
135
- "1_current_persistent_conversation_foundation": Effect.gen(function* () {
136
- const sql = yield* SqlClient.SqlClient;
137
- yield* sql`
138
- CREATE TABLE effect_agent_conversations (
139
- conversation_id TEXT PRIMARY KEY NOT NULL,
140
- created_at TEXT NOT NULL,
141
- tail_sequence INTEGER NOT NULL,
142
- tail_digest TEXT NOT NULL,
143
- producer_epoch INTEGER NOT NULL
144
- )
145
- `.withoutTransform;
146
- yield* sql`
147
- CREATE TABLE effect_agent_canonical_batches (
148
- conversation_id TEXT NOT NULL,
149
- batch_id TEXT NOT NULL,
150
- first_sequence INTEGER NOT NULL,
151
- last_sequence INTEGER NOT NULL,
152
- batch_digest TEXT NOT NULL,
153
- tail_digest TEXT NOT NULL,
154
- batch_json TEXT NOT NULL,
155
- PRIMARY KEY (conversation_id, batch_id),
156
- FOREIGN KEY (conversation_id)
157
- REFERENCES effect_agent_conversations(conversation_id)
158
- ON DELETE RESTRICT
159
- )
160
- `.withoutTransform;
161
- yield* sql`
162
- CREATE TABLE effect_agent_canonical_records (
163
- conversation_id TEXT NOT NULL,
164
- sequence INTEGER NOT NULL,
165
- record_id TEXT NOT NULL,
166
- batch_id TEXT NOT NULL,
167
- record_json TEXT NOT NULL,
168
- PRIMARY KEY (conversation_id, sequence),
169
- UNIQUE (conversation_id, record_id),
170
- FOREIGN KEY (conversation_id, batch_id)
171
- REFERENCES effect_agent_canonical_batches(conversation_id, batch_id)
172
- ON DELETE RESTRICT
173
- )
174
- `.withoutTransform;
175
- yield* sql`
176
- CREATE INDEX effect_agent_canonical_records_batch
177
- ON effect_agent_canonical_records (conversation_id, batch_id, sequence)
178
- `.withoutTransform;
179
- yield* sql`
180
- CREATE TABLE effect_agent_checkpoints (
181
- conversation_id TEXT NOT NULL,
182
- through_sequence INTEGER NOT NULL,
183
- tail_digest TEXT NOT NULL,
184
- checkpoint_json TEXT NOT NULL,
185
- PRIMARY KEY (conversation_id, through_sequence),
186
- FOREIGN KEY (conversation_id)
187
- REFERENCES effect_agent_conversations(conversation_id)
188
- ON DELETE RESTRICT
189
- )
190
- `.withoutTransform;
191
- yield* sql`PRAGMA user_version = 1`.withoutTransform;
192
- }),
193
- "2_durable_submission_ledger": Effect.gen(function* () {
194
- const sql = yield* SqlClient.SqlClient;
195
- yield* sql`
196
- CREATE TABLE effect_agent_submissions (
197
- submission_id TEXT PRIMARY KEY NOT NULL,
198
- conversation_id TEXT NOT NULL,
199
- queue_sequence INTEGER NOT NULL,
200
- principal TEXT NOT NULL,
201
- idempotency_key TEXT NOT NULL,
202
- agent_id TEXT NOT NULL,
203
- agent_digests_json TEXT NOT NULL,
204
- deployment_id TEXT NOT NULL,
205
- input_json TEXT NOT NULL,
206
- input_digest TEXT NOT NULL,
207
- receipt_id TEXT NOT NULL,
208
- state TEXT NOT NULL,
209
- settled_outcome TEXT,
210
- created_at TEXT NOT NULL,
211
- ready_at TEXT,
212
- input_applied_record_id TEXT,
213
- input_applied_sequence INTEGER,
214
- UNIQUE (conversation_id, principal, idempotency_key),
215
- UNIQUE (conversation_id, queue_sequence)
216
- )
217
- `.withoutTransform;
218
- yield* sql`
219
- CREATE TABLE effect_agent_submission_ownership (
220
- submission_id TEXT PRIMARY KEY NOT NULL,
221
- attempt_id TEXT NOT NULL,
222
- ownership_token TEXT NOT NULL,
223
- producer_epoch INTEGER NOT NULL,
224
- owner_producer_id TEXT NOT NULL,
225
- lease_expires_at TEXT NOT NULL,
226
- FOREIGN KEY (submission_id)
227
- REFERENCES effect_agent_submissions(submission_id)
228
- ON DELETE RESTRICT
229
- )
230
- `.withoutTransform;
231
- yield* sql`
232
- CREATE TABLE effect_agent_attempts (
233
- attempt_id TEXT PRIMARY KEY NOT NULL,
234
- submission_id TEXT NOT NULL,
235
- conversation_id TEXT NOT NULL,
236
- owner_producer_id TEXT NOT NULL,
237
- producer_epoch INTEGER NOT NULL,
238
- claimed_at TEXT NOT NULL,
239
- FOREIGN KEY (submission_id)
240
- REFERENCES effect_agent_submissions(submission_id)
241
- ON DELETE RESTRICT
242
- )
243
- `.withoutTransform;
244
- yield* sql`
245
- CREATE TABLE effect_agent_settlement_reservations (
246
- submission_id TEXT PRIMARY KEY NOT NULL,
247
- settlement_id TEXT NOT NULL,
248
- outcome TEXT NOT NULL,
249
- record_id TEXT NOT NULL,
250
- record_json TEXT NOT NULL,
251
- record_digest TEXT NOT NULL,
252
- reserved_at TEXT NOT NULL,
253
- finalized_at TEXT,
254
- FOREIGN KEY (submission_id)
255
- REFERENCES effect_agent_submissions(submission_id)
256
- ON DELETE RESTRICT
257
- )
258
- `.withoutTransform;
259
- yield* sql`
260
- CREATE TABLE effect_agent_abort_intents (
261
- submission_id TEXT PRIMARY KEY NOT NULL,
262
- author TEXT NOT NULL,
263
- reason TEXT NOT NULL,
264
- requested_at TEXT NOT NULL,
265
- canonical_record_id TEXT,
266
- FOREIGN KEY (submission_id)
267
- REFERENCES effect_agent_submissions(submission_id)
268
- ON DELETE RESTRICT
269
- )
270
- `.withoutTransform;
271
- yield* sql`PRAGMA user_version = 2`.withoutTransform;
272
- }),
273
- "3_durable_tools_and_joined_input": Effect.gen(function* () {
274
- const sql = yield* SqlClient.SqlClient;
275
- yield* sql`
276
- ALTER TABLE effect_agent_submissions
277
- ADD COLUMN joined_host_submission_id TEXT
278
- `.withoutTransform;
279
- yield* sql`
280
- ALTER TABLE effect_agent_submissions
281
- ADD COLUMN suspended_reason_json TEXT
282
- `.withoutTransform;
283
- yield* sql`
284
- ALTER TABLE effect_agent_submissions
285
- ADD COLUMN suspended_at TEXT
286
- `.withoutTransform;
287
- yield* sql`
288
- ALTER TABLE effect_agent_submissions
289
- ADD COLUMN unknown_reason TEXT
290
- `.withoutTransform;
291
- yield* sql`
292
- ALTER TABLE effect_agent_submissions
293
- ADD COLUMN unknown_tool_call_ids_json TEXT
294
- `.withoutTransform;
295
- yield* sql`
296
- CREATE INDEX effect_agent_submissions_joined_host
297
- ON effect_agent_submissions (joined_host_submission_id)
298
- `.withoutTransform;
299
- yield* sql`
300
- CREATE TABLE effect_agent_approval_decisions (
301
- submission_id TEXT NOT NULL,
302
- tool_call_id TEXT NOT NULL,
303
- decision TEXT NOT NULL,
304
- resolver TEXT NOT NULL,
305
- reason TEXT NOT NULL,
306
- decided_at TEXT NOT NULL,
307
- PRIMARY KEY (submission_id, tool_call_id),
308
- FOREIGN KEY (submission_id)
309
- REFERENCES effect_agent_submissions(submission_id)
310
- ON DELETE RESTRICT
311
- )
312
- `.withoutTransform;
313
- yield* sql`
314
- CREATE TABLE effect_agent_unknown_resolutions (
315
- submission_id TEXT NOT NULL,
316
- tool_call_id TEXT NOT NULL,
317
- author TEXT NOT NULL,
318
- reason TEXT NOT NULL,
319
- resolution_json TEXT NOT NULL,
320
- resolved_at TEXT NOT NULL,
321
- PRIMARY KEY (submission_id, tool_call_id),
322
- FOREIGN KEY (submission_id)
323
- REFERENCES effect_agent_submissions(submission_id)
324
- ON DELETE RESTRICT
325
- )
326
- `.withoutTransform;
327
- yield* sql`PRAGMA user_version = 3`.withoutTransform;
328
- }),
329
- "4_durable_subagents": Effect.gen(function* () {
330
- const sql = yield* SqlClient.SqlClient;
331
- yield* sql`
332
- ALTER TABLE effect_agent_submissions
333
- ADD COLUMN parent_submission_id TEXT
334
- `.withoutTransform;
335
- yield* sql`
336
- ALTER TABLE effect_agent_submissions
337
- ADD COLUMN parent_tool_call_id TEXT
338
- `.withoutTransform;
339
- yield* sql`
340
- CREATE INDEX effect_agent_submissions_parent
341
- ON effect_agent_submissions (parent_submission_id)
342
- `.withoutTransform;
343
- yield* sql`
344
- CREATE TABLE effect_agent_child_reservations (
345
- reservation_id TEXT PRIMARY KEY NOT NULL,
346
- parent_submission_id TEXT NOT NULL,
347
- parent_tool_call_id TEXT NOT NULL,
348
- child_submission_id TEXT,
349
- status TEXT NOT NULL,
350
- allocation_json TEXT NOT NULL,
351
- allocation_digest TEXT NOT NULL,
352
- accounting_json TEXT,
353
- reserved_at TEXT NOT NULL,
354
- release_began_at TEXT,
355
- released_at TEXT,
356
- UNIQUE (parent_submission_id, parent_tool_call_id),
357
- FOREIGN KEY (parent_submission_id)
358
- REFERENCES effect_agent_submissions(submission_id)
359
- ON DELETE RESTRICT
360
- )
361
- `.withoutTransform;
362
- yield* sql`PRAGMA user_version = 4`.withoutTransform;
363
- })
364
- });
365
- //#endregion
366
- //#region src/sqlite-journal.ts
367
- const BoundedStoredText$1 = Schema.String.check(Schema.isMaxLength(16 * 1024 * 1024));
368
- const BoundedIdentifier$1 = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
369
- const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
370
- const MAX_RECORDS_PER_CONVERSATION = 65536;
371
- const MAX_STORED_TEXT_BYTES = 16 * 1024 * 1024;
372
- const MAX_IDENTIFIER_LENGTH = 1024;
373
- const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
374
- var SqliteVersionRow = class extends Schema.Class("SqliteVersionRow")({ user_version: NonNegativeInt }) {};
375
- var SqliteJournalModeRow = class extends Schema.Class("SqliteJournalModeRow")({ journal_mode: Schema.NonEmptyString.check(Schema.isMaxLength(32)) }) {};
376
- var SqliteNameRow = class extends Schema.Class("SqliteNameRow")({ name: BoundedIdentifier$1 }) {};
377
- var ConversationRow = class extends Schema.Class("ConversationRow")({
378
- conversation_id: BoundedIdentifier$1,
379
- created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
380
- producer_epoch: ProducerEpoch,
381
- tail_digest: BoundedStoredText$1,
382
- tail_sequence: CanonicalSequence
383
- }) {};
384
- var BatchRow = class extends Schema.Class("BatchRow")({
385
- batch_digest: BoundedStoredText$1,
386
- batch_id: BoundedIdentifier$1,
387
- batch_json: BoundedStoredText$1,
388
- conversation_id: BoundedIdentifier$1,
389
- first_sequence: CanonicalSequence,
390
- last_sequence: CanonicalSequence,
391
- tail_digest: BoundedStoredText$1
392
- }) {};
393
- var RecordRow = class extends Schema.Class("RecordRow")({
394
- batch_id: BoundedIdentifier$1,
395
- conversation_id: BoundedIdentifier$1,
396
- record_id: BoundedIdentifier$1,
397
- record_json: BoundedStoredText$1,
398
- sequence: CanonicalSequence
399
- }) {};
400
- var CheckpointRow = class extends Schema.Class("CheckpointRow")({
401
- checkpoint_json: BoundedStoredText$1,
402
- conversation_id: BoundedIdentifier$1,
403
- tail_digest: BoundedStoredText$1,
404
- through_sequence: CanonicalSequence
405
- }) {};
406
- var RawRecord = class extends Schema.Class("@effect-agent/storage-sqlite/RawRecord")({
407
- recordId: BoundedIdentifier$1,
408
- recordJson: BoundedStoredText$1
409
- }) {};
410
- var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-sqlite/RawAppendRequest")({
411
- batchDigest: BoundedStoredText$1,
412
- batchId: BoundedIdentifier$1,
413
- batchJson: BoundedStoredText$1,
414
- conversationId: BoundedIdentifier$1,
415
- expectedTailDigest: BoundedStoredText$1,
416
- expectedTailSequence: CanonicalSequence,
417
- producerEpoch: ProducerEpoch,
418
- records: Schema.NonEmptyArray(RawRecord).check(Schema.isMaxLength(256)),
419
- tailDigest: BoundedStoredText$1
420
- }) {};
421
- var RawAppendResult = class extends Schema.Class("@effect-agent/storage-sqlite/RawAppendResult")({
422
- firstSequence: CanonicalSequence,
423
- lastSequence: CanonicalSequence,
424
- replayed: Schema.Boolean,
425
- tailDigest: BoundedStoredText$1
426
- }) {};
427
- var RawReadRequest = class extends Schema.Class("@effect-agent/storage-sqlite/RawReadRequest")({
428
- conversationId: BoundedIdentifier$1,
429
- fromSequenceExclusive: CanonicalSequence,
430
- limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
431
- }) {};
432
- var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-sqlite/RawCheckpoint")({
433
- checkpointJson: BoundedStoredText$1,
434
- conversationId: BoundedIdentifier$1,
435
- tailDigest: BoundedStoredText$1,
436
- throughSequence: CanonicalSequence
437
- }) {};
438
- var RawConversationExport = class extends Schema.Class("@effect-agent/storage-sqlite/RawConversationExport")({
439
- batches: Schema.Array(BatchRow),
440
- checkpoints: Schema.Array(CheckpointRow),
441
- conversation: ConversationRow,
442
- records: Schema.Array(RecordRow)
443
- }) {};
444
- const noFailpoint$1 = () => Effect.void;
445
- const storageError = (operation) => (error) => SqliteStorageError.make({
446
- cause: error,
447
- operation,
448
- message: error.message
449
- });
450
- /** Decode raw SQLite rows against a Schema, reporting failures as typed corruption. */
451
- const decodeRows = Effect.fn("SqliteJournal.decodeRows")((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
452
- table,
453
- rowKey,
454
- message: String(error)
455
- }))));
456
- /** Decode exactly one raw SQLite row against a Schema, reporting failures as typed corruption. */
457
- 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({
458
- table,
459
- rowKey,
460
- message: `Expected exactly one row but found ${decoded.length}.`
461
- })))));
462
- const ensureCurrentStorage = Effect.fn("SqliteJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint$1, busyTimeoutMillis = 5e3) {
463
- yield* sql`PRAGMA foreign_keys = ON`.pipe(Effect.mapError(storageError("enable foreign keys")));
464
- yield* sql.unsafe(`PRAGMA busy_timeout = ${busyTimeoutMillis}`).pipe(Effect.mapError(storageError("configure busy timeout")));
465
- const journalModeRows = yield* sql`PRAGMA journal_mode`.pipe(Effect.mapError(storageError("read journal mode")));
466
- const journalMode = yield* decodeSingleRow(Schema.Array(SqliteJournalModeRow), "pragma_journal_mode", "singleton", journalModeRows);
467
- if (journalMode.journal_mode.toLowerCase() !== "wal") return yield* SqliteStorageCompatibilityError.make({
468
- actualVersion: 0,
469
- supportedVersion: 4,
470
- message: `SQLite WAL mode is required; the database reported ${journalMode.journal_mode}.`
471
- });
472
- const versionRows = yield* sql`PRAGMA user_version`.pipe(Effect.mapError(storageError("read storage version")));
473
- const version = yield* decodeSingleRow(Schema.Array(SqliteVersionRow), "pragma_user_version", "singleton", versionRows);
474
- if (version.user_version !== 0 && version.user_version !== 4) return yield* SqliteStorageCompatibilityError.make({
475
- actualVersion: version.user_version,
476
- supportedVersion: 4,
477
- message: `The SQLite file uses private-development storage version ${version.user_version}; this build supports exactly version 4. Reset the database file explicitly; automatic stored-data migrations are not provided during private development.`
478
- });
479
- if (version.user_version === 0) {
480
- const existingRows = yield* sql`
481
- SELECT name
482
- FROM sqlite_master
483
- WHERE type = 'table'
484
- AND name LIKE 'effect_agent_%'
485
- ORDER BY name
486
- `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
487
- if ((yield* decodeRows(Schema.Array(SqliteNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* SqliteStorageCompatibilityError.make({
488
- actualVersion: 0,
489
- supportedVersion: 4,
490
- message: "The SQLite file contains unversioned Effect Agent tables. Reset it explicitly; refusing to mutate ambiguous stored data."
491
- });
492
- yield* SqliteMigrator.run({ loader: sqliteMigrations }).pipe(Effect.provideService(SqlClient.SqlClient, sql), Effect.mapError((error) => SqliteStorageError.make({
493
- cause: error,
494
- operation: "initialize current storage",
495
- message: error.message
496
- })));
497
- }
498
- const requiredRows = yield* sql`
499
- SELECT name
500
- FROM sqlite_master
501
- WHERE type = 'table'
502
- AND name IN (
503
- 'effect_agent_conversations',
504
- 'effect_agent_canonical_batches',
505
- 'effect_agent_canonical_records',
506
- 'effect_agent_checkpoints',
507
- 'effect_agent_submissions',
508
- 'effect_agent_submission_ownership',
509
- 'effect_agent_attempts',
510
- 'effect_agent_settlement_reservations',
511
- 'effect_agent_abort_intents',
512
- 'effect_agent_approval_decisions',
513
- 'effect_agent_unknown_resolutions'
514
- )
515
- ORDER BY name
516
- `.pipe(Effect.mapError(storageError("verify storage tables")));
517
- if ((yield* decodeRows(Schema.Array(SqliteNameRow), "sqlite_master", "required_tables", requiredRows)).length !== 11) return yield* SqliteStorageCompatibilityError.make({
518
- actualVersion: 4,
519
- supportedVersion: 4,
520
- message: "The SQLite file claims the current format but is missing required tables. Reset the corrupt private-development data."
521
- });
522
- return makeJournal(sql, failpoint);
523
- });
524
- const makeJournal = (sql, failpoint) => {
525
- const classifyWriteFailure = (operation) => (error) => error.reason._tag === "LockTimeoutError" ? SqliteWriteContention.make({
526
- cause: error,
527
- operation,
528
- message: `Another producer holds the SQLite write lock; ${operation} is safe to retry.`
529
- }) : storageError(operation)(error);
530
- /**
531
- * Runs one journal write transaction under `BEGIN IMMEDIATE`. SQLite's deferred `BEGIN`
532
- * would let a read-then-write transaction start as a reader and fail with
533
- * SQLITE_BUSY_SNAPSHOT on upgrade, which `busy_timeout` never retries. Taking the write
534
- * lock up front keeps cross-owner contention inside the bounded busy retry; a lock
535
- * timeout is classified as the retryable SqliteWriteContention. A failed `BEGIN` leaves
536
- * no transaction, so no rollback is attempted for it.
537
- *
538
- * Journal write transactions are always top level. Nesting one inside another would
539
- * deadlock the single-connection client, so new journal operations must not wrap this
540
- * helper inside another transaction.
541
- */
542
- const withWriteTransaction = (operation) => (effect) => Effect.uninterruptibleMask((restore) => Effect.scoped(Effect.gen(function* () {
543
- const connection = yield* sql.reserve.pipe(Effect.mapError(classifyWriteFailure(operation)));
544
- yield* connection.executeUnprepared("BEGIN IMMEDIATE", [], void 0).pipe(Effect.mapError(classifyWriteFailure(operation)));
545
- const exit = yield* restore(Effect.provideService(effect, sql.transactionService, [connection, 0])).pipe(Effect.exit);
546
- if (Exit.isSuccess(exit)) {
547
- yield* connection.executeUnprepared("COMMIT", [], void 0).pipe(Effect.mapError(classifyWriteFailure(operation)));
548
- return exit.value;
549
- }
550
- yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], void 0));
551
- return yield* exit;
552
- })).pipe(Effect.withSpan("SqliteJournal.withWriteTransaction", { attributes: { operation } })));
553
- /**
554
- * Runs a read-only snapshot under a deferred transaction. Effect's SQLite client now
555
- * starts every writable-client `withTransaction` with `BEGIN IMMEDIATE`, which is the
556
- * right default for mutations but would make exports take the write lock and block a
557
- * concurrent append. Reserving the connection and beginning explicitly preserves the
558
- * adapter's snapshot-with-concurrent-writer contract. As with the write helper, a failed
559
- * `BEGIN` is reported directly because there is no transaction to roll back.
560
- */
561
- const withReadTransaction = (operation) => (effect) => Effect.uninterruptibleMask((restore) => Effect.scoped(Effect.gen(function* () {
562
- const connection = yield* sql.reserve.pipe(Effect.mapError(storageError(operation)));
563
- yield* connection.executeUnprepared("BEGIN", [], void 0).pipe(Effect.mapError(storageError(operation)));
564
- const exit = yield* restore(Effect.provideService(effect, sql.transactionService, [connection, 0])).pipe(Effect.exit);
565
- if (Exit.isSuccess(exit)) {
566
- yield* connection.executeUnprepared("COMMIT", [], void 0).pipe(Effect.mapError(storageError(operation)));
567
- return exit.value;
568
- }
569
- yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], void 0));
570
- return yield* exit;
571
- })).pipe(Effect.withSpan("SqliteJournal.withReadTransaction", { attributes: { operation } })));
572
- const materialize = Effect.fn("SqliteJournal.materialize")(function* (conversationId, createdAt, emptyTailDigest, producerEpoch) {
573
- if (conversationId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(emptyTailDigest) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
574
- operation: "materialize conversation",
575
- message: "Conversation identity or initial digest exceeds the SQLite storage bounds."
576
- });
577
- yield* withWriteTransaction("materialize transaction")(Effect.gen(function* () {
578
- const existingRows = yield* sql`
579
- SELECT
580
- conversation_id,
581
- created_at,
582
- tail_sequence,
583
- tail_digest,
584
- producer_epoch
585
- FROM effect_agent_conversations
586
- WHERE conversation_id = ${conversationId}
587
- `.pipe(Effect.mapError(storageError("read materialized conversation")));
588
- const existing = yield* decodeRows(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, existingRows);
589
- if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
590
- table: "effect_agent_conversations",
591
- rowKey: conversationId,
592
- message: "A conversation primary key returned more than one row."
593
- });
594
- if (existing.length === 0) {
595
- yield* sql`
596
- INSERT INTO effect_agent_conversations (
597
- conversation_id,
598
- created_at,
599
- tail_sequence,
600
- tail_digest,
601
- producer_epoch
602
- ) VALUES (
603
- ${conversationId},
604
- ${createdAt},
605
- 0,
606
- ${emptyTailDigest},
607
- ${producerEpoch}
608
- )
609
- `.pipe(Effect.mapError(storageError("materialize conversation")));
610
- return;
611
- }
612
- if (producerEpoch < existing[0].producer_epoch) return yield* SqliteFenceRejected.make({
613
- producerEpoch,
614
- actualEpoch: existing[0].producer_epoch,
615
- message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
616
- });
617
- if (producerEpoch > existing[0].producer_epoch) yield* sql`
618
- UPDATE effect_agent_conversations
619
- SET producer_epoch = ${producerEpoch}
620
- WHERE conversation_id = ${conversationId}
621
- `.pipe(Effect.mapError(storageError("advance materialization epoch")));
622
- }));
623
- });
624
- const getConversation = Effect.fn("SqliteJournal.getConversation")(function* (conversationId) {
625
- const rows = yield* sql`
626
- SELECT
627
- conversation_id,
628
- created_at,
629
- tail_sequence,
630
- tail_digest,
631
- producer_epoch
632
- FROM effect_agent_conversations
633
- WHERE conversation_id = ${conversationId}
634
- `.pipe(Effect.mapError(storageError("read conversation")));
635
- return yield* decodeRows(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, rows);
636
- });
637
- const append = Effect.fn("SqliteJournal.append")(function* (request) {
638
- if (request.conversationId.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({
639
- operation: "append canonical batch",
640
- message: "Canonical identifiers or encoded JSON exceed the SQLite storage bounds."
641
- });
642
- return yield* withWriteTransaction("append transaction")(Effect.gen(function* () {
643
- const recordIds = request.records.map((record) => record.recordId);
644
- if (new Set(recordIds).size !== recordIds.length) return yield* SqliteAppendConflict.make({
645
- message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
646
- reason: "record-identity"
647
- });
648
- const conversationRows = yield* sql`
649
- SELECT
650
- conversation_id,
651
- created_at,
652
- tail_sequence,
653
- tail_digest,
654
- producer_epoch
655
- FROM effect_agent_conversations
656
- WHERE conversation_id = ${request.conversationId}
657
- `.pipe(Effect.mapError(storageError("read append tail")));
658
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", request.conversationId, conversationRows);
659
- if (request.producerEpoch !== conversation.producer_epoch) return yield* SqliteFenceRejected.make({
660
- producerEpoch: request.producerEpoch,
661
- actualEpoch: conversation.producer_epoch,
662
- message: `Producer epoch ${request.producerEpoch} is not the current epoch ${conversation.producer_epoch}.`
663
- });
664
- const batchRows = yield* sql`
665
- SELECT
666
- conversation_id,
667
- batch_id,
668
- first_sequence,
669
- last_sequence,
670
- batch_digest,
671
- tail_digest,
672
- batch_json
673
- FROM effect_agent_canonical_batches
674
- WHERE conversation_id = ${request.conversationId}
675
- AND batch_id = ${request.batchId}
676
- `.pipe(Effect.mapError(storageError("read idempotent batch")));
677
- const batches = yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.conversationId}/${request.batchId}`, batchRows);
678
- if (batches.length > 1) return yield* SqliteStorageCorruptionError.make({
679
- table: "effect_agent_canonical_batches",
680
- rowKey: `${request.conversationId}/${request.batchId}`,
681
- message: "A canonical batch primary key returned more than one row."
682
- });
683
- if (batches.length === 1) {
684
- const existing = batches[0];
685
- if (existing.batch_digest !== request.batchDigest) return yield* SqliteAppendConflict.make({
686
- message: `Batch ${request.batchId} already exists with different canonical content.`,
687
- reason: "batch-digest"
688
- });
689
- return RawAppendResult.make({
690
- firstSequence: existing.first_sequence,
691
- lastSequence: existing.last_sequence,
692
- replayed: true,
693
- tailDigest: existing.tail_digest
694
- });
695
- }
696
- if (request.expectedTailSequence !== conversation.tail_sequence || request.expectedTailDigest !== conversation.tail_digest) return yield* SqliteAppendConflict.make({
697
- message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${conversation.tail_sequence}/${conversation.tail_digest}.`,
698
- reason: "tail",
699
- actualTailSequence: conversation.tail_sequence,
700
- actualTailDigest: conversation.tail_digest
701
- });
702
- if (conversation.tail_sequence + request.records.length > MAX_RECORDS_PER_CONVERSATION) return yield* SqliteStorageError.make({
703
- operation: "append canonical batch",
704
- message: `Conversation record limit ${MAX_RECORDS_PER_CONVERSATION} would be exceeded.`
705
- });
706
- const existingRecordRows = yield* sql`
707
- SELECT
708
- conversation_id,
709
- sequence,
710
- record_id,
711
- batch_id,
712
- record_json
713
- FROM effect_agent_canonical_records
714
- WHERE conversation_id = ${request.conversationId}
715
- AND record_id IN ${sql.in(recordIds)}
716
- ORDER BY sequence
717
- `.pipe(Effect.mapError(storageError("check canonical record identities")));
718
- const existingRecords = yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}/record_ids`, existingRecordRows);
719
- if (existingRecords.length > 0) return yield* SqliteAppendConflict.make({
720
- message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
721
- reason: "record-identity"
722
- });
723
- const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(conversation.tail_sequence + 1).pipe(Effect.mapError((error) => SqliteStorageError.make({
724
- cause: error,
725
- operation: "append canonical batch",
726
- message: error.message
727
- })));
728
- const lastSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(firstSequence + request.records.length - 1).pipe(Effect.mapError((error) => SqliteStorageError.make({
729
- cause: error,
730
- operation: "append canonical batch",
731
- message: error.message
732
- })));
733
- yield* sql`
734
- INSERT INTO effect_agent_canonical_batches (
735
- conversation_id,
736
- batch_id,
737
- first_sequence,
738
- last_sequence,
739
- batch_digest,
740
- tail_digest,
741
- batch_json
742
- ) VALUES (
743
- ${request.conversationId},
744
- ${request.batchId},
745
- ${firstSequence},
746
- ${lastSequence},
747
- ${request.batchDigest},
748
- ${request.tailDigest},
749
- ${request.batchJson}
750
- )
751
- `.pipe(Effect.mapError(storageError("insert canonical batch")));
752
- yield* failpoint("append:after-batch-insert");
753
- yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
754
- yield* sql`
755
- INSERT INTO effect_agent_canonical_records (
756
- conversation_id,
757
- sequence,
758
- record_id,
759
- batch_id,
760
- record_json
761
- ) VALUES (
762
- ${request.conversationId},
763
- ${firstSequence + index},
764
- ${record.recordId},
765
- ${request.batchId},
766
- ${record.recordJson}
767
- )
768
- `.pipe(Effect.mapError(storageError("insert canonical record")));
769
- yield* failpoint("append:after-record-insert");
770
- }), { discard: true });
771
- yield* sql`
772
- UPDATE effect_agent_conversations
773
- SET
774
- tail_sequence = ${lastSequence},
775
- tail_digest = ${request.tailDigest},
776
- producer_epoch = ${request.producerEpoch}
777
- WHERE conversation_id = ${request.conversationId}
778
- `.pipe(Effect.mapError(storageError("advance conversation tail")));
779
- yield* failpoint("append:after-tail-update");
780
- return RawAppendResult.make({
781
- firstSequence,
782
- lastSequence,
783
- replayed: false,
784
- tailDigest: request.tailDigest
785
- });
786
- }));
787
- });
788
- const read = Effect.fn("SqliteJournal.read")(function* (request) {
789
- const rows = yield* sql`
790
- SELECT
791
- conversation_id,
792
- sequence,
793
- record_id,
794
- batch_id,
795
- record_json
796
- FROM effect_agent_canonical_records
797
- WHERE conversation_id = ${request.conversationId}
798
- AND sequence > ${request.fromSequenceExclusive}
799
- ORDER BY sequence
800
- LIMIT ${request.limit}
801
- `.pipe(Effect.mapError(storageError("read canonical records")));
802
- return yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}>${request.fromSequenceExclusive}`, rows);
803
- });
804
- const exportConversation = Effect.fn("SqliteJournal.exportConversation")(function* (conversationId) {
805
- return yield* withReadTransaction("export transaction")(Effect.gen(function* () {
806
- const conversationRows = yield* sql`
807
- SELECT
808
- conversation_id,
809
- created_at,
810
- tail_sequence,
811
- tail_digest,
812
- producer_epoch
813
- FROM effect_agent_conversations
814
- WHERE conversation_id = ${conversationId}
815
- `.pipe(Effect.mapError(storageError("export conversation")));
816
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, conversationRows);
817
- yield* failpoint("export:after-conversation-read");
818
- const batchRows = yield* sql`
819
- SELECT
820
- conversation_id,
821
- batch_id,
822
- first_sequence,
823
- last_sequence,
824
- batch_digest,
825
- tail_digest,
826
- batch_json
827
- FROM effect_agent_canonical_batches
828
- WHERE conversation_id = ${conversationId}
829
- ORDER BY first_sequence
830
- `.pipe(Effect.mapError(storageError("export canonical batches")));
831
- const recordRows = yield* sql`
832
- SELECT
833
- conversation_id,
834
- sequence,
835
- record_id,
836
- batch_id,
837
- record_json
838
- FROM effect_agent_canonical_records
839
- WHERE conversation_id = ${conversationId}
840
- ORDER BY sequence
841
- `.pipe(Effect.mapError(storageError("export canonical records")));
842
- const checkpointRows = yield* sql`
843
- SELECT
844
- conversation_id,
845
- through_sequence,
846
- tail_digest,
847
- checkpoint_json
848
- FROM effect_agent_checkpoints
849
- WHERE conversation_id = ${conversationId}
850
- ORDER BY through_sequence
851
- `.pipe(Effect.mapError(storageError("export checkpoints")));
852
- return RawConversationExport.make({
853
- conversation,
854
- batches: yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", conversationId, batchRows),
855
- records: yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", conversationId, recordRows),
856
- checkpoints: yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", conversationId, checkpointRows)
857
- });
858
- }));
859
- });
860
- const saveCheckpoint = Effect.fn("SqliteJournal.saveCheckpoint")(function* (checkpoint) {
861
- if (checkpoint.conversationId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(checkpoint.checkpointJson) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
862
- operation: "save checkpoint",
863
- message: "Checkpoint identity or encoded JSON exceeds the SQLite storage bounds."
864
- });
865
- yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
866
- const conversationRows = yield* sql`
867
- SELECT
868
- conversation_id,
869
- created_at,
870
- tail_sequence,
871
- tail_digest,
872
- producer_epoch
873
- FROM effect_agent_conversations
874
- WHERE conversation_id = ${checkpoint.conversationId}
875
- `.pipe(Effect.mapError(storageError("read checkpoint tail")));
876
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", checkpoint.conversationId, conversationRows);
877
- if (checkpoint.throughSequence > conversation.tail_sequence) return yield* SqliteCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${conversation.tail_sequence}.` });
878
- const checkpointRows = yield* sql`
879
- SELECT
880
- conversation_id,
881
- through_sequence,
882
- tail_digest,
883
- checkpoint_json
884
- FROM effect_agent_checkpoints
885
- WHERE conversation_id = ${checkpoint.conversationId}
886
- AND through_sequence = ${checkpoint.throughSequence}
887
- `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
888
- const existing = yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.conversationId}/${checkpoint.throughSequence}`, checkpointRows);
889
- if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
890
- table: "effect_agent_checkpoints",
891
- rowKey: `${checkpoint.conversationId}/${checkpoint.throughSequence}`,
892
- message: "A checkpoint primary key returned more than one row."
893
- });
894
- if (existing.length === 1) {
895
- 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." });
896
- return;
897
- }
898
- yield* sql`
899
- INSERT INTO effect_agent_checkpoints (
900
- conversation_id,
901
- through_sequence,
902
- tail_digest,
903
- checkpoint_json
904
- ) VALUES (
905
- ${checkpoint.conversationId},
906
- ${checkpoint.throughSequence},
907
- ${checkpoint.tailDigest},
908
- ${checkpoint.checkpointJson}
909
- )
910
- `.pipe(Effect.mapError(storageError("insert checkpoint")));
911
- }));
912
- });
913
- const loadCheckpoint = Effect.fn("SqliteJournal.loadCheckpoint")(function* (conversationId, atOrBeforeSequence) {
914
- const rows = yield* sql`
915
- SELECT
916
- conversation_id,
917
- through_sequence,
918
- tail_digest,
919
- checkpoint_json
920
- FROM effect_agent_checkpoints
921
- WHERE conversation_id = ${conversationId}
922
- AND through_sequence <= ${atOrBeforeSequence}
923
- ORDER BY through_sequence DESC
924
- LIMIT 1
925
- `.pipe(Effect.mapError(storageError("load checkpoint")));
926
- return yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${conversationId}<=${atOrBeforeSequence}`, rows);
927
- });
928
- return {
929
- append,
930
- exportConversation,
931
- getConversation,
932
- getTailDigestAt: Effect.fn("SqliteJournal.getTailDigestAt")(function* (conversationId, sequence) {
933
- if (sequence === 0) {
934
- const conversations = yield* getConversation(conversationId);
935
- return conversations.length === 0 ? [] : [conversations[0].tail_sequence === 0 ? conversations[0].tail_digest : void 0].filter((value) => value !== void 0);
936
- }
937
- const rows = yield* sql`
938
- SELECT
939
- conversation_id,
940
- batch_id,
941
- first_sequence,
942
- last_sequence,
943
- batch_digest,
944
- tail_digest,
945
- batch_json
946
- FROM effect_agent_canonical_batches
947
- WHERE conversation_id = ${conversationId}
948
- AND last_sequence = ${sequence}
949
- `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
950
- return (yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${conversationId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
951
- }),
952
- loadCheckpoint,
953
- materialize,
954
- read,
955
- saveCheckpoint,
956
- scanStoredPayloads: Effect.fn("SqliteJournal.scanStoredPayloads")(function* () {
957
- return yield* withReadTransaction("startup scan transaction")(Effect.gen(function* () {
958
- const conversations = yield* sql`
959
- SELECT
960
- conversation_id,
961
- created_at,
962
- tail_sequence,
963
- tail_digest,
964
- producer_epoch
965
- FROM effect_agent_conversations
966
- ORDER BY conversation_id
967
- `.pipe(Effect.mapError(storageError("scan conversations")));
968
- const batches = yield* sql`
969
- SELECT
970
- conversation_id,
971
- batch_id,
972
- first_sequence,
973
- last_sequence,
974
- batch_digest,
975
- tail_digest,
976
- batch_json
977
- FROM effect_agent_canonical_batches
978
- ORDER BY conversation_id, first_sequence
979
- `.pipe(Effect.mapError(storageError("scan canonical batches")));
980
- const records = yield* sql`
981
- SELECT
982
- conversation_id,
983
- sequence,
984
- record_id,
985
- batch_id,
986
- record_json
987
- FROM effect_agent_canonical_records
988
- ORDER BY conversation_id, sequence
989
- `.pipe(Effect.mapError(storageError("scan canonical records")));
990
- const checkpoints = yield* sql`
991
- SELECT
992
- conversation_id,
993
- through_sequence,
994
- tail_digest,
995
- checkpoint_json
996
- FROM effect_agent_checkpoints
997
- ORDER BY conversation_id, through_sequence
998
- `.pipe(Effect.mapError(storageError("scan checkpoints")));
999
- return {
1000
- conversations: yield* decodeRows(Schema.Array(ConversationRow), "effect_agent_conversations", "startup_scan", conversations),
1001
- batches: yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
1002
- records: yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
1003
- checkpoints: yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
1004
- };
1005
- }));
1006
- }),
1007
- withWriteTransaction
1008
- };
1009
- };
1010
- const initializeSqliteJournal = ensureCurrentStorage;
1011
- //#endregion
1012
- //#region src/sqlite-storage-config.ts
1013
- const ObservationPollInterval = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
1014
- const BusyTimeoutMillis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
1015
- const OwnershipLeaseMillis = Schema.Int.check(Schema.isGreaterThan(0));
1016
- /**
1017
- * Validated construction configuration consumed by the SQLite storage Layer. The database
1018
- * identity itself belongs to the SqlClient Layer; duplicating it here could silently diverge
1019
- * from the connection actually in use.
1020
- */
1021
- var SqliteStorageConfigValue = class extends Schema.Class("@effect-agent/storage-sqlite/SqliteStorageConfigValue")({
1022
- observationPollInterval: ObservationPollInterval,
1023
- /** Bounded SQLITE_BUSY retry window for write-lock acquisition, in milliseconds. */
1024
- busyTimeout: BusyTimeoutMillis,
1025
- /**
1026
- * Submission ownership lease duration in milliseconds (D5). The lease is a liveness hint
1027
- * that makes an abandoned claim reclaimable; correctness never depends on it because every
1028
- * canonical append is fenced by producer epoch. Convenience layers default this to
1029
- * `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/session`.
1030
- */
1031
- ownershipLeaseDuration: OwnershipLeaseMillis,
1032
- /**
1033
- * Re-verify every stored payload and digest chain while opening the store. Per-operation
1034
- * Schema decoding and the digest chain already fail clearly on corrupt rows, so the full
1035
- * scan is an explicit opt-in integrity audit rather than a startup requirement.
1036
- */
1037
- verifyOnOpen: Schema.Boolean
1038
- }) {};
1039
- /** Explicit SQLite storage configuration authority. */
1040
- var SqliteStorageConfig = class extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageConfig") {};
1041
- //#endregion
1042
- //#region src/sqlite-storage-failpoint.ts
1043
- const noFailpoint = () => Effect.void;
1044
- /** Test-only control for replacing the active SQLite failpoint handler. */
1045
- var SqliteStorageFailpointTestControl = class extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageFailpointTestControl") {};
1046
- /** Explicit fault-injection authority used at SQLite operation boundaries. */
1047
- var SqliteStorageFailpoint = class SqliteStorageFailpoint extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageFailpoint") {
1048
- /** Production default: no fault injection. */
1049
- static layer = Layer.succeed(this)({ hit: noFailpoint });
1050
- /** Reusable test Layer with a control service backed by the same handler Ref. */
1051
- static layerTest = Layer.effectContext(Effect.gen(function* () {
1052
- const handler = yield* Ref.make(noFailpoint);
1053
- return Context.make(SqliteStorageFailpoint, SqliteStorageFailpoint.of({ hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))) })).pipe(Context.add(SqliteStorageFailpointTestControl, SqliteStorageFailpointTestControl.of({
1054
- clear: Ref.set(handler, noFailpoint),
1055
- setHandler: (next) => Ref.set(handler, next)
1056
- })));
1057
- }));
1058
- };
1059
- //#endregion
1060
- //#region src/sqlite-conversation-store.ts
1061
- const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
1062
- const SQLITE_OFFSET_PREFIX = "effect-agent-sqlite@1:";
1063
- const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
1064
- const isDigest = Schema.is(Digest);
1065
- const storeError = (operation, error) => ConversationStoreError.make({
1066
- cause: error,
1067
- operation,
1068
- message: error.message
1069
- });
1070
- const schemaStoreError = (operation, error) => ConversationStoreError.make({
1071
- cause: error,
1072
- operation,
1073
- message: error.message
1074
- });
1075
- const makeOffset = Effect.fn("SqliteConversationStore.makeOffset")(function* (conversationId, sequence) {
1076
- return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.flatMap((validatedSequence) => Schema.decodeUnknownEffect(ObservationOffset)(`${SQLITE_OFFSET_PREFIX}${encodeURIComponent(conversationId)}:${validatedSequence}`)), Effect.mapError((error) => schemaStoreError("encode observation offset", error)));
1077
- });
1078
- const parseOffset = Effect.fn("SqliteConversationStore.parseOffset")(function* (conversationId, offset) {
1079
- if (offset === void 0) return ZERO_CANONICAL_SEQUENCE;
1080
- const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
1081
- const conversationPrefix = `${SQLITE_OFFSET_PREFIX}${encodeURIComponent(conversationId)}:`;
1082
- if (!text.startsWith(conversationPrefix)) return yield* ConversationStoreError.make({
1083
- operation: "decode observation offset",
1084
- message: "The observation offset belongs to a different adapter, storage version, or Conversation."
1085
- });
1086
- const sequenceText = text.slice(conversationPrefix.length);
1087
- if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield* ConversationStoreError.make({
1088
- operation: "decode observation offset",
1089
- message: "The observation offset is malformed."
1090
- });
1091
- return yield* Schema.decodeUnknownEffect(CanonicalSequence)(Number(sequenceText)).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
1092
- });
1093
- const mapFence = (conversationId, error) => FenceRejected.make({
1094
- conversationId,
1095
- actualEpoch: error.actualEpoch,
1096
- attemptedEpoch: error.producerEpoch
1097
- });
1098
- const encodeCanonicalRecord = Effect.fn("SqliteConversationStore.encodeCanonicalRecord")(function* (record) {
1099
- return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(record).pipe(Effect.mapError((error) => schemaStoreError("encode canonical record", error)));
1100
- });
1101
- const encodeCanonicalBatch = Effect.fn("SqliteConversationStore.encodeCanonicalBatch")(function* (batch) {
1102
- return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalBatch))(batch).pipe(Effect.mapError((error) => schemaStoreError("encode canonical batch", error)));
1103
- });
1104
- const encodeCheckpoint = Effect.fn("SqliteConversationStore.encodeCheckpoint")(function* (checkpoint) {
1105
- return yield* Schema.encodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpoint).pipe(Effect.mapError((error) => schemaStoreError("encode checkpoint", error)));
1106
- });
1107
- const decodeEnvelope = Effect.fn("SqliteConversationStore.decodeEnvelope")(function* (row) {
1108
- const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) => ConversationStoreError.make({
1109
- operation: "decode canonical record",
1110
- message: error.message
1111
- })));
1112
- const conversationId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.conversationId)(row.conversation_id).pipe(Effect.mapError((error) => schemaStoreError("decode conversation identity", error)));
1113
- const offset = yield* makeOffset(conversationId, row.sequence);
1114
- const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(row.batch_id).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
1115
- return CanonicalRecordEnvelope.make({
1116
- conversationId,
1117
- batchId,
1118
- sequence: row.sequence,
1119
- offset,
1120
- record
1121
- });
1122
- });
1123
- const decodeCheckpoint = Effect.fn("SqliteConversationStore.decodeCheckpoint")(function* (checkpointJson) {
1124
- return yield* Schema.decodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpointJson).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint", error)));
1125
- });
1126
- const requireConversation = Effect.fn("SqliteConversationStore.requireConversation")(function* (journal, conversationId) {
1127
- const rows = yield* journal.getConversation(conversationId).pipe(Effect.mapError((error) => storeError("read conversation", error)));
1128
- if (rows.length === 0) return yield* ConversationNotMaterialized.make({ conversationId });
1129
- return rows[0];
1130
- });
1131
- const tailDigestAt = Effect.fn("SqliteConversationStore.tailDigestAt")(function* (journal, conversationId, sequence) {
1132
- if (sequence === 0) return EMPTY_TAIL_DIGEST;
1133
- const digests = yield* journal.getTailDigestAt(conversationId, sequence).pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
1134
- if (digests.length !== 1) return yield* CheckpointRejected.make({
1135
- conversationId,
1136
- reason: "digest-mismatch"
1137
- });
1138
- return yield* Schema.decodeUnknownEffect(Digest)(digests[0]).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)));
1139
- });
1140
- const groupByKey = (rows, key) => {
1141
- const grouped = /* @__PURE__ */ new Map();
1142
- for (const row of rows) {
1143
- const existing = grouped.get(key(row));
1144
- if (existing === void 0) grouped.set(key(row), [row]);
1145
- else existing.push(row);
1146
- }
1147
- return grouped;
1148
- };
1149
- /**
1150
- * Opt-in full integrity audit (`verifyOnOpen`). Every stored payload is decoded, re-encoded,
1151
- * and re-digested against the canonical chain. Routine opens skip this scan: per-operation
1152
- * Schema decoding plus the digest chain already fail clearly on corrupt rows.
1153
- */
1154
- const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPayloads")(function* (journal, crypto) {
1155
- const stored = yield* journal.scanStoredPayloads();
1156
- const batches = yield* Effect.forEach(stored.batches, (batch) => Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(Effect.map((decoded) => ({
1157
- decoded,
1158
- row: batch
1159
- })), Effect.mapError((error) => SqliteStorageCorruptionError.make({
1160
- table: "effect_agent_canonical_batches",
1161
- rowKey: `${batch.conversation_id}/${batch.batch_id}`,
1162
- message: error.message
1163
- }))));
1164
- const records = yield* Effect.forEach(stored.records, (record) => Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(Effect.map((decoded) => ({
1165
- decoded,
1166
- row: record
1167
- })), Effect.mapError((error) => SqliteStorageCorruptionError.make({
1168
- table: "effect_agent_canonical_records",
1169
- rowKey: `${record.conversation_id}/${record.sequence}`,
1170
- message: error.message
1171
- }))));
1172
- const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpoint.checkpoint_json).pipe(Effect.map((decoded) => ({
1173
- decoded,
1174
- row: checkpoint
1175
- })), Effect.mapError((error) => SqliteStorageCorruptionError.make({
1176
- table: "effect_agent_checkpoints",
1177
- rowKey: `${checkpoint.conversation_id}/${checkpoint.through_sequence}`,
1178
- message: error.message
1179
- }))));
1180
- const batchesByConversation = groupByKey(batches, ({ row }) => row.conversation_id);
1181
- const recordsByConversation = groupByKey(records, ({ row }) => row.conversation_id);
1182
- const checkpointsByConversation = groupByKey(checkpoints, ({ row }) => row.conversation_id);
1183
- const materializedIds = new Set(stored.conversations.map((conversation) => conversation.conversation_id));
1184
- for (const conversation of stored.conversations) {
1185
- const conversationBatches = batchesByConversation.get(conversation.conversation_id) ?? [];
1186
- const conversationRecords = recordsByConversation.get(conversation.conversation_id) ?? [];
1187
- const conversationCheckpoints = checkpointsByConversation.get(conversation.conversation_id) ?? [];
1188
- const recordsByBatch = groupByKey(conversationRecords, ({ row }) => row.batch_id);
1189
- let previousDigest = EMPTY_TAIL_DIGEST;
1190
- let expectedSequence = 1;
1191
- const tailDigests = /* @__PURE__ */ new Map([[0, EMPTY_TAIL_DIGEST]]);
1192
- for (const { decoded: canonicalBatch, row: batchRow } of conversationBatches) {
1193
- const key = `${batchRow.conversation_id}/${batchRow.batch_id}`;
1194
- if (canonicalBatch.batchId !== batchRow.batch_id || batchRow.first_sequence !== expectedSequence || batchRow.last_sequence !== batchRow.first_sequence + canonicalBatch.records.length - 1) return yield* SqliteStorageCorruptionError.make({
1195
- table: "effect_agent_canonical_batches",
1196
- rowKey: key,
1197
- message: "Canonical batch identity, sequence, or record count is inconsistent."
1198
- });
1199
- const digest = yield* digestCanonicalBatch(previousDigest, canonicalBatch).pipe(Effect.provideService(Crypto.Crypto, crypto), Effect.mapError((error) => SqliteStorageCorruptionError.make({
1200
- table: "effect_agent_canonical_batches",
1201
- rowKey: key,
1202
- message: error.message
1203
- })));
1204
- if (batchRow.batch_digest !== digest || batchRow.tail_digest !== digest) return yield* SqliteStorageCorruptionError.make({
1205
- table: "effect_agent_canonical_batches",
1206
- rowKey: key,
1207
- message: "Canonical batch digest does not match its decoded content and prior tail."
1208
- });
1209
- const batchRecords = recordsByBatch.get(batchRow.batch_id) ?? [];
1210
- if (batchRecords.length !== canonicalBatch.records.length) return yield* SqliteStorageCorruptionError.make({
1211
- table: "effect_agent_canonical_records",
1212
- rowKey: key,
1213
- message: "Canonical batch and record-table counts differ."
1214
- });
1215
- for (let index = 0; index < canonicalBatch.records.length; index++) {
1216
- const expectedRecord = canonicalBatch.records[index];
1217
- const storedRecord = batchRecords[index];
1218
- const expectedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(expectedRecord).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
1219
- table: "effect_agent_canonical_batches",
1220
- rowKey: key,
1221
- message: error.message
1222
- })));
1223
- const storedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(storedRecord.decoded).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
1224
- table: "effect_agent_canonical_records",
1225
- rowKey: `${key}/${storedRecord.row.sequence}`,
1226
- message: error.message
1227
- })));
1228
- if (storedRecord.row.sequence !== batchRow.first_sequence + index || storedRecord.row.record_id !== expectedRecord.recordId || expectedJson !== storedJson) return yield* SqliteStorageCorruptionError.make({
1229
- table: "effect_agent_canonical_records",
1230
- rowKey: `${key}/${storedRecord.row.sequence}`,
1231
- message: "Canonical record identity, sequence, or payload differs from its batch."
1232
- });
1233
- }
1234
- previousDigest = digest;
1235
- expectedSequence = batchRow.last_sequence + 1;
1236
- tailDigests.set(batchRow.last_sequence, digest);
1237
- }
1238
- if (conversationRecords.length !== conversation.tail_sequence || conversation.tail_sequence !== expectedSequence - 1 || conversation.tail_digest !== previousDigest) return yield* SqliteStorageCorruptionError.make({
1239
- table: "effect_agent_conversations",
1240
- rowKey: conversation.conversation_id,
1241
- message: "Conversation tail does not match its canonical batch chain."
1242
- });
1243
- for (const checkpoint of conversationCheckpoints) if (checkpoint.decoded.conversationId !== conversation.conversation_id || checkpoint.decoded.throughSequence !== checkpoint.row.through_sequence || checkpoint.decoded.tailDigest !== checkpoint.row.tail_digest || tailDigests.get(checkpoint.row.through_sequence) !== checkpoint.row.tail_digest) return yield* SqliteStorageCorruptionError.make({
1244
- table: "effect_agent_checkpoints",
1245
- rowKey: `${conversation.conversation_id}/${checkpoint.row.through_sequence}`,
1246
- message: "Checkpoint identity or digest is not bound to a canonical batch tail."
1247
- });
1248
- }
1249
- if (batches.some(({ row }) => !materializedIds.has(row.conversation_id)) || records.some(({ row }) => !materializedIds.has(row.conversation_id)) || checkpoints.some(({ row }) => !materializedIds.has(row.conversation_id))) return yield* SqliteStorageCorruptionError.make({
1250
- table: "effect_agent_conversations",
1251
- rowKey: "startup_scan",
1252
- message: "Canonical rows exist without a materialized Conversation."
1253
- });
1254
- });
1255
- const makeServices$1 = Effect.fn("SqliteConversationStore.makeServices")(function* () {
1256
- const config = yield* SqliteStorageConfig;
1257
- const failpoint = yield* SqliteStorageFailpoint;
1258
- const sql = yield* SqlClient.SqlClient;
1259
- const crypto = yield* Crypto.Crypto;
1260
- const journal = yield* initializeSqliteJournal(sql, failpoint.hit, config.busyTimeout);
1261
- if (config.verifyOnOpen) yield* decodeStartupPayloads(journal, crypto);
1262
- const provideCrypto = (effect) => Effect.provideService(effect, Crypto.Crypto, crypto);
1263
- const hitFailpoint = Effect.fn("SqliteConversationStore.hitFailpoint")((location) => failpoint.hit(location).pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))));
1264
- const materialize = Effect.fn("SqliteConversationStore.materialize")(function* (request) {
1265
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationMaterialization))(request).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
1266
- const now = yield* Clock.currentTimeMillis;
1267
- yield* hitFailpoint("materialize:before");
1268
- yield* journal.materialize(validated.conversationId, new Date(now).toISOString(), EMPTY_TAIL_DIGEST, validated.producerEpoch).pipe(Effect.mapError((error) => error._tag === "SqliteFenceRejected" ? mapFence(validated.conversationId, error) : storeError("materialize conversation", error)));
1269
- yield* hitFailpoint("materialize:after");
1270
- });
1271
- const append = Effect.fn("SqliteConversationStore.append")(function* (request) {
1272
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
1273
- yield* requireConversation(journal, validated.conversationId);
1274
- const tailDigest = yield* provideCrypto(digestCanonicalBatch(validated.expectedTailDigest, validated.batch)).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
1275
- const batchJson = yield* encodeCanonicalBatch(validated.batch);
1276
- const rawRecords = yield* Effect.forEach(validated.batch.records, (record) => encodeCanonicalRecord(record).pipe(Effect.map((recordJson) => ({
1277
- recordId: record.recordId,
1278
- recordJson
1279
- }))));
1280
- const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
1281
- conversationId: validated.conversationId,
1282
- batchId: validated.batch.batchId,
1283
- batchDigest: tailDigest,
1284
- batchJson,
1285
- expectedTailSequence: validated.expectedTailSequence,
1286
- expectedTailDigest: validated.expectedTailDigest,
1287
- producerEpoch: validated.producerEpoch,
1288
- records: rawRecords,
1289
- tailDigest
1290
- }).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
1291
- yield* hitFailpoint("append:before");
1292
- const result = yield* journal.append(rawRequest).pipe(Effect.mapError((error) => {
1293
- if (error instanceof SqliteFenceRejected) return mapFence(validated.conversationId, error);
1294
- if (error instanceof SqliteAppendConflict) return error.actualTailSequence !== void 0 && isDigest(error.actualTailDigest) ? AppendConflict.make({
1295
- conversationId: validated.conversationId,
1296
- batchId: validated.batch.batchId,
1297
- reason: error.reason,
1298
- actualTailSequence: error.actualTailSequence,
1299
- actualTailDigest: error.actualTailDigest
1300
- }) : AppendConflict.make({
1301
- conversationId: validated.conversationId,
1302
- batchId: validated.batch.batchId,
1303
- reason: error.reason
1304
- });
1305
- return storeError("append canonical batch", error);
1306
- }), Effect.flatMap((result) => Schema.decodeUnknownEffect(AppendResult)(result).pipe(Effect.mapError((error) => schemaStoreError("decode append result", error)))));
1307
- yield* hitFailpoint("append:after");
1308
- return result;
1309
- });
1310
- const loadRecords = Effect.fn("SqliteConversationStore.loadRecords")(function* (request) {
1311
- const rows = yield* journal.read(request).pipe(Effect.mapError((error) => storeError("read canonical records", error)));
1312
- return yield* Effect.forEach(rows, decodeEnvelope);
1313
- });
1314
- const readEffect = Effect.fn("SqliteConversationStore.read")(function* (request) {
1315
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationRead))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation read", error)));
1316
- yield* requireConversation(journal, validated.conversationId);
1317
- const records = yield* loadRecords(RawReadRequest.make({
1318
- conversationId: validated.conversationId,
1319
- fromSequenceExclusive: validated.afterSequence ?? ZERO_CANONICAL_SEQUENCE,
1320
- limit: validated.limit
1321
- }));
1322
- return Stream.fromIterable(records);
1323
- });
1324
- const read = (request) => Stream.unwrap(readEffect(request));
1325
- const observeEffect = Effect.fn("SqliteConversationStore.observe")(function* (request) {
1326
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationObservation))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation observation", error)));
1327
- yield* requireConversation(journal, validated.conversationId);
1328
- const initialSequence = yield* parseOffset(validated.conversationId, validated.afterOffset);
1329
- const cursor = yield* Ref.make(initialSequence);
1330
- const poll = Effect.fn("SqliteConversationStore.observePoll")(function* () {
1331
- const fromSequenceExclusive = yield* Ref.get(cursor);
1332
- const records = yield* loadRecords(RawReadRequest.make({
1333
- conversationId: validated.conversationId,
1334
- fromSequenceExclusive,
1335
- limit: 1024
1336
- }));
1337
- if (records.length === 0) {
1338
- yield* Effect.sleep(config.observationPollInterval);
1339
- return [];
1340
- }
1341
- yield* Ref.set(cursor, records[records.length - 1].sequence);
1342
- return records;
1343
- });
1344
- return Stream.fromIterableEffectRepeat(poll());
1345
- });
1346
- const observe = (request) => Stream.unwrap(observeEffect(request));
1347
- const exportConversation = Effect.fn("SqliteConversationStore.export")(function* (request) {
1348
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationExportRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation export", error)));
1349
- yield* requireConversation(journal, validated.conversationId);
1350
- const exported = yield* journal.exportConversation(validated.conversationId).pipe(Effect.mapError((error) => storeError("export conversation", error)));
1351
- const records = yield* Effect.forEach(exported.records, decodeEnvelope);
1352
- if (records.length > 65536) return yield* ConversationStoreError.make({
1353
- operation: "decode conversation export",
1354
- message: "The conversation exceeds the current export record limit."
1355
- });
1356
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.conversation.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
1357
- return ConversationExport.make({
1358
- format: "effect-agent/conversation@1",
1359
- conversationId: validated.conversationId,
1360
- tailSequence: exported.conversation.tail_sequence,
1361
- tailDigest,
1362
- records
1363
- });
1364
- });
1365
- const inspectTail = Effect.fn("SqliteConversationStore.inspectTail")(function* (request) {
1366
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationTailRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
1367
- const conversation = yield* requireConversation(journal, validated.conversationId);
1368
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(conversation.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode tail digest", error)));
1369
- return ConversationTail.make({
1370
- conversationId: validated.conversationId,
1371
- tailSequence: conversation.tail_sequence,
1372
- tailDigest,
1373
- producerEpoch: conversation.producer_epoch
1374
- });
1375
- });
1376
- const saveCheckpoint = Effect.fn("SqliteConversationStore.saveCheckpoint")(function* (request) {
1377
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
1378
- const conversation = yield* requireConversation(journal, validated.checkpoint.conversationId);
1379
- if (validated.checkpoint.throughSequence > conversation.tail_sequence) return yield* CheckpointRejected.make({
1380
- conversationId: validated.checkpoint.conversationId,
1381
- reason: "ahead-of-tail"
1382
- });
1383
- if ((yield* tailDigestAt(journal, validated.checkpoint.conversationId, validated.checkpoint.throughSequence)) !== validated.checkpoint.tailDigest) return yield* CheckpointRejected.make({
1384
- conversationId: validated.checkpoint.conversationId,
1385
- reason: "digest-mismatch"
1386
- });
1387
- const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
1388
- const raw = RawCheckpoint.make({
1389
- conversationId: validated.checkpoint.conversationId,
1390
- throughSequence: validated.checkpoint.throughSequence,
1391
- tailDigest: validated.checkpoint.tailDigest,
1392
- checkpointJson
1393
- });
1394
- yield* hitFailpoint("save-checkpoint:before");
1395
- yield* journal.saveCheckpoint(raw).pipe(Effect.mapError((error) => error instanceof SqliteCheckpointConflict ? CheckpointRejected.make({
1396
- conversationId: validated.checkpoint.conversationId,
1397
- reason: "digest-mismatch"
1398
- }) : storeError("save checkpoint", error)));
1399
- yield* hitFailpoint("save-checkpoint:after");
1400
- });
1401
- const loadCheckpoint = Effect.fn("SqliteConversationStore.loadCheckpoint")(function* (request) {
1402
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
1403
- const conversation = yield* requireConversation(journal, validated.conversationId);
1404
- const rows = yield* journal.loadCheckpoint(validated.conversationId, validated.atOrBeforeSequence ?? conversation.tail_sequence).pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
1405
- if (rows.length === 0) return Option.none();
1406
- if (rows.length !== 1) return yield* ConversationStoreError.make({
1407
- operation: "load checkpoint",
1408
- message: `Expected at most one checkpoint row but found ${rows.length}.`
1409
- });
1410
- const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
1411
- if ((yield* tailDigestAt(journal, checkpoint.conversationId, checkpoint.throughSequence)) !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
1412
- conversationId: checkpoint.conversationId,
1413
- reason: "digest-mismatch"
1414
- });
1415
- return Option.some(checkpoint);
1416
- });
1417
- const conversationStore = ConversationStore.of({
1418
- append,
1419
- export: exportConversation,
1420
- inspectTail,
1421
- loadCheckpoint,
1422
- materialize,
1423
- observe,
1424
- read,
1425
- saveCheckpoint
1426
- });
1427
- return Context.make(ConversationStore, conversationStore);
1428
- });
1429
- /**
1430
- * SQLite Conversation Store implementation with configuration, failpoint, SQL, and Crypto
1431
- * authority kept visible in its input channel.
1432
- */
1433
- const conversationStoreLayer = Layer.effectContext(makeServices$1());
1434
- /**
1435
- * Validated SQLite storage configuration Layer with the documented defaults applied. Shared
1436
- * by the ConversationStore and SubmissionLedger convenience layers so their defaults cannot
1437
- * drift.
1438
- */
1439
- const storageConfigLayer = (options) => Layer.effect(SqliteStorageConfig)(Schema.decodeUnknownEffect(SqliteStorageConfigValue)({
1440
- observationPollInterval: options.observationPollInterval ?? 25,
1441
- busyTimeout: options.busyTimeout ?? 5e3,
1442
- ownershipLeaseDuration: options.ownershipLeaseDuration ?? Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION),
1443
- verifyOnOpen: options.verifyOnOpen ?? false
1444
- }).pipe(Effect.mapError((error) => SqliteStorageError.make({
1445
- cause: error,
1446
- operation: "configure SQLite storage",
1447
- message: error.message
1448
- }))));
1449
- /** The failpoint Layer selected by convenience options: explicit handler or the no-op default. */
1450
- const storageFailpointLayer = (options) => options.failpoint === void 0 ? SqliteStorageFailpoint.layer : Layer.succeed(SqliteStorageFailpoint)({ hit: options.failpoint });
1451
- /**
1452
- * A composition-root convenience Layer for canonical Conversations. Durable accepted work is
1453
- * served by the separate SubmissionLedger port.
1454
- */
1455
- const layer = (options) => {
1456
- const sqlLayer = SqliteClient.layer({ filename: options.filename });
1457
- return conversationStoreLayer.pipe(Layer.provide(Layer.mergeAll(storageConfigLayer(options), storageFailpointLayer(options), sqlLayer, NodeCrypto.layer)));
1458
- };
1459
- /** Create an adapter-owned resumable observation offset for a known canonical sequence. */
1460
- const observationOffsetAt = makeOffset;
1461
- //#endregion
1462
- //#region src/sqlite-ledger.ts
1463
- const BoundedStoredText = Schema.String.check(Schema.isMaxLength(16 * 1024 * 1024));
1464
- const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
1465
- const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
1466
- const SCAN_PAGE_SIZE = 256;
1467
- const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
1468
- var SubmissionRow = class extends Schema.Class("SubmissionRow")({
1469
- submission_id: BoundedIdentifier,
1470
- conversation_id: BoundedIdentifier,
1471
- queue_sequence: QueueSequence,
1472
- principal: BoundedIdentifier,
1473
- idempotency_key: BoundedIdentifier,
1474
- agent_id: BoundedIdentifier,
1475
- agent_digests_json: BoundedStoredText,
1476
- deployment_id: BoundedIdentifier,
1477
- input_json: BoundedStoredText,
1478
- input_digest: Digest,
1479
- receipt_id: BoundedIdentifier,
1480
- state: SubmissionState,
1481
- settled_outcome: Schema.NullOr(SettlementOutcome),
1482
- created_at: BoundedTimestamp,
1483
- ready_at: Schema.NullOr(BoundedTimestamp),
1484
- input_applied_record_id: Schema.NullOr(BoundedIdentifier),
1485
- input_applied_sequence: Schema.NullOr(CanonicalSequence),
1486
- joined_host_submission_id: Schema.NullOr(BoundedIdentifier),
1487
- suspended_reason_json: Schema.NullOr(BoundedStoredText),
1488
- suspended_at: Schema.NullOr(BoundedTimestamp),
1489
- unknown_reason: Schema.NullOr(BoundedStoredText),
1490
- unknown_tool_call_ids_json: Schema.NullOr(BoundedStoredText),
1491
- parent_submission_id: Schema.NullOr(BoundedIdentifier),
1492
- parent_tool_call_id: Schema.NullOr(BoundedIdentifier)
1493
- }) {};
1494
- var ChildReservationRow = class extends Schema.Class("ChildReservationRow")({
1495
- reservation_id: BoundedIdentifier,
1496
- parent_submission_id: BoundedIdentifier,
1497
- parent_tool_call_id: BoundedIdentifier,
1498
- child_submission_id: Schema.NullOr(BoundedIdentifier),
1499
- status: ChildReservationStatus,
1500
- allocation_json: BoundedStoredText,
1501
- allocation_digest: Digest,
1502
- accounting_json: Schema.NullOr(BoundedStoredText),
1503
- reserved_at: BoundedTimestamp,
1504
- release_began_at: Schema.NullOr(BoundedTimestamp),
1505
- released_at: Schema.NullOr(BoundedTimestamp)
1506
- }) {};
1507
- var ApprovalDecisionRow = class extends Schema.Class("ApprovalDecisionRow")({
1508
- submission_id: BoundedIdentifier,
1509
- tool_call_id: BoundedIdentifier,
1510
- decision: ApprovalDecision,
1511
- resolver: BoundedIdentifier,
1512
- reason: BoundedStoredText,
1513
- decided_at: BoundedTimestamp
1514
- }) {};
1515
- var UnknownResolutionRow = class extends Schema.Class("UnknownResolutionRow")({
1516
- submission_id: BoundedIdentifier,
1517
- tool_call_id: BoundedIdentifier,
1518
- author: BoundedIdentifier,
1519
- reason: BoundedStoredText,
1520
- resolution_json: BoundedStoredText,
1521
- resolved_at: BoundedTimestamp
1522
- }) {};
1523
- var OwnershipRow = class extends Schema.Class("OwnershipRow")({
1524
- submission_id: BoundedIdentifier,
1525
- attempt_id: BoundedIdentifier,
1526
- ownership_token: BoundedIdentifier,
1527
- producer_epoch: ProducerEpoch,
1528
- owner_producer_id: BoundedIdentifier,
1529
- lease_expires_at: BoundedTimestamp
1530
- }) {};
1531
- var ReservationRow = class extends Schema.Class("ReservationRow")({
1532
- submission_id: BoundedIdentifier,
1533
- settlement_id: BoundedIdentifier,
1534
- outcome: SettlementOutcome,
1535
- record_id: BoundedIdentifier,
1536
- record_json: BoundedStoredText,
1537
- record_digest: Digest,
1538
- reserved_at: BoundedTimestamp,
1539
- finalized_at: Schema.NullOr(BoundedTimestamp)
1540
- }) {};
1541
- var AbortIntentRow = class extends Schema.Class("AbortIntentRow")({
1542
- submission_id: BoundedIdentifier,
1543
- author: BoundedIdentifier,
1544
- reason: BoundedStoredText,
1545
- requested_at: BoundedTimestamp,
1546
- canonical_record_id: Schema.NullOr(BoundedIdentifier)
1547
- }) {};
1548
- var MaxQueueSequenceRow = class extends Schema.Class("MaxQueueSequenceRow")({ max_queue_sequence: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) }) {};
1549
- var CanonicalRecordIdRow = class extends Schema.Class("CanonicalRecordIdRow")({ record_id: BoundedIdentifier }) {};
1550
- const SUBMISSION_COLUMNS = `
1551
- submission_id,
1552
- conversation_id,
1553
- queue_sequence,
1554
- principal,
1555
- idempotency_key,
1556
- agent_id,
1557
- agent_digests_json,
1558
- deployment_id,
1559
- input_json,
1560
- input_digest,
1561
- receipt_id,
1562
- state,
1563
- settled_outcome,
1564
- created_at,
1565
- ready_at,
1566
- input_applied_record_id,
1567
- input_applied_sequence,
1568
- joined_host_submission_id,
1569
- suspended_reason_json,
1570
- suspended_at,
1571
- unknown_reason,
1572
- unknown_tool_call_ids_json,
1573
- parent_submission_id,
1574
- parent_tool_call_id
1575
- `;
1576
- const CHILD_RESERVATION_COLUMNS = `
1577
- reservation_id,
1578
- parent_submission_id,
1579
- parent_tool_call_id,
1580
- child_submission_id,
1581
- status,
1582
- allocation_json,
1583
- allocation_digest,
1584
- accounting_json,
1585
- reserved_at,
1586
- release_began_at,
1587
- released_at
1588
- `;
1589
- /** The branded ToolCallId schema, reached through the session port so no core import is needed. */
1590
- const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
1591
- const ToolCallIdList = Schema.Array(ToolCallIdSchema);
1592
- const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
1593
- const encodeDefinitionDigestsText = Schema.encodeEffect(Schema.fromJsonString(DefinitionDigests));
1594
- const encodeRecordEnvelopeText = Schema.encodeEffect(Schema.fromJsonString(RecordEnvelope));
1595
- const decodeRecordEnvelopeText = Schema.decodeEffect(Schema.fromJsonString(RecordEnvelope));
1596
- const encodeSuspensionReasonText = Schema.encodeEffect(Schema.fromJsonString(SuspensionReason));
1597
- const encodeUnknownResolutionText = Schema.encodeEffect(Schema.fromJsonString(UnknownResolution));
1598
- const encodeToolCallIdsText = Schema.encodeEffect(Schema.fromJsonString(ToolCallIdList));
1599
- const decodeToolCallIdsText = Schema.decodeEffect(Schema.fromJsonString(ToolCallIdList));
1600
- const parseStoredJsonText = Schema.decodeEffect(Schema.fromJsonString(Schema.Json));
1601
- const decodeAdmissionResult = Schema.decodeUnknownEffect(AdmissionResult);
1602
- const decodeClaim = Schema.decodeUnknownEffect(Claim);
1603
- const decodeOwnershipRenewal = Schema.decodeUnknownEffect(OwnershipRenewal);
1604
- const decodeSettlement = Schema.decodeUnknownEffect(Settlement);
1605
- const decodeAbortIntent = Schema.decodeUnknownEffect(AbortIntent);
1606
- const decodeOwnershipSnapshot = Schema.decodeUnknownEffect(OwnershipSnapshot);
1607
- const decodeInputAppliedMarker = Schema.decodeUnknownEffect(InputAppliedMarker);
1608
- const decodeSubmissionSnapshotUnknown = Schema.decodeUnknownEffect(SubmissionSnapshot);
1609
- const decodeSubmissionId = Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId);
1610
- const decodeQueueSequence = Schema.decodeUnknownEffect(QueueSequence);
1611
- const decodeUtcInstant = Schema.decodeUnknownEffect(Schema.DateTimeUtcFromString);
1612
- const decodeJoiningClaim = Schema.decodeUnknownEffect(JoiningClaim);
1613
- const decodeJoinSnapshot = Schema.decodeUnknownEffect(JoinSnapshot);
1614
- const decodeSuspensionSnapshot = Schema.decodeUnknownEffect(SuspensionSnapshot);
1615
- const decodeApprovalDecisionIntent = Schema.decodeUnknownEffect(ApprovalDecisionIntent);
1616
- const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResolutionIntent);
1617
- const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
1618
- const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(ChildBudgetReservationSnapshot);
1619
- const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
1620
- /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
1621
- const internalFailure = (operation) => (error) => LedgerError.make({
1622
- operation,
1623
- message: error.message,
1624
- cause: error
1625
- });
1626
- /** Classify raw SQL failures: write-lock timeouts stay retryable typed contention. */
1627
- const sqlFailure = (operation) => (error) => {
1628
- const internal = error.reason._tag === "LockTimeoutError" ? SqliteWriteContention.make({
1629
- cause: error,
1630
- operation,
1631
- message: `Another producer holds the SQLite write lock; ${operation} is safe to retry.`
1632
- }) : SqliteLedgerError.make({
1633
- cause: error,
1634
- operation,
1635
- message: error.message
1636
- });
1637
- return internalFailure(operation)(internal);
1638
- };
1639
- const corruptionFailure = (operation, table, rowKey, message) => internalFailure(operation)(SqliteStorageCorruptionError.make({
1640
- table,
1641
- rowKey,
1642
- message
1643
- }));
1644
- const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function* () {
1645
- const config = yield* SqliteStorageConfig;
1646
- const failpoint = yield* SqliteStorageFailpoint;
1647
- const sql = yield* SqlClient.SqlClient;
1648
- const crypto = yield* Crypto.Crypto;
1649
- const journal = yield* initializeSqliteJournal(sql, failpoint.hit, config.busyTimeout);
1650
- const hitFailpoint = (location, operation) => failpoint.hit(location).pipe(Effect.mapError((error) => internalFailure(operation)(error)));
1651
- /**
1652
- * Run one ledger mutation under the journal's `BEGIN IMMEDIATE` write transaction so
1653
- * ownership-token and epoch checks are atomic with their writes (DUR-006). Transaction
1654
- * acquisition failures surface as LedgerError carrying the typed retryable
1655
- * SqliteWriteContention (or SqliteStorageError) as cause.
1656
- */
1657
- const inWriteTransaction = (operation, effect) => journal.withWriteTransaction(operation)(effect).pipe(Effect.mapError((error) => error instanceof SqliteStorageError || error instanceof SqliteWriteContention ? internalFailure(operation)(error) : error));
1658
- const mintIdentifier = (prefix, operation) => crypto.randomUUIDv7.pipe(Effect.map((uuid) => `${prefix}-${uuid}`), Effect.mapError((error) => internalFailure(operation)(error)));
1659
- const currentInstant = Effect.map(Clock.currentTimeMillis, (millis) => ({
1660
- millis,
1661
- iso: new Date(millis).toISOString()
1662
- }));
1663
- const timestampMillis = (operation, rowKey) => (timestamp) => decodeUtcInstant(timestamp).pipe(Effect.map(DateTime.toEpochMillis), Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submission_ownership", rowKey, error.message)));
1664
- const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1665
- const readSubmission = Effect.fn("SqliteSubmissionLedger.readSubmission")(function* (operation, submissionId) {
1666
- const rows = yield* sql`
1667
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1668
- FROM effect_agent_submissions
1669
- WHERE submission_id = ${submissionId}
1670
- `.pipe(Effect.mapError(sqlFailure(operation)));
1671
- const decoded = yield* decodeSubmissionRows(operation, submissionId, rows);
1672
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", submissionId, "A submission primary key returned more than one row.");
1673
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1674
- });
1675
- const requireSubmission = Effect.fn("SqliteSubmissionLedger.requireSubmission")(function* (operation, submissionId) {
1676
- const submission = yield* readSubmission(operation, submissionId);
1677
- if (Option.isNone(submission)) return yield* LedgerError.make({
1678
- operation,
1679
- message: `Unknown submission ${submissionId}.`
1680
- });
1681
- return submission.value;
1682
- });
1683
- const readOwnership = Effect.fn("SqliteSubmissionLedger.readOwnership")(function* (operation, submissionId) {
1684
- const rows = yield* sql`
1685
- SELECT
1686
- submission_id,
1687
- attempt_id,
1688
- ownership_token,
1689
- producer_epoch,
1690
- owner_producer_id,
1691
- lease_expires_at
1692
- FROM effect_agent_submission_ownership
1693
- WHERE submission_id = ${submissionId}
1694
- `.pipe(Effect.mapError(sqlFailure(operation)));
1695
- const decoded = yield* decodeRows(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1696
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submission_ownership", submissionId, "An ownership primary key returned more than one row.");
1697
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1698
- });
1699
- const conversationEpoch = Effect.fn("SqliteSubmissionLedger.conversationEpoch")(function* (operation, conversationId) {
1700
- const conversations = yield* journal.getConversation(conversationId).pipe(Effect.mapError(internalFailure(operation)));
1701
- return conversations.length === 0 ? EPOCH_ZERO : conversations[0].producer_epoch;
1702
- });
1703
- /**
1704
- * Verify inside the surrounding write transaction that the presented token still owns the
1705
- * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
1706
- * Conversation's current producer epoch (DUR-006).
1707
- */
1708
- const requireOwnership = Effect.fn("SqliteSubmissionLedger.requireOwnership")(function* (operation, submission, ownershipToken) {
1709
- const ownership = yield* readOwnership(operation, submission.submission_id);
1710
- if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
1711
- const actualEpoch = yield* conversationEpoch(operation, submission.conversation_id);
1712
- const submissionId = yield* Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId)(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
1713
- return yield* OwnershipLost.make({
1714
- submissionId,
1715
- actualEpoch
1716
- });
1717
- }
1718
- return ownership.value;
1719
- });
1720
- const decodeSubmissionSnapshot = Effect.fn("SqliteSubmissionLedger.decodeSubmissionSnapshot")(function* (operation, row) {
1721
- const agentDigests = yield* parseStoredJsonText(row.agent_digests_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
1722
- const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
1723
- if (row.parent_submission_id === null !== (row.parent_tool_call_id === null)) return yield* corruptionFailure(operation, "effect_agent_submissions", row.submission_id, "A parent linkage must record both the parent Submission and the parent Tool Call.");
1724
- return yield* decodeSubmissionSnapshotUnknown({
1725
- submissionId: row.submission_id,
1726
- conversationId: row.conversation_id,
1727
- queueSequence: row.queue_sequence,
1728
- principal: row.principal,
1729
- idempotencyKey: row.idempotency_key,
1730
- agentId: row.agent_id,
1731
- agentDigests,
1732
- deploymentId: row.deployment_id,
1733
- inputPayload,
1734
- inputDigest: row.input_digest,
1735
- receiptId: row.receipt_id,
1736
- state: row.state,
1737
- createdAt: row.created_at,
1738
- ...row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome },
1739
- ...row.ready_at === null ? {} : { readyAt: row.ready_at },
1740
- ...row.parent_submission_id === null || row.parent_tool_call_id === null ? {} : { parentLinkage: {
1741
- parentSubmissionId: row.parent_submission_id,
1742
- parentToolCallId: row.parent_tool_call_id
1743
- } }
1744
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
1745
- });
1746
- const readReservation = Effect.fn("SqliteSubmissionLedger.readReservation")(function* (operation, submissionId) {
1747
- const rows = yield* sql`
1748
- SELECT
1749
- submission_id,
1750
- settlement_id,
1751
- outcome,
1752
- record_id,
1753
- record_json,
1754
- record_digest,
1755
- reserved_at,
1756
- finalized_at
1757
- FROM effect_agent_settlement_reservations
1758
- WHERE submission_id = ${submissionId}
1759
- `.pipe(Effect.mapError(sqlFailure(operation)));
1760
- const decoded = yield* decodeRows(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1761
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", submissionId, "A settlement reservation primary key returned more than one row.");
1762
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1763
- });
1764
- const readAbortIntent = Effect.fn("SqliteSubmissionLedger.readAbortIntent")(function* (operation, submissionId) {
1765
- const rows = yield* sql`
1766
- SELECT
1767
- submission_id,
1768
- author,
1769
- reason,
1770
- requested_at,
1771
- canonical_record_id
1772
- FROM effect_agent_abort_intents
1773
- WHERE submission_id = ${submissionId}
1774
- `.pipe(Effect.mapError(sqlFailure(operation)));
1775
- const decoded = yield* decodeRows(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1776
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_abort_intents", submissionId, "An abort intent primary key returned more than one row.");
1777
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1778
- });
1779
- const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1780
- const readChildReservation = Effect.fn("SqliteSubmissionLedger.readChildReservation")(function* (operation, reservationId) {
1781
- const rows = yield* sql`
1782
- SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
1783
- FROM effect_agent_child_reservations
1784
- WHERE reservation_id = ${reservationId}
1785
- `.pipe(Effect.mapError(sqlFailure(operation)));
1786
- const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
1787
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_child_reservations", reservationId, "A child reservation primary key returned more than one row.");
1788
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1789
- });
1790
- const readChildReservationForCall = Effect.fn("SqliteSubmissionLedger.readChildReservationForCall")(function* (operation, parentSubmissionId, parentToolCallId) {
1791
- const rows = yield* sql`
1792
- SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
1793
- FROM effect_agent_child_reservations
1794
- WHERE parent_submission_id = ${parentSubmissionId}
1795
- AND parent_tool_call_id = ${parentToolCallId}
1796
- `.pipe(Effect.mapError(sqlFailure(operation)));
1797
- const decoded = yield* decodeChildReservationRows(operation, `${parentSubmissionId}/${parentToolCallId}`, rows);
1798
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_child_reservations", `${parentSubmissionId}/${parentToolCallId}`, "A parent Tool Call returned more than one child reservation.");
1799
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1800
- });
1801
- const childReservationSnapshotFromRow = Effect.fn("SqliteSubmissionLedger.childReservationSnapshotFromRow")(function* (operation, row) {
1802
- const rowFailure = (error) => corruptionFailure(operation, "effect_agent_child_reservations", row.reservation_id, error.message);
1803
- const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(Effect.mapError(rowFailure));
1804
- const accounting = row.accounting_json === null ? void 0 : yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
1805
- return yield* decodeChildReservationSnapshotUnknown({
1806
- reservationId: row.reservation_id,
1807
- parentSubmissionId: row.parent_submission_id,
1808
- parentToolCallId: row.parent_tool_call_id,
1809
- status: row.status,
1810
- allocation,
1811
- allocationDigest: row.allocation_digest,
1812
- reservedAt: row.reserved_at,
1813
- ...row.child_submission_id === null ? {} : { childSubmissionId: row.child_submission_id },
1814
- ...accounting === void 0 ? {} : { accounting },
1815
- ...row.release_began_at === null ? {} : { releaseBeganAt: row.release_began_at },
1816
- ...row.released_at === null ? {} : { releasedAt: row.released_at }
1817
- }).pipe(Effect.mapError(rowFailure));
1818
- });
1819
- const readApprovalDecisions = Effect.fn("SqliteSubmissionLedger.readApprovalDecisions")(function* (operation, submissionId) {
1820
- const rows = yield* sql`
1821
- SELECT
1822
- submission_id,
1823
- tool_call_id,
1824
- decision,
1825
- resolver,
1826
- reason,
1827
- decided_at
1828
- FROM effect_agent_approval_decisions
1829
- WHERE submission_id = ${submissionId}
1830
- ORDER BY tool_call_id ASC
1831
- `.pipe(Effect.mapError(sqlFailure(operation)));
1832
- return yield* decodeRows(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1833
- });
1834
- const approvalIntentFromRow = Effect.fn("SqliteSubmissionLedger.approvalIntentFromRow")(function* (operation, row) {
1835
- return yield* decodeApprovalDecisionIntent({
1836
- submissionId: row.submission_id,
1837
- toolCallId: row.tool_call_id,
1838
- decision: row.decision,
1839
- resolver: row.resolver,
1840
- reason: row.reason,
1841
- decidedAt: row.decided_at
1842
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_approval_decisions", `${row.submission_id}/${row.tool_call_id}`, error.message)));
1843
- });
1844
- const readUnknownResolutions = Effect.fn("SqliteSubmissionLedger.readUnknownResolutions")(function* (operation, submissionId) {
1845
- const rows = yield* sql`
1846
- SELECT
1847
- submission_id,
1848
- tool_call_id,
1849
- author,
1850
- reason,
1851
- resolution_json,
1852
- resolved_at
1853
- FROM effect_agent_unknown_resolutions
1854
- WHERE submission_id = ${submissionId}
1855
- ORDER BY tool_call_id ASC
1856
- `.pipe(Effect.mapError(sqlFailure(operation)));
1857
- return yield* decodeRows(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1858
- });
1859
- const unknownResolutionIntentFromRow = Effect.fn("SqliteSubmissionLedger.unknownResolutionIntentFromRow")(function* (operation, row) {
1860
- const resolution = yield* parseStoredJsonText(row.resolution_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_unknown_resolutions", `${row.submission_id}/${row.tool_call_id}`, error.message)));
1861
- return yield* decodeUnknownResolutionIntent({
1862
- submissionId: row.submission_id,
1863
- toolCallId: row.tool_call_id,
1864
- author: row.author,
1865
- reason: row.reason,
1866
- resolution,
1867
- resolvedAt: row.resolved_at
1868
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_unknown_resolutions", `${row.submission_id}/${row.tool_call_id}`, error.message)));
1869
- });
1870
- /** The Submission's marked-unknown open Tool Call identities, empty when never marked. */
1871
- const storedUnknownToolCallIds = Effect.fn("SqliteSubmissionLedger.storedUnknownToolCallIds")(function* (operation, submission) {
1872
- if (submission.unknown_tool_call_ids_json === null) return [];
1873
- return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", submission.submission_id, error.message)));
1874
- });
1875
- /**
1876
- * Canonical history is the abort authority (DUR-015): the intent's canonicalRecordId is
1877
- * derived from the shared canonical-records table using the deterministic abort record
1878
- * identity, never from a cached ledger marker.
1879
- */
1880
- const canonicalAbortRecordId = Effect.fn("SqliteSubmissionLedger.canonicalAbortRecordId")(function* (operation, conversationId, submissionId) {
1881
- const recordId = submissionAbortRecordId(submissionId);
1882
- const rows = yield* sql`
1883
- SELECT record_id
1884
- FROM effect_agent_canonical_records
1885
- WHERE conversation_id = ${conversationId}
1886
- AND record_id = ${recordId}
1887
- `.pipe(Effect.mapError(sqlFailure(operation)));
1888
- return (yield* decodeRows(Schema.Array(CanonicalRecordIdRow), "effect_agent_canonical_records", `${conversationId}/${recordId}`, rows).pipe(Effect.mapError(internalFailure(operation)))).length === 0 ? void 0 : recordId;
1889
- });
1890
- const abortIntentFromRow = Effect.fn("SqliteSubmissionLedger.abortIntentFromRow")(function* (operation, submission, submissionId, row) {
1891
- const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.conversation_id, submissionId);
1892
- return yield* decodeAbortIntent({
1893
- submissionId: row.submission_id,
1894
- author: row.author,
1895
- reason: row.reason,
1896
- requestedAt: row.requested_at,
1897
- ...canonicalRecordId === void 0 ? {} : { canonicalRecordId }
1898
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_abort_intents", row.submission_id, error.message)));
1899
- });
1900
- const capabilities = Effect.succeed(LedgerCapabilities.make({ durability: "durable-node" }));
1901
- const admit = Effect.fn("SqliteSubmissionLedger.admit")(function* (request) {
1902
- const operation = "ledger admit";
1903
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AdmissionRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
1904
- const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(Effect.mapError(internalFailure(operation)));
1905
- const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(Effect.mapError(internalFailure(operation)));
1906
- const mintedSubmissionId = yield* mintIdentifier("submission", operation);
1907
- const mintedReceiptId = yield* mintIdentifier("receipt", operation);
1908
- yield* hitFailpoint("ledger:admit:before", operation);
1909
- const result = yield* inWriteTransaction(operation, Effect.gen(function* () {
1910
- const keyRowKey = `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`;
1911
- const existingRows = yield* sql`
1912
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1913
- FROM effect_agent_submissions
1914
- WHERE conversation_id = ${validated.conversationId}
1915
- AND principal = ${validated.principal}
1916
- AND idempotency_key = ${validated.idempotencyKey}
1917
- `.pipe(Effect.mapError(sqlFailure(operation)));
1918
- const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
1919
- if (existing.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", keyRowKey, "An admission idempotency key returned more than one row.");
1920
- if (existing.length === 1) {
1921
- const sameLinkage = validated.parentLinkage === void 0 ? existing[0].parent_submission_id === null && existing[0].parent_tool_call_id === null : existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId && existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
1922
- if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) return yield* AdmissionConflict.make({
1923
- conversationId: validated.conversationId,
1924
- principal: validated.principal,
1925
- idempotencyKey: validated.idempotencyKey,
1926
- existingInputDigest: existing[0].input_digest,
1927
- attemptedInputDigest: validated.inputDigest
1928
- });
1929
- return yield* decodeAdmissionResult({
1930
- submissionId: existing[0].submission_id,
1931
- receiptId: existing[0].receipt_id,
1932
- queueSequence: existing[0].queue_sequence,
1933
- state: existing[0].state,
1934
- replayed: true
1935
- }).pipe(Effect.mapError(internalFailure(operation)));
1936
- }
1937
- const maxRows = yield* sql`
1938
- SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
1939
- FROM effect_agent_submissions
1940
- WHERE conversation_id = ${validated.conversationId}
1941
- `.pipe(Effect.mapError(sqlFailure(operation)));
1942
- const decodedMax = yield* decodeRows(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.conversationId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
1943
- const queueSequence = yield* decodeQueueSequence((decodedMax[0]?.max_queue_sequence ?? 0) + 1).pipe(Effect.mapError(internalFailure(operation)));
1944
- const now = yield* currentInstant;
1945
- yield* sql`
1946
- INSERT INTO effect_agent_submissions (
1947
- submission_id,
1948
- conversation_id,
1949
- queue_sequence,
1950
- principal,
1951
- idempotency_key,
1952
- agent_id,
1953
- agent_digests_json,
1954
- deployment_id,
1955
- input_json,
1956
- input_digest,
1957
- receipt_id,
1958
- state,
1959
- created_at,
1960
- parent_submission_id,
1961
- parent_tool_call_id
1962
- ) VALUES (
1963
- ${mintedSubmissionId},
1964
- ${validated.conversationId},
1965
- ${queueSequence},
1966
- ${validated.principal},
1967
- ${validated.idempotencyKey},
1968
- ${validated.agentId},
1969
- ${agentDigestsJson},
1970
- ${validated.deploymentId},
1971
- ${inputJson},
1972
- ${validated.inputDigest},
1973
- ${mintedReceiptId},
1974
- 'admitted',
1975
- ${now.iso},
1976
- ${validated.parentLinkage?.parentSubmissionId ?? null},
1977
- ${validated.parentLinkage?.parentToolCallId ?? null}
1978
- )
1979
- `.pipe(Effect.mapError(sqlFailure(operation)));
1980
- return yield* decodeAdmissionResult({
1981
- submissionId: mintedSubmissionId,
1982
- receiptId: mintedReceiptId,
1983
- queueSequence,
1984
- state: "admitted",
1985
- replayed: false
1986
- }).pipe(Effect.mapError(internalFailure(operation)));
1987
- }));
1988
- yield* hitFailpoint("ledger:admit:after", operation);
1989
- return result;
1990
- });
1991
- const markReady = Effect.fn("SqliteSubmissionLedger.markReady")(function* (request) {
1992
- const operation = "ledger mark ready";
1993
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkReadyRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
1994
- yield* hitFailpoint("ledger:mark-ready:before", operation);
1995
- yield* inWriteTransaction(operation, Effect.gen(function* () {
1996
- if ((yield* requireSubmission(operation, validated.submissionId)).state !== "admitted") return;
1997
- const now = yield* currentInstant;
1998
- yield* sql`
1999
- UPDATE effect_agent_submissions
2000
- SET state = 'ready', ready_at = ${now.iso}
2001
- WHERE submission_id = ${validated.submissionId}
2002
- `.pipe(Effect.mapError(sqlFailure(operation)));
2003
- }));
2004
- yield* hitFailpoint("ledger:mark-ready:after", operation);
2005
- });
2006
- const lookup = Effect.fn("SqliteSubmissionLedger.lookup")(function* (request) {
2007
- const operation = "ledger lookup";
2008
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookup))(request).pipe(Effect.mapError(internalFailure(operation)));
2009
- if (validated._tag === "SubmissionLookupById") {
2010
- const row = yield* readSubmission(operation, validated.submissionId);
2011
- if (Option.isNone(row)) return Option.none();
2012
- return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
2013
- }
2014
- const rows = yield* sql`
2015
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2016
- FROM effect_agent_submissions
2017
- WHERE conversation_id = ${validated.conversationId}
2018
- AND principal = ${validated.principal}
2019
- AND idempotency_key = ${validated.idempotencyKey}
2020
- `.pipe(Effect.mapError(sqlFailure(operation)));
2021
- const decoded = yield* decodeSubmissionRows(operation, `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2022
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, "An admission idempotency key returned more than one row.");
2023
- if (decoded.length === 0) return Option.none();
2024
- return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
2025
- });
2026
- const resolveAdmission = Effect.fn("SqliteSubmissionLedger.resolveAdmission")(function* (request) {
2027
- const operation = "ledger resolve admission";
2028
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookupByKey))(request).pipe(Effect.mapError(internalFailure(operation)));
2029
- const rows = yield* sql`
2030
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2031
- FROM effect_agent_submissions
2032
- WHERE conversation_id = ${validated.conversationId}
2033
- AND principal = ${validated.principal}
2034
- AND idempotency_key = ${validated.idempotencyKey}
2035
- `.pipe(Effect.mapError(sqlFailure(operation)));
2036
- const decoded = yield* decodeSubmissionRows(operation, `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2037
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, "An admission idempotency key returned more than one row.");
2038
- if (decoded.length === 0) return AdmissionNotAdmitted.make();
2039
- return AdmissionAdmitted.make({ submission: yield* decodeSubmissionSnapshot(operation, decoded[0]) });
2040
- });
2041
- const claim = Effect.fn("SqliteSubmissionLedger.claim")(function* (request) {
2042
- const operation = "ledger claim";
2043
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2044
- const attemptId = yield* mintIdentifier("attempt", operation);
2045
- const ownershipToken = yield* mintIdentifier("owner", operation);
2046
- yield* hitFailpoint("ledger:claim:before", operation);
2047
- const claimed = yield* inWriteTransaction(operation, Effect.gen(function* () {
2048
- const headRows = yield* sql`
2049
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2050
- FROM effect_agent_submissions
2051
- WHERE conversation_id = ${validated.conversationId}
2052
- AND state <> 'settled'
2053
- ORDER BY queue_sequence ASC
2054
- LIMIT 1
2055
- `.pipe(Effect.mapError(sqlFailure(operation)));
2056
- const heads = yield* decodeSubmissionRows(operation, validated.conversationId, headRows);
2057
- if (heads.length === 0) return Option.none();
2058
- const head = heads[0];
2059
- if (head.state === "joining" || head.state === "joined" || head.state === "suspended" || head.state === "unknown") return Option.none();
2060
- const now = yield* currentInstant;
2061
- const ownership = yield* readOwnership(operation, head.submission_id);
2062
- if (Option.isSome(ownership)) {
2063
- if ((yield* timestampMillis(operation, head.submission_id)(ownership.value.lease_expires_at)) > now.millis) return Option.none();
2064
- }
2065
- const conversations = yield* journal.getConversation(head.conversation_id).pipe(Effect.mapError(internalFailure(operation)));
2066
- let producerEpoch;
2067
- if (conversations.length === 0) {
2068
- producerEpoch = 1;
2069
- yield* sql`
2070
- INSERT INTO effect_agent_conversations (
2071
- conversation_id,
2072
- created_at,
2073
- tail_sequence,
2074
- tail_digest,
2075
- producer_epoch
2076
- ) VALUES (
2077
- ${head.conversation_id},
2078
- ${now.iso},
2079
- 0,
2080
- ${EMPTY_TAIL_DIGEST},
2081
- ${producerEpoch}
2082
- )
2083
- `.pipe(Effect.mapError(sqlFailure(operation)));
2084
- } else {
2085
- producerEpoch = conversations[0].producer_epoch + 1;
2086
- yield* sql`
2087
- UPDATE effect_agent_conversations
2088
- SET producer_epoch = ${producerEpoch}
2089
- WHERE conversation_id = ${head.conversation_id}
2090
- `.pipe(Effect.mapError(sqlFailure(operation)));
2091
- }
2092
- const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
2093
- yield* sql`
2094
- INSERT INTO effect_agent_submission_ownership (
2095
- submission_id,
2096
- attempt_id,
2097
- ownership_token,
2098
- producer_epoch,
2099
- owner_producer_id,
2100
- lease_expires_at
2101
- ) VALUES (
2102
- ${head.submission_id},
2103
- ${attemptId},
2104
- ${ownershipToken},
2105
- ${producerEpoch},
2106
- ${validated.producerId},
2107
- ${leaseExpiresAt}
2108
- )
2109
- ON CONFLICT (submission_id) DO UPDATE SET
2110
- attempt_id = excluded.attempt_id,
2111
- ownership_token = excluded.ownership_token,
2112
- producer_epoch = excluded.producer_epoch,
2113
- owner_producer_id = excluded.owner_producer_id,
2114
- lease_expires_at = excluded.lease_expires_at
2115
- `.pipe(Effect.mapError(sqlFailure(operation)));
2116
- yield* sql`
2117
- INSERT INTO effect_agent_attempts (
2118
- attempt_id,
2119
- submission_id,
2120
- conversation_id,
2121
- owner_producer_id,
2122
- producer_epoch,
2123
- claimed_at
2124
- ) VALUES (
2125
- ${attemptId},
2126
- ${head.submission_id},
2127
- ${head.conversation_id},
2128
- ${validated.producerId},
2129
- ${producerEpoch},
2130
- ${now.iso}
2131
- )
2132
- `.pipe(Effect.mapError(sqlFailure(operation)));
2133
- if (head.state === "ready") yield* sql`
2134
- UPDATE effect_agent_submissions
2135
- SET state = 'running'
2136
- WHERE submission_id = ${head.submission_id}
2137
- `.pipe(Effect.mapError(sqlFailure(operation)));
2138
- const inputPayload = yield* parseStoredJsonText(head.input_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", head.submission_id, error.message)));
2139
- return Option.some(yield* decodeClaim({
2140
- submissionId: head.submission_id,
2141
- attemptId,
2142
- ownershipToken,
2143
- producerEpoch,
2144
- leaseExpiresAt,
2145
- inputPayload
2146
- }).pipe(Effect.mapError(internalFailure(operation))));
2147
- }));
2148
- yield* hitFailpoint("ledger:claim:after", operation);
2149
- return claimed;
2150
- });
2151
- const renewOwnership = Effect.fn("SqliteSubmissionLedger.renewOwnership")(function* (request) {
2152
- const operation = "ledger renew ownership";
2153
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RenewOwnershipRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2154
- yield* hitFailpoint("ledger:renew:before", operation);
2155
- const renewal = yield* inWriteTransaction(operation, Effect.gen(function* () {
2156
- const submission = yield* requireSubmission(operation, validated.submissionId);
2157
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2158
- const now = yield* currentInstant;
2159
- const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
2160
- yield* sql`
2161
- UPDATE effect_agent_submission_ownership
2162
- SET lease_expires_at = ${leaseExpiresAt}
2163
- WHERE submission_id = ${validated.submissionId}
2164
- `.pipe(Effect.mapError(sqlFailure(operation)));
2165
- return yield* decodeOwnershipRenewal({
2166
- ownershipToken: validated.ownershipToken,
2167
- leaseExpiresAt
2168
- }).pipe(Effect.mapError(internalFailure(operation)));
2169
- }));
2170
- yield* hitFailpoint("ledger:renew:after", operation);
2171
- return renewal;
2172
- });
2173
- const releaseOwnership = Effect.fn("SqliteSubmissionLedger.releaseOwnership")(function* (request) {
2174
- const operation = "ledger release ownership";
2175
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseOwnershipRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2176
- yield* hitFailpoint("ledger:release:before", operation);
2177
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2178
- const submission = yield* requireSubmission(operation, validated.submissionId);
2179
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2180
- yield* sql`
2181
- DELETE FROM effect_agent_submission_ownership
2182
- WHERE submission_id = ${validated.submissionId}
2183
- `.pipe(Effect.mapError(sqlFailure(operation)));
2184
- if (submission.state === "running") yield* sql`
2185
- UPDATE effect_agent_submissions
2186
- SET state = 'ready'
2187
- WHERE submission_id = ${validated.submissionId}
2188
- `.pipe(Effect.mapError(sqlFailure(operation)));
2189
- }));
2190
- yield* hitFailpoint("ledger:release:after", operation);
2191
- });
2192
- const markInputApplied = Effect.fn("SqliteSubmissionLedger.markInputApplied")(function* (request) {
2193
- const operation = "ledger mark input applied";
2194
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkInputAppliedRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2195
- yield* hitFailpoint("ledger:mark-input-applied:before", operation);
2196
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2197
- const submission = yield* requireSubmission(operation, validated.submissionId);
2198
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2199
- if (submission.input_applied_record_id !== null) {
2200
- if (submission.input_applied_record_id === validated.recordId && submission.input_applied_sequence === validated.sequence) return;
2201
- return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A different canonical input marker is already recorded for this Submission.");
2202
- }
2203
- yield* sql`
2204
- UPDATE effect_agent_submissions
2205
- SET
2206
- input_applied_record_id = ${validated.recordId},
2207
- input_applied_sequence = ${validated.sequence},
2208
- state = CASE
2209
- WHEN state IN ('admitted', 'ready', 'running') THEN 'input-applied'
2210
- ELSE state
2211
- END
2212
- WHERE submission_id = ${validated.submissionId}
2213
- `.pipe(Effect.mapError(sqlFailure(operation)));
2214
- }));
2215
- yield* hitFailpoint("ledger:mark-input-applied:after", operation);
2216
- });
2217
- const reserveSettlement = Effect.fn("SqliteSubmissionLedger.reserveSettlement")(function* (request) {
2218
- const operation = "ledger reserve settlement";
2219
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementReservation))(request).pipe(Effect.mapError(internalFailure(operation)));
2220
- const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(Effect.mapError(internalFailure(operation)));
2221
- yield* hitFailpoint("ledger:reserve-settlement:before", operation);
2222
- const reserved = yield* inWriteTransaction(operation, Effect.gen(function* () {
2223
- const existing = yield* readReservation(operation, validated.submissionId);
2224
- if (Option.isSome(existing)) {
2225
- if (!(existing.value.settlement_id === validated.settlementId && existing.value.outcome === validated.outcome && existing.value.record_digest === validated.recordDigest && existing.value.record_json === recordJson)) return yield* SettlementConflict.make({
2226
- submissionId: validated.submissionId,
2227
- existingOutcome: existing.value.outcome
2228
- });
2229
- const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, error.message)));
2230
- return ReservedSettlement.make({
2231
- submissionId: validated.submissionId,
2232
- settlementId: validated.settlementId,
2233
- outcome: validated.outcome,
2234
- record,
2235
- recordDigest: validated.recordDigest,
2236
- replayed: true
2237
- });
2238
- }
2239
- const submission = yield* requireSubmission(operation, validated.submissionId);
2240
- if (submission.state === "settled") {
2241
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2242
- return yield* SettlementConflict.make({
2243
- submissionId: validated.submissionId,
2244
- existingOutcome: submission.settled_outcome
2245
- });
2246
- }
2247
- if (!(submission.state === "joined" && submission.joined_host_submission_id !== null)) {
2248
- let queuedAbortSettlement = false;
2249
- if (validated.outcome === "aborted" && (submission.state === "ready" || submission.state === "terminalizing")) {
2250
- const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
2251
- if (Option.isSome(abortIntent)) {
2252
- const ownership = yield* readOwnership(operation, validated.submissionId);
2253
- queuedAbortSettlement = Option.isNone(ownership);
2254
- }
2255
- }
2256
- if (!queuedAbortSettlement) yield* requireOwnership(operation, submission, validated.ownershipToken);
2257
- }
2258
- const now = yield* currentInstant;
2259
- yield* sql`
2260
- INSERT INTO effect_agent_settlement_reservations (
2261
- submission_id,
2262
- settlement_id,
2263
- outcome,
2264
- record_id,
2265
- record_json,
2266
- record_digest,
2267
- reserved_at
2268
- ) VALUES (
2269
- ${validated.submissionId},
2270
- ${validated.settlementId},
2271
- ${validated.outcome},
2272
- ${validated.record.recordId},
2273
- ${recordJson},
2274
- ${validated.recordDigest},
2275
- ${now.iso}
2276
- )
2277
- `.pipe(Effect.mapError(sqlFailure(operation)));
2278
- yield* sql`
2279
- UPDATE effect_agent_submissions
2280
- SET state = 'terminalizing'
2281
- WHERE submission_id = ${validated.submissionId}
2282
- `.pipe(Effect.mapError(sqlFailure(operation)));
2283
- return ReservedSettlement.make({
2284
- submissionId: validated.submissionId,
2285
- settlementId: validated.settlementId,
2286
- outcome: validated.outcome,
2287
- record: validated.record,
2288
- recordDigest: validated.recordDigest,
2289
- replayed: false
2290
- });
2291
- }));
2292
- yield* hitFailpoint("ledger:reserve-settlement:after", operation);
2293
- return reserved;
2294
- });
2295
- const finalizeSettlement = Effect.fn("SqliteSubmissionLedger.finalizeSettlement")(function* (request) {
2296
- const operation = "ledger finalize settlement";
2297
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementFinalization))(request).pipe(Effect.mapError(internalFailure(operation)));
2298
- yield* hitFailpoint("ledger:finalize-settlement:before", operation);
2299
- const settlement = yield* inWriteTransaction(operation, Effect.gen(function* () {
2300
- const reservation = yield* readReservation(operation, validated.submissionId);
2301
- if (Option.isNone(reservation)) return yield* LedgerError.make({
2302
- operation,
2303
- message: `No settlement reservation exists for submission ${validated.submissionId}.`
2304
- });
2305
- if (reservation.value.settlement_id !== validated.settlementId) return yield* SettlementConflict.make({
2306
- submissionId: validated.submissionId,
2307
- existingOutcome: reservation.value.outcome
2308
- });
2309
- const submission = yield* requireSubmission(operation, validated.submissionId);
2310
- if (submission.state === "settled") {
2311
- if (reservation.value.finalized_at === null) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, "A settled Submission's reservation carries no finalization timestamp.");
2312
- return yield* decodeSettlement({
2313
- submissionId: validated.submissionId,
2314
- settlementId: validated.settlementId,
2315
- receiptId: submission.receipt_id,
2316
- outcome: reservation.value.outcome,
2317
- settledAt: reservation.value.finalized_at
2318
- }).pipe(Effect.mapError(internalFailure(operation)));
2319
- }
2320
- const now = yield* currentInstant;
2321
- yield* sql`
2322
- UPDATE effect_agent_submissions
2323
- SET state = 'settled', settled_outcome = ${reservation.value.outcome}
2324
- WHERE submission_id = ${validated.submissionId}
2325
- `.pipe(Effect.mapError(sqlFailure(operation)));
2326
- yield* sql`
2327
- UPDATE effect_agent_settlement_reservations
2328
- SET finalized_at = ${now.iso}
2329
- WHERE submission_id = ${validated.submissionId}
2330
- `.pipe(Effect.mapError(sqlFailure(operation)));
2331
- yield* sql`
2332
- DELETE FROM effect_agent_submission_ownership
2333
- WHERE submission_id = ${validated.submissionId}
2334
- `.pipe(Effect.mapError(sqlFailure(operation)));
2335
- return yield* decodeSettlement({
2336
- submissionId: validated.submissionId,
2337
- settlementId: validated.settlementId,
2338
- receiptId: submission.receipt_id,
2339
- outcome: reservation.value.outcome,
2340
- settledAt: now.iso
2341
- }).pipe(Effect.mapError(internalFailure(operation)));
2342
- }));
2343
- yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2344
- return settlement;
2345
- });
2346
- const requestAbort = Effect.fn("SqliteSubmissionLedger.requestAbort")(function* (request) {
2347
- const operation = "ledger request abort";
2348
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AbortCommand))(request).pipe(Effect.mapError(internalFailure(operation)));
2349
- yield* hitFailpoint("ledger:request-abort:before", operation);
2350
- const intent = yield* inWriteTransaction(operation, Effect.gen(function* () {
2351
- const submission = yield* requireSubmission(operation, validated.submissionId);
2352
- if (submission.state === "settled") {
2353
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2354
- return yield* SettlementConflict.make({
2355
- submissionId: validated.submissionId,
2356
- existingOutcome: submission.settled_outcome
2357
- });
2358
- }
2359
- if (submission.state === "joined") {
2360
- if (submission.joined_host_submission_id === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A joined Submission carries no host linkage.");
2361
- const hostSubmissionId = yield* decodeSubmissionId(submission.joined_host_submission_id).pipe(Effect.mapError(internalFailure(operation)));
2362
- return yield* JoinedToHost.make({
2363
- submissionId: validated.submissionId,
2364
- hostSubmissionId
2365
- });
2366
- }
2367
- const existing = yield* readAbortIntent(operation, validated.submissionId);
2368
- if (Option.isSome(existing)) return yield* abortIntentFromRow(operation, submission, validated.submissionId, existing.value);
2369
- const now = yield* currentInstant;
2370
- yield* sql`
2371
- INSERT INTO effect_agent_abort_intents (
2372
- submission_id,
2373
- author,
2374
- reason,
2375
- requested_at
2376
- ) VALUES (
2377
- ${validated.submissionId},
2378
- ${validated.author},
2379
- ${validated.reason},
2380
- ${now.iso}
2381
- )
2382
- `.pipe(Effect.mapError(sqlFailure(operation)));
2383
- const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.conversation_id, validated.submissionId);
2384
- return yield* decodeAbortIntent({
2385
- submissionId: validated.submissionId,
2386
- author: validated.author,
2387
- reason: validated.reason,
2388
- requestedAt: now.iso,
2389
- ...canonicalRecordId === void 0 ? {} : { canonicalRecordId }
2390
- }).pipe(Effect.mapError(internalFailure(operation)));
2391
- }));
2392
- yield* hitFailpoint("ledger:request-abort:after", operation);
2393
- return intent;
2394
- });
2395
- const claimJoining = Effect.fn("SqliteSubmissionLedger.claimJoining")(function* (request) {
2396
- const operation = "ledger claim joining";
2397
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2398
- yield* hitFailpoint("ledger:claim-joining:before", operation);
2399
- const claims = yield* inWriteTransaction(operation, Effect.gen(function* () {
2400
- const host = yield* requireSubmission(operation, validated.hostSubmissionId);
2401
- if (host.conversation_id !== validated.conversationId) return yield* LedgerError.make({
2402
- operation,
2403
- message: `Host submission ${validated.hostSubmissionId} does not belong to conversation ${validated.conversationId}.`
2404
- });
2405
- yield* requireOwnership(operation, host, validated.ownershipToken);
2406
- const laterRows = yield* sql`
2407
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2408
- FROM effect_agent_submissions
2409
- WHERE conversation_id = ${validated.conversationId}
2410
- AND queue_sequence > ${host.queue_sequence}
2411
- ORDER BY queue_sequence ASC
2412
- `.pipe(Effect.mapError(sqlFailure(operation)));
2413
- const later = yield* decodeSubmissionRows(operation, validated.conversationId, laterRows);
2414
- const claimed = [];
2415
- for (const row of later) {
2416
- if (claimed.length >= validated.maxCount) break;
2417
- if ((row.state === "joining" || row.state === "joined") && row.joined_host_submission_id === validated.hostSubmissionId) continue;
2418
- if (row.state === "settled" && row.settled_outcome === "aborted") continue;
2419
- if (row.state !== "ready") break;
2420
- yield* sql`
2421
- UPDATE effect_agent_submissions
2422
- SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
2423
- WHERE submission_id = ${row.submission_id}
2424
- `.pipe(Effect.mapError(sqlFailure(operation)));
2425
- const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
2426
- claimed.push(yield* decodeJoiningClaim({
2427
- submissionId: row.submission_id,
2428
- queueSequence: row.queue_sequence,
2429
- inputPayload
2430
- }).pipe(Effect.mapError(internalFailure(operation))));
2431
- }
2432
- return claimed;
2433
- }));
2434
- yield* hitFailpoint("ledger:claim-joining:after", operation);
2435
- return claims;
2436
- });
2437
- const markJoined = Effect.fn("SqliteSubmissionLedger.markJoined")(function* (request) {
2438
- const operation = "ledger mark joined";
2439
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkJoinedRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2440
- yield* hitFailpoint("ledger:mark-joined:before", operation);
2441
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2442
- const submission = yield* requireSubmission(operation, validated.submissionId);
2443
- if (submission.joined_host_submission_id === null) return yield* LedgerError.make({
2444
- operation,
2445
- message: `Submission ${validated.submissionId} was never claimed for joining.`
2446
- });
2447
- const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
2448
- yield* requireOwnership(operation, host, validated.ownershipToken);
2449
- if (submission.input_applied_record_id !== null) {
2450
- if (submission.input_applied_record_id === validated.recordId && submission.input_applied_sequence === validated.sequence) return;
2451
- return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A different join marker is already recorded for this Submission.");
2452
- }
2453
- if (submission.state !== "joining" && submission.state !== "joined") return yield* LedgerError.make({
2454
- operation,
2455
- message: `Cannot mark submission ${validated.submissionId} joined from state ${submission.state}.`
2456
- });
2457
- yield* sql`
2458
- UPDATE effect_agent_submissions
2459
- SET
2460
- input_applied_record_id = ${validated.recordId},
2461
- input_applied_sequence = ${validated.sequence},
2462
- state = 'joined'
2463
- WHERE submission_id = ${validated.submissionId}
2464
- `.pipe(Effect.mapError(sqlFailure(operation)));
2465
- }));
2466
- yield* hitFailpoint("ledger:mark-joined:after", operation);
2467
- });
2468
- const revertJoining = Effect.fn("SqliteSubmissionLedger.revertJoining")(function* (request) {
2469
- const operation = "ledger revert joining";
2470
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RevertJoiningRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2471
- yield* hitFailpoint("ledger:revert-joining:before", operation);
2472
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2473
- if ((yield* requireSubmission(operation, validated.submissionId)).state !== "joining") return;
2474
- yield* sql`
2475
- UPDATE effect_agent_submissions
2476
- SET state = 'ready', joined_host_submission_id = NULL
2477
- WHERE submission_id = ${validated.submissionId}
2478
- `.pipe(Effect.mapError(sqlFailure(operation)));
2479
- }));
2480
- yield* hitFailpoint("ledger:revert-joining:after", operation);
2481
- });
2482
- const suspend = Effect.fn("SqliteSubmissionLedger.suspend")(function* (request) {
2483
- const operation = "ledger suspend";
2484
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SuspendRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2485
- const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(Effect.mapError(internalFailure(operation)));
2486
- yield* hitFailpoint("ledger:suspend:before", operation);
2487
- const outcome = yield* inWriteTransaction(operation, Effect.gen(function* () {
2488
- const submission = yield* requireSubmission(operation, validated.submissionId);
2489
- if (submission.state === "settled") {
2490
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2491
- return yield* SettlementConflict.make({
2492
- submissionId: validated.submissionId,
2493
- existingOutcome: submission.settled_outcome
2494
- });
2495
- }
2496
- const reservation = yield* readReservation(operation, validated.submissionId);
2497
- if (Option.isSome(reservation)) return yield* SettlementConflict.make({
2498
- submissionId: validated.submissionId,
2499
- existingOutcome: reservation.value.outcome
2500
- });
2501
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2502
- if (validated.reason._tag === "ApprovalPending") {
2503
- const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
2504
- const decided = new Set(decisions.map((row) => row.tool_call_id));
2505
- if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return "resume-immediately";
2506
- } else {
2507
- let allSettled = true;
2508
- for (const child of validated.reason.children) {
2509
- const childRow = yield* readSubmission(operation, child.childSubmissionId);
2510
- if (Option.isNone(childRow) || childRow.value.state !== "settled") {
2511
- allSettled = false;
2512
- break;
2513
- }
2514
- }
2515
- if (allSettled) return "resume-immediately";
2516
- }
2517
- const now = yield* currentInstant;
2518
- yield* sql`
2519
- UPDATE effect_agent_submissions
2520
- SET
2521
- state = 'suspended',
2522
- suspended_reason_json = ${reasonJson},
2523
- suspended_at = ${now.iso}
2524
- WHERE submission_id = ${validated.submissionId}
2525
- `.pipe(Effect.mapError(sqlFailure(operation)));
2526
- yield* sql`
2527
- DELETE FROM effect_agent_submission_ownership
2528
- WHERE submission_id = ${validated.submissionId}
2529
- `.pipe(Effect.mapError(sqlFailure(operation)));
2530
- return "suspended";
2531
- }));
2532
- yield* hitFailpoint("ledger:suspend:after", operation);
2533
- return outcome;
2534
- });
2535
- /**
2536
- * Once every pending call of a recorded ApprovalPending suspension has a decision intent,
2537
- * the lane wakes: suspended → input-applied, suspension cleared (plan §2.6). A
2538
- * WaitingForChild suspension wakes only through recordChildSettled. Runs inside the caller's
2539
- * write transaction.
2540
- */
2541
- const wakeSuspendedIfCovered = Effect.fn("SqliteSubmissionLedger.wakeSuspendedIfCovered")(function* (operation, submission) {
2542
- if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
2543
- const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(submission.suspended_reason_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", submission.submission_id, error.message)));
2544
- if (reason._tag !== "ApprovalPending") return;
2545
- const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
2546
- const decided = new Set(decisions.map((row) => row.tool_call_id));
2547
- if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
2548
- yield* sql`
2549
- UPDATE effect_agent_submissions
2550
- SET
2551
- state = 'input-applied',
2552
- suspended_reason_json = NULL,
2553
- suspended_at = NULL
2554
- WHERE submission_id = ${submission.submission_id}
2555
- `.pipe(Effect.mapError(sqlFailure(operation)));
2556
- });
2557
- const recordApprovalDecision = Effect.fn("SqliteSubmissionLedger.recordApprovalDecision")(function* (command) {
2558
- const operation = "ledger record approval decision";
2559
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ApprovalDecisionCommand))(command).pipe(Effect.mapError(internalFailure(operation)));
2560
- yield* hitFailpoint("ledger:approval-decision:before", operation);
2561
- const intent = yield* inWriteTransaction(operation, Effect.gen(function* () {
2562
- const submission = yield* requireSubmission(operation, validated.submissionId);
2563
- if (submission.state === "settled") {
2564
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2565
- return yield* SettlementConflict.make({
2566
- submissionId: validated.submissionId,
2567
- existingOutcome: submission.settled_outcome
2568
- });
2569
- }
2570
- const existing = (yield* readApprovalDecisions(operation, validated.submissionId)).find((row) => row.tool_call_id === validated.toolCallId);
2571
- if (existing !== void 0) {
2572
- if (existing.decision !== validated.decision) return yield* ApprovalConflict.make({
2573
- submissionId: validated.submissionId,
2574
- toolCallId: validated.toolCallId,
2575
- existingDecision: existing.decision
2576
- });
2577
- return yield* approvalIntentFromRow(operation, existing);
2578
- }
2579
- const now = yield* currentInstant;
2580
- yield* sql`
2581
- INSERT INTO effect_agent_approval_decisions (
2582
- submission_id,
2583
- tool_call_id,
2584
- decision,
2585
- resolver,
2586
- reason,
2587
- decided_at
2588
- ) VALUES (
2589
- ${validated.submissionId},
2590
- ${validated.toolCallId},
2591
- ${validated.decision},
2592
- ${validated.resolver},
2593
- ${validated.reason},
2594
- ${now.iso}
2595
- )
2596
- `.pipe(Effect.mapError(sqlFailure(operation)));
2597
- yield* wakeSuspendedIfCovered(operation, submission);
2598
- return yield* decodeApprovalDecisionIntent({
2599
- submissionId: validated.submissionId,
2600
- toolCallId: validated.toolCallId,
2601
- decision: validated.decision,
2602
- resolver: validated.resolver,
2603
- reason: validated.reason,
2604
- decidedAt: now.iso
2605
- }).pipe(Effect.mapError(internalFailure(operation)));
2606
- }));
2607
- yield* hitFailpoint("ledger:approval-decision:after", operation);
2608
- return intent;
2609
- });
2610
- const markUnknown = Effect.fn("SqliteSubmissionLedger.markUnknown")(function* (request) {
2611
- const operation = "ledger mark unknown";
2612
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkUnknownRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2613
- yield* hitFailpoint("ledger:mark-unknown:before", operation);
2614
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2615
- const submission = yield* requireSubmission(operation, validated.submissionId);
2616
- if (submission.state === "settled") {
2617
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2618
- return yield* SettlementConflict.make({
2619
- submissionId: validated.submissionId,
2620
- existingOutcome: submission.settled_outcome
2621
- });
2622
- }
2623
- const reservation = yield* readReservation(operation, validated.submissionId);
2624
- if (Option.isSome(reservation)) return yield* SettlementConflict.make({
2625
- submissionId: validated.submissionId,
2626
- existingOutcome: reservation.value.outcome
2627
- });
2628
- const existingIds = yield* storedUnknownToolCallIds(operation, submission);
2629
- const known = new Set(existingIds);
2630
- const merged = [...existingIds, ...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId))];
2631
- const idsJson = yield* encodeToolCallIdsText(merged).pipe(Effect.mapError(internalFailure(operation)));
2632
- yield* sql`
2633
- UPDATE effect_agent_submissions
2634
- SET
2635
- state = 'unknown',
2636
- unknown_reason = ${submission.unknown_reason ?? validated.reason},
2637
- unknown_tool_call_ids_json = ${idsJson}
2638
- WHERE submission_id = ${validated.submissionId}
2639
- `.pipe(Effect.mapError(sqlFailure(operation)));
2640
- }));
2641
- yield* hitFailpoint("ledger:mark-unknown:after", operation);
2642
- });
2643
- const recordUnknownResolution = Effect.fn("SqliteSubmissionLedger.recordUnknownResolution")(function* (command) {
2644
- const operation = "ledger record unknown resolution";
2645
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(UnknownResolutionCommand))(command).pipe(Effect.mapError(internalFailure(operation)));
2646
- const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(Effect.mapError(internalFailure(operation)));
2647
- yield* hitFailpoint("ledger:unknown-resolution:before", operation);
2648
- const intent = yield* inWriteTransaction(operation, Effect.gen(function* () {
2649
- const submission = yield* requireSubmission(operation, validated.submissionId);
2650
- if (submission.state === "settled") {
2651
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2652
- return yield* SettlementConflict.make({
2653
- submissionId: validated.submissionId,
2654
- existingOutcome: submission.settled_outcome
2655
- });
2656
- }
2657
- const existing = (yield* readUnknownResolutions(operation, validated.submissionId)).find((row) => row.tool_call_id === validated.toolCallId);
2658
- if (existing !== void 0 && existing.resolution_json !== resolutionJson) return yield* UnknownResolutionConflict.make({
2659
- submissionId: validated.submissionId,
2660
- toolCallId: validated.toolCallId
2661
- });
2662
- let resolved;
2663
- if (existing !== void 0) resolved = yield* unknownResolutionIntentFromRow(operation, existing);
2664
- else {
2665
- const now = yield* currentInstant;
2666
- yield* sql`
2667
- INSERT INTO effect_agent_unknown_resolutions (
2668
- submission_id,
2669
- tool_call_id,
2670
- author,
2671
- reason,
2672
- resolution_json,
2673
- resolved_at
2674
- ) VALUES (
2675
- ${validated.submissionId},
2676
- ${validated.toolCallId},
2677
- ${validated.author},
2678
- ${validated.reason},
2679
- ${resolutionJson},
2680
- ${now.iso}
2681
- )
2682
- `.pipe(Effect.mapError(sqlFailure(operation)));
2683
- const resolution = yield* parseStoredJsonText(resolutionJson).pipe(Effect.mapError(internalFailure(operation)));
2684
- resolved = yield* decodeUnknownResolutionIntent({
2685
- submissionId: validated.submissionId,
2686
- toolCallId: validated.toolCallId,
2687
- author: validated.author,
2688
- reason: validated.reason,
2689
- resolution,
2690
- resolvedAt: now.iso
2691
- }).pipe(Effect.mapError(internalFailure(operation)));
2692
- }
2693
- if (submission.state === "unknown" && submission.unknown_tool_call_ids_json !== null) {
2694
- const markedIds = yield* storedUnknownToolCallIds(operation, submission);
2695
- const covering = yield* readUnknownResolutions(operation, validated.submissionId);
2696
- const coveredIds = new Set(covering.map((row) => row.tool_call_id));
2697
- if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) yield* sql`
2698
- UPDATE effect_agent_submissions
2699
- SET
2700
- state = 'input-applied',
2701
- unknown_reason = NULL,
2702
- unknown_tool_call_ids_json = NULL
2703
- WHERE submission_id = ${validated.submissionId}
2704
- `.pipe(Effect.mapError(sqlFailure(operation)));
2705
- }
2706
- return resolved;
2707
- }));
2708
- yield* hitFailpoint("ledger:unknown-resolution:after", operation);
2709
- return intent;
2710
- });
2711
- const recordChildSettled = Effect.fn("SqliteSubmissionLedger.recordChildSettled")(function* (request) {
2712
- const operation = "ledger record child settled";
2713
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildSettledNotification))(request).pipe(Effect.mapError(internalFailure(operation)));
2714
- yield* hitFailpoint("ledger:child-settled:before", operation);
2715
- const outcome = yield* inWriteTransaction(operation, Effect.gen(function* () {
2716
- const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2717
- const child = yield* readSubmission(operation, validated.childSubmissionId);
2718
- if (Option.isNone(child) || child.value.state !== "settled") return yield* LedgerError.make({
2719
- operation,
2720
- message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`
2721
- });
2722
- if (parent.state !== "suspended" || parent.suspended_reason_json === null) return "not-waiting";
2723
- const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(parent.suspended_reason_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", parent.submission_id, error.message)));
2724
- if (reason._tag !== "WaitingForChild") return "not-waiting";
2725
- if (!reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)) return "not-waiting";
2726
- for (const entry of reason.children) {
2727
- const listed = yield* readSubmission(operation, entry.childSubmissionId);
2728
- if (Option.isNone(listed) || listed.value.state !== "settled") return "still-waiting";
2729
- }
2730
- yield* sql`
2731
- UPDATE effect_agent_submissions
2732
- SET
2733
- state = 'input-applied',
2734
- suspended_reason_json = NULL,
2735
- suspended_at = NULL
2736
- WHERE submission_id = ${validated.parentSubmissionId}
2737
- `.pipe(Effect.mapError(sqlFailure(operation)));
2738
- return "woken";
2739
- }));
2740
- yield* hitFailpoint("ledger:child-settled:after", operation);
2741
- return outcome;
2742
- });
2743
- const reserveChildBudget = Effect.fn("SqliteSubmissionLedger.reserveChildBudget")(function* (request) {
2744
- const operation = "ledger reserve child budget";
2745
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildBudgetReservationRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2746
- const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(Effect.mapError(internalFailure(operation)));
2747
- yield* hitFailpoint("ledger:child-reservation:before", operation);
2748
- const reserved = yield* inWriteTransaction(operation, Effect.gen(function* () {
2749
- const existing = yield* readChildReservation(operation, validated.reservationId);
2750
- if (Option.isSome(existing)) {
2751
- if (!(existing.value.parent_submission_id === validated.parentSubmissionId && existing.value.parent_tool_call_id === validated.parentToolCallId && existing.value.allocation_digest === validated.allocationDigest && existing.value.allocation_json === allocationJson)) return yield* ChildReservationConflict.make({
2752
- reservationId: validated.reservationId,
2753
- status: existing.value.status,
2754
- message: "A reservation with this identity exists with a different parent Tool Call or allocation."
2755
- });
2756
- return ReservedChildBudget.make({
2757
- reservation: yield* childReservationSnapshotFromRow(operation, existing.value),
2758
- replayed: true
2759
- });
2760
- }
2761
- const collision = yield* readChildReservationForCall(operation, validated.parentSubmissionId, validated.parentToolCallId);
2762
- if (Option.isSome(collision)) return yield* ChildReservationConflict.make({
2763
- reservationId: validated.reservationId,
2764
- status: collision.value.status,
2765
- message: `Parent Tool Call ${validated.parentToolCallId} already owns reservation ${collision.value.reservation_id}.`
2766
- });
2767
- const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2768
- yield* requireOwnership(operation, parent, validated.ownershipToken);
2769
- const now = yield* currentInstant;
2770
- yield* sql`
2771
- INSERT INTO effect_agent_child_reservations (
2772
- reservation_id,
2773
- parent_submission_id,
2774
- parent_tool_call_id,
2775
- status,
2776
- allocation_json,
2777
- allocation_digest,
2778
- reserved_at
2779
- ) VALUES (
2780
- ${validated.reservationId},
2781
- ${validated.parentSubmissionId},
2782
- ${validated.parentToolCallId},
2783
- 'reserved',
2784
- ${allocationJson},
2785
- ${validated.allocationDigest},
2786
- ${now.iso}
2787
- )
2788
- `.pipe(Effect.mapError(sqlFailure(operation)));
2789
- const inserted = yield* readChildReservation(operation, validated.reservationId);
2790
- if (Option.isNone(inserted)) return yield* corruptionFailure(operation, "effect_agent_child_reservations", validated.reservationId, "An inserted child reservation row is missing inside its own transaction.");
2791
- return ReservedChildBudget.make({
2792
- reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
2793
- replayed: false
2794
- });
2795
- }));
2796
- yield* hitFailpoint("ledger:child-reservation:after", operation);
2797
- return reserved;
2798
- });
2799
- const attachChildToReservation = Effect.fn("SqliteSubmissionLedger.attachChildToReservation")(function* (request) {
2800
- const operation = "ledger attach child to reservation";
2801
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AttachChildToReservationRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2802
- yield* hitFailpoint("ledger:child-attach:before", operation);
2803
- const attached = yield* inWriteTransaction(operation, Effect.gen(function* () {
2804
- const existing = yield* readChildReservation(operation, validated.reservationId);
2805
- if (Option.isNone(existing)) return yield* LedgerError.make({
2806
- operation,
2807
- message: `Unknown child reservation ${validated.reservationId}.`
2808
- });
2809
- if (existing.value.child_submission_id !== null) {
2810
- if (existing.value.child_submission_id === validated.childSubmissionId) return yield* childReservationSnapshotFromRow(operation, existing.value);
2811
- return yield* ChildReservationConflict.make({
2812
- reservationId: validated.reservationId,
2813
- status: existing.value.status,
2814
- message: `Reservation ${validated.reservationId} already records child ${existing.value.child_submission_id}.`
2815
- });
2816
- }
2817
- const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
2818
- yield* requireOwnership(operation, parent, validated.ownershipToken);
2819
- if (existing.value.status !== "reserved") return yield* ChildReservationConflict.make({
2820
- reservationId: validated.reservationId,
2821
- status: existing.value.status,
2822
- message: `Cannot attach a child to a ${existing.value.status} reservation.`
2823
- });
2824
- const child = yield* readSubmission(operation, validated.childSubmissionId);
2825
- if (Option.isNone(child)) return yield* LedgerError.make({
2826
- operation,
2827
- message: `Unknown child submission ${validated.childSubmissionId}.`
2828
- });
2829
- yield* sql`
2830
- UPDATE effect_agent_child_reservations
2831
- SET child_submission_id = ${validated.childSubmissionId}
2832
- WHERE reservation_id = ${validated.reservationId}
2833
- `.pipe(Effect.mapError(sqlFailure(operation)));
2834
- const updated = yield* readChildReservation(operation, validated.reservationId);
2835
- if (Option.isNone(updated)) return yield* corruptionFailure(operation, "effect_agent_child_reservations", validated.reservationId, "An updated child reservation row is missing inside its own transaction.");
2836
- return yield* childReservationSnapshotFromRow(operation, updated.value);
2837
- }));
2838
- yield* hitFailpoint("ledger:child-attach:after", operation);
2839
- return attached;
2840
- });
2841
- const beginChildBudgetRelease = Effect.fn("SqliteSubmissionLedger.beginChildBudgetRelease")(function* (request) {
2842
- const operation = "ledger begin child budget release";
2843
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(BeginChildBudgetReleaseRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2844
- const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(Effect.mapError(internalFailure(operation)));
2845
- yield* hitFailpoint("ledger:child-release-pending:before", operation);
2846
- const frozen = yield* inWriteTransaction(operation, Effect.gen(function* () {
2847
- const existing = yield* readChildReservation(operation, validated.reservationId);
2848
- if (Option.isNone(existing)) return yield* LedgerError.make({
2849
- operation,
2850
- message: `Unknown child reservation ${validated.reservationId}.`
2851
- });
2852
- if (existing.value.status !== "reserved") {
2853
- if (existing.value.accounting_json === accountingJson) return yield* childReservationSnapshotFromRow(operation, existing.value);
2854
- return yield* ChildReservationConflict.make({
2855
- reservationId: validated.reservationId,
2856
- status: existing.value.status,
2857
- message: "A different accounting decision is already frozen for this reservation."
2858
- });
2859
- }
2860
- const now = yield* currentInstant;
2861
- yield* sql`
2862
- UPDATE effect_agent_child_reservations
2863
- SET
2864
- status = 'releasePending',
2865
- accounting_json = ${accountingJson},
2866
- release_began_at = ${now.iso}
2867
- WHERE reservation_id = ${validated.reservationId}
2868
- `.pipe(Effect.mapError(sqlFailure(operation)));
2869
- const updated = yield* readChildReservation(operation, validated.reservationId);
2870
- if (Option.isNone(updated)) return yield* corruptionFailure(operation, "effect_agent_child_reservations", validated.reservationId, "An updated child reservation row is missing inside its own transaction.");
2871
- return yield* childReservationSnapshotFromRow(operation, updated.value);
2872
- }));
2873
- yield* hitFailpoint("ledger:child-release-pending:after", operation);
2874
- return frozen;
2875
- });
2876
- const releaseChildBudget = Effect.fn("SqliteSubmissionLedger.releaseChildBudget")(function* (request) {
2877
- const operation = "ledger release child budget";
2878
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseChildBudgetRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2879
- yield* hitFailpoint("ledger:child-release:before", operation);
2880
- const released = yield* inWriteTransaction(operation, Effect.gen(function* () {
2881
- const existing = yield* readChildReservation(operation, validated.reservationId);
2882
- if (Option.isNone(existing)) return yield* LedgerError.make({
2883
- operation,
2884
- message: `Unknown child reservation ${validated.reservationId}.`
2885
- });
2886
- if (existing.value.status === "released") return yield* childReservationSnapshotFromRow(operation, existing.value);
2887
- if (existing.value.status !== "releasePending") return yield* ChildReservationConflict.make({
2888
- reservationId: validated.reservationId,
2889
- status: existing.value.status,
2890
- message: "Cannot release a reservation whose accounting decision is not frozen."
2891
- });
2892
- const now = yield* currentInstant;
2893
- yield* sql`
2894
- UPDATE effect_agent_child_reservations
2895
- SET status = 'released', released_at = ${now.iso}
2896
- WHERE reservation_id = ${validated.reservationId}
2897
- `.pipe(Effect.mapError(sqlFailure(operation)));
2898
- const updated = yield* readChildReservation(operation, validated.reservationId);
2899
- if (Option.isNone(updated)) return yield* corruptionFailure(operation, "effect_agent_child_reservations", validated.reservationId, "An updated child reservation row is missing inside its own transaction.");
2900
- return yield* childReservationSnapshotFromRow(operation, updated.value);
2901
- }));
2902
- yield* hitFailpoint("ledger:child-release:after", operation);
2903
- return released;
2904
- });
2905
- const scanPage = Effect.fn("SqliteSubmissionLedger.scanPage")(function* (cursor) {
2906
- const operation = "ledger scan nonterminal";
2907
- const rows = yield* (cursor === void 0 ? sql`
2908
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2909
- FROM effect_agent_submissions
2910
- WHERE state <> 'settled'
2911
- ORDER BY conversation_id ASC, queue_sequence ASC
2912
- LIMIT ${SCAN_PAGE_SIZE}
2913
- ` : sql`
2914
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2915
- FROM effect_agent_submissions
2916
- WHERE state <> 'settled'
2917
- AND (
2918
- conversation_id > ${cursor.conversationId}
2919
- OR (
2920
- conversation_id = ${cursor.conversationId}
2921
- AND queue_sequence > ${cursor.queueSequence}
2922
- )
2923
- )
2924
- ORDER BY conversation_id ASC, queue_sequence ASC
2925
- LIMIT ${SCAN_PAGE_SIZE}
2926
- `).pipe(Effect.mapError(sqlFailure(operation)));
2927
- const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
2928
- const snapshots = yield* Effect.forEach(decoded, (row) => decodeSubmissionSnapshot(operation, row));
2929
- const last = decoded[decoded.length - 1];
2930
- return [snapshots, last === void 0 || decoded.length < SCAN_PAGE_SIZE ? Option.none() : Option.some({
2931
- conversationId: last.conversation_id,
2932
- queueSequence: last.queue_sequence
2933
- })];
2934
- });
2935
- const scanNonterminal = Stream.paginate(void 0, scanPage);
2936
- const loadRecoverySnapshot = Effect.fn("SqliteSubmissionLedger.loadRecoverySnapshot")(function* (request) {
2937
- const operation = "ledger load recovery snapshot";
2938
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RecoverySnapshotRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2939
- return yield* sql.withTransaction(Effect.gen(function* () {
2940
- const submissionRow = yield* requireSubmission(operation, validated.submissionId);
2941
- const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
2942
- let ownership;
2943
- const ownershipRow = yield* readOwnership(operation, validated.submissionId);
2944
- if (Option.isSome(ownershipRow)) ownership = yield* decodeOwnershipSnapshot({
2945
- attemptId: ownershipRow.value.attempt_id,
2946
- ownerProducerId: ownershipRow.value.owner_producer_id,
2947
- producerEpoch: ownershipRow.value.producer_epoch,
2948
- leaseExpiresAt: ownershipRow.value.lease_expires_at
2949
- }).pipe(Effect.mapError(internalFailure(operation)));
2950
- let inputApplied;
2951
- if (submissionRow.input_applied_record_id !== null && submissionRow.input_applied_sequence !== null) inputApplied = yield* decodeInputAppliedMarker({
2952
- recordId: submissionRow.input_applied_record_id,
2953
- sequence: submissionRow.input_applied_sequence
2954
- }).pipe(Effect.mapError(internalFailure(operation)));
2955
- let reservation;
2956
- const reservationRow = yield* readReservation(operation, validated.submissionId);
2957
- if (Option.isSome(reservationRow)) {
2958
- const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, error.message)));
2959
- const settlementId = yield* Schema.decodeUnknownEffect(SettlementReservationSnapshot.fields.settlementId)(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
2960
- reservation = SettlementReservationSnapshot.make({
2961
- settlementId,
2962
- outcome: reservationRow.value.outcome,
2963
- record,
2964
- recordDigest: reservationRow.value.record_digest,
2965
- finalized: reservationRow.value.finalized_at !== null
2966
- });
2967
- }
2968
- let abortIntent;
2969
- const abortRow = yield* readAbortIntent(operation, validated.submissionId);
2970
- if (Option.isSome(abortRow)) abortIntent = yield* abortIntentFromRow(operation, submissionRow, validated.submissionId, abortRow.value);
2971
- const joinRows = yield* sql`
2972
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2973
- FROM effect_agent_submissions
2974
- WHERE joined_host_submission_id = ${validated.submissionId}
2975
- ORDER BY queue_sequence ASC
2976
- `.pipe(Effect.mapError(sqlFailure(operation)));
2977
- const joinSubmissions = yield* decodeSubmissionRows(operation, validated.submissionId, joinRows);
2978
- const joins = yield* Effect.forEach(joinSubmissions, (row) => decodeJoinSnapshot({
2979
- submissionId: row.submission_id,
2980
- state: row.state,
2981
- hostSubmissionId: validated.submissionId
2982
- }).pipe(Effect.mapError(internalFailure(operation))));
2983
- let hostSubmissionId;
2984
- if (submissionRow.joined_host_submission_id !== null) hostSubmissionId = yield* decodeSubmissionId(submissionRow.joined_host_submission_id).pipe(Effect.mapError(internalFailure(operation)));
2985
- let suspension;
2986
- if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
2987
- const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, error.message)));
2988
- suspension = yield* decodeSuspensionSnapshot({
2989
- reason,
2990
- suspendedAt: submissionRow.suspended_at
2991
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, error.message)));
2992
- }
2993
- const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
2994
- const approvalDecisions = yield* Effect.forEach(decisionRows, (row) => approvalIntentFromRow(operation, row));
2995
- const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
2996
- const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) => unknownResolutionIntentFromRow(operation, row));
2997
- const childReservationRows = yield* sql`
2998
- SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
2999
- FROM effect_agent_child_reservations
3000
- WHERE parent_submission_id = ${validated.submissionId}
3001
- ORDER BY parent_tool_call_id ASC
3002
- `.pipe(Effect.mapError(sqlFailure(operation)));
3003
- const decodedChildReservations = yield* decodeChildReservationRows(operation, validated.submissionId, childReservationRows);
3004
- const childReservations = yield* Effect.forEach(decodedChildReservations, (row) => childReservationSnapshotFromRow(operation, row));
3005
- const childAttachments = [];
3006
- for (const row of decodedChildReservations) {
3007
- if (row.child_submission_id === null) continue;
3008
- const child = yield* readSubmission(operation, row.child_submission_id);
3009
- if (Option.isNone(child)) continue;
3010
- childAttachments.push(yield* decodeChildAttachmentSnapshot({
3011
- toolCallId: row.parent_tool_call_id,
3012
- childSubmissionId: row.child_submission_id,
3013
- childState: child.value.state,
3014
- ...child.value.settled_outcome === null ? {} : { childOutcome: child.value.settled_outcome }
3015
- }).pipe(Effect.mapError(internalFailure(operation))));
3016
- }
3017
- let parentLinkage;
3018
- if (submissionRow.parent_submission_id !== null && submissionRow.parent_tool_call_id !== null) parentLinkage = yield* decodeParentLinkage({
3019
- parentSubmissionId: submissionRow.parent_submission_id,
3020
- parentToolCallId: submissionRow.parent_tool_call_id
3021
- }).pipe(Effect.mapError(internalFailure(operation)));
3022
- return RecoverySnapshot.make({
3023
- submission,
3024
- joins,
3025
- approvalDecisions,
3026
- unknownResolutions,
3027
- childReservations,
3028
- childAttachments,
3029
- ...parentLinkage === void 0 ? {} : { parentLinkage },
3030
- ...hostSubmissionId === void 0 ? {} : { hostSubmissionId },
3031
- ...suspension === void 0 ? {} : { suspension },
3032
- ...ownership === void 0 ? {} : { ownership },
3033
- ...inputApplied === void 0 ? {} : { inputApplied },
3034
- ...reservation === void 0 ? {} : { reservation },
3035
- ...abortIntent === void 0 ? {} : { abortIntent }
3036
- });
3037
- })).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(sqlFailure(operation)(error))));
3038
- });
3039
- return Context.make(SubmissionLedger, SubmissionLedger.of({
3040
- capabilities,
3041
- admit,
3042
- markReady,
3043
- lookup,
3044
- resolveAdmission,
3045
- claim,
3046
- renewOwnership,
3047
- releaseOwnership,
3048
- markInputApplied,
3049
- reserveSettlement,
3050
- finalizeSettlement,
3051
- requestAbort,
3052
- claimJoining,
3053
- markJoined,
3054
- revertJoining,
3055
- suspend,
3056
- recordApprovalDecision,
3057
- markUnknown,
3058
- recordUnknownResolution,
3059
- recordChildSettled,
3060
- reserveChildBudget,
3061
- attachChildToReservation,
3062
- beginChildBudgetRelease,
3063
- releaseChildBudget,
3064
- scanNonterminal,
3065
- loadRecoverySnapshot
3066
- }));
3067
- });
3068
- /**
3069
- * SQLite SubmissionLedger implementation sharing the journal's database file, write
3070
- * transaction discipline, and producer-epoch fencing substrate. Configuration, failpoint,
3071
- * SQL, and Crypto authority stay visible in the input channel.
3072
- */
3073
- const submissionLedgerLayer = Layer.effectContext(makeServices());
3074
- /**
3075
- * A composition-root convenience Layer for the durable Submission Ledger. Point it at the
3076
- * same database file as the ConversationStore so claims fence the same producer epochs.
3077
- */
3078
- const ledgerLayer = (options) => submissionLedgerLayer.pipe(Layer.provide(Layer.mergeAll(storageConfigLayer(options), storageFailpointLayer(options), SqliteClient.layer({ filename: options.filename }), NodeCrypto.layer)));
3079
- //#endregion
3080
- export { CurrentSqliteStorageVersion, SqliteAppendConflict, SqliteCheckpointConflict, SqliteFenceRejected, SqliteLedgerError, SqliteStorageCompatibilityError, SqliteStorageConfig, SqliteStorageConfigValue, SqliteStorageCorruptionError, SqliteStorageError, SqliteStorageFailpoint, SqliteStorageFailpointError, SqliteStorageFailpointLocation, SqliteStorageFailpointTestControl, SqliteWriteContention, conversationStoreLayer, layer, ledgerLayer, observationOffsetAt, sqliteMigrations, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer };
3081
-
3082
- //# sourceMappingURL=index.mjs.map
1
+ import { t as SqliteStorageConfig_exports } from "./SqliteStorageConfig.mjs";
2
+ import { t as SqliteStorageError_exports } from "./SqliteStorageError.mjs";
3
+ import { t as SqliteStorageFailpoint_exports } from "./SqliteStorageFailpoint.mjs";
4
+ import { t as SqliteMessageDeliveryStore_exports } from "./SqliteMessageDeliveryStore.mjs";
5
+ import { t as SqliteActivityStore_exports } from "./SqliteActivityStore.mjs";
6
+ import { t as SqliteScheduleStore_exports } from "./SqliteScheduleStore.mjs";
7
+ import { t as SqliteStorageVersion_exports } from "./SqliteStorageVersion.mjs";
8
+ import { t as SqliteThreadStore_exports } from "./SqliteThreadStore.mjs";
9
+ import { t as SqliteSubmissionLedger_exports } from "./SqliteSubmissionLedger.mjs";
10
+ import { t as SqliteSubscriptionStore_exports } from "./SqliteSubscriptionStore.mjs";
11
+ export { SqliteActivityStore_exports as SqliteActivityStore, SqliteMessageDeliveryStore_exports as SqliteMessageDeliveryStore, SqliteScheduleStore_exports as SqliteScheduleStore, SqliteStorageConfig_exports as SqliteStorageConfig, SqliteStorageError_exports as SqliteStorageError, SqliteStorageFailpoint_exports as SqliteStorageFailpoint, SqliteStorageVersion_exports as SqliteStorageVersion, SqliteSubmissionLedger_exports as SqliteSubmissionLedger, SqliteSubscriptionStore_exports as SqliteSubscriptionStore, SqliteThreadStore_exports as SqliteThreadStore };