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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/SqliteActivityStore.d.mts +14 -0
  2. package/dist/SqliteActivityStore.mjs +310 -0
  3. package/dist/SqliteActivityStore.mjs.map +1 -0
  4. package/dist/SqliteScheduleStore.d.mts +14 -0
  5. package/dist/SqliteScheduleStore.mjs +255 -0
  6. package/dist/SqliteScheduleStore.mjs.map +1 -0
  7. package/dist/SqliteStorageConfig.d.mts +34 -0
  8. package/dist/SqliteStorageConfig.mjs +39 -0
  9. package/dist/SqliteStorageConfig.mjs.map +1 -0
  10. package/dist/{sqlite-storage-failpoint-Cr0SXflP.d.mts → SqliteStorageError-W1oM5ka3.d.mts} +6 -15
  11. package/dist/SqliteStorageError.d.mts +2 -0
  12. package/dist/SqliteStorageError.mjs +144 -0
  13. package/dist/SqliteStorageError.mjs.map +1 -0
  14. package/dist/SqliteStorageFailpoint.d.mts +17 -0
  15. package/dist/{sqlite-storage-failpoint-DIwWk5dw.mjs → SqliteStorageFailpoint.mjs} +5 -3
  16. package/dist/SqliteStorageFailpoint.mjs.map +1 -0
  17. package/dist/{testing.d.mts → SqliteStorageFailpointTesting.d.mts} +3 -3
  18. package/dist/{testing.mjs → SqliteStorageFailpointTesting.mjs} +3 -3
  19. package/dist/SqliteStorageFailpointTesting.mjs.map +1 -0
  20. package/dist/SqliteStorageVersion-DwQbn4bw.d.mts +9 -0
  21. package/dist/SqliteStorageVersion.d.mts +2 -0
  22. package/dist/SqliteStorageVersion.mjs +8 -0
  23. package/dist/SqliteStorageVersion.mjs.map +1 -0
  24. package/dist/SqliteSubmissionLedger.d.mts +23 -0
  25. package/dist/SqliteSubmissionLedger.mjs +1656 -0
  26. package/dist/SqliteSubmissionLedger.mjs.map +1 -0
  27. package/dist/SqliteSubscriptionStore.d.mts +13 -0
  28. package/dist/SqliteSubscriptionStore.mjs +596 -0
  29. package/dist/SqliteSubscriptionStore.mjs.map +1 -0
  30. package/dist/SqliteThreadStore.d.mts +49 -0
  31. package/dist/SqliteThreadStore.mjs +422 -0
  32. package/dist/SqliteThreadStore.mjs.map +1 -0
  33. package/dist/index.d.mts +10 -108
  34. package/dist/index.mjs +10 -4265
  35. package/dist/migrations-B82C9A3r.mjs +293 -0
  36. package/dist/migrations-B82C9A3r.mjs.map +1 -0
  37. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  38. package/dist/sqlite-journal-C5SZnSdC.mjs +656 -0
  39. package/dist/sqlite-journal-C5SZnSdC.mjs.map +1 -0
  40. package/package.json +1 -1
  41. package/src/{sqlite-activity-store.ts → SqliteActivityStore.ts} +2 -2
  42. package/src/{sqlite-schedule-store.ts → SqliteScheduleStore.ts} +9 -7
  43. package/src/{errors.ts → SqliteStorageError.ts} +1 -1
  44. package/src/{sqlite-storage-failpoint.ts → SqliteStorageFailpoint.ts} +4 -1
  45. package/src/{sqlite-storage-failpoint-testing.ts → SqliteStorageFailpointTesting.ts} +1 -1
  46. package/src/SqliteStorageVersion.ts +2 -0
  47. package/src/{sqlite-ledger.ts → SqliteSubmissionLedger.ts} +17 -15
  48. package/src/{sqlite-subscription-store.ts → SqliteSubscriptionStore.ts} +14 -12
  49. package/src/{sqlite-thread-store.ts → SqliteThreadStore.ts} +19 -21
  50. package/src/index.ts +9 -10
  51. package/src/{sqlite-journal.ts → internal/sqlite-journal.ts} +5 -5
  52. package/dist/index.mjs.map +0 -1
  53. package/dist/sqlite-storage-failpoint-DIwWk5dw.mjs.map +0 -1
  54. package/dist/testing.mjs.map +0 -1
  55. package/src/sqlite-memory-store.ts +0 -7
  56. package/src/testing.ts +0 -2
  57. /package/src/{sqlite-storage-config.ts → SqliteStorageConfig.ts} +0 -0
  58. /package/src/{migrations.ts → internal/migrations.ts} +0 -0
package/dist/index.mjs CHANGED
@@ -1,4265 +1,10 @@
1
- import { t as SqliteStorageFailpoint } from "./sqlite-storage-failpoint-DIwWk5dw.mjs";
2
- import { AbortCommand, AbortIntent, AcceptedEvent, ActivityBusy, ActivityClaim, ActivityClaimRequest, ActivityMutationFailpoint, ActivityOwnershipLost, ActivityProcessorKey, ActivityProcessorStore, ActivityProgress, ActivityStoreError, ActivityWorkConflict, 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, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, DeliveryChange, Digest, EMPTY_TAIL_DIGEST, FenceRejected, FencedAppendRequest, InputAppliedMarker, JoinSnapshot, JoinedToHost, JoiningClaim, LedgerCapabilities, LedgerError, LoadCheckpointRequest, MarkInputAppliedRequest, MarkJoinedRequest, MarkReadyRequest, MarkUnknownRequest, ObservationOffset, OwnershipLost, OwnershipRenewal, OwnershipSnapshot, ParentLinkage, PersistedJson, PreparedActivity, ProducerEpoch, QueueSequence, RecordEnvelope, RecoverySnapshot, RecoverySnapshotRequest, ReleaseChildBudgetRequest, ReleaseOwnershipRequest, RenewOwnershipRequest, ReservedChildBudget, ReservedSettlement, RevertJoiningRequest, SaveCheckpointRequest, ScheduleCapacityError, ScheduleChange, ScheduleConflict, ScheduleDueCursor, ScheduleFailpoint, ScheduleId, ScheduleInstant, ScheduleKey, ScheduleNotFound, ScheduleOwner, SchedulePageRequest, ScheduleRecord, ScheduleStorageError, ScheduleStore, Settlement, SettlementConflict, SettlementFinalization, SettlementOutcome, SettlementReservation, SettlementReservationSnapshot, SourcePartition, SubmissionLedger, SubmissionLookup, SubmissionLookupByKey, SubmissionSnapshot, SubmissionState, SubscriptionDelivery, SubscriptionDeliveryKey, SubscriptionError, SubscriptionFailpoint, SubscriptionKey, SubscriptionLimits, SubscriptionName, SubscriptionRecord, SubscriptionScanCursors, SubscriptionStore, SuspendRequest, SuspensionReason, SuspensionSnapshot, ThreadCheckpoint, ThreadExport, ThreadExportRequest, ThreadMaterialization, ThreadNotMaterialized, ThreadObservation, ThreadRead, ThreadStore, ThreadStoreError, ThreadTail, ThreadTailRequest, UnknownResolution, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, applyScheduleChange, applySubscriptionDeliveryChange, defaultSchedulingLimits, digestCanonicalBatch, sameAcceptedEventIdentity, sameSourcePartition, scheduleDeadline, scheduleUsesCapacity, settlementFailureFromRecord, submissionAbortRecordId, subscriptionCanSelect, subscriptionDeliveryCanSelect, subscriptionDeliveryKeyString } from "@effect-agent/thread";
3
- import { Clock, Context, Crypto, DateTime, Duration, Effect, Exit, Layer, Option, Ref, Result, Schema, Stream } from "effect";
4
- import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node";
5
- import * as SqlClient from "effect/unstable/sql/SqlClient";
6
- import { NodeCrypto } from "@effect/platform-node";
7
- import { memoryReaderLayer, memoryStoreLayer, memoryStoreLayerWithFailpoints } from "@effect-agent/thread/sql-memory";
8
- //#region src/errors.ts
9
- /** The SQLite file uses a private-development format this adapter cannot read. */
10
- var SqliteStorageCompatibilityError = class extends Schema.TaggedError()("SqliteStorageCompatibilityError", {
11
- actualVersion: Schema.Int,
12
- message: Schema.String,
13
- supportedVersion: Schema.Int
14
- }) {};
15
- /** Stored bytes failed the current Schema and cannot be used as recovery truth. */
16
- var SqliteStorageCorruptionError = class extends Schema.TaggedError()("SqliteStorageCorruptionError", {
17
- message: Schema.String,
18
- rowKey: Schema.String,
19
- table: Schema.String
20
- }) {};
21
- /** SQLite infrastructure failed while opening or operating the store. */
22
- var SqliteStorageError = class extends Schema.TaggedError()("SqliteStorageError", {
23
- cause: Schema.optionalKey(Schema.Defect()),
24
- message: Schema.String,
25
- operation: Schema.String
26
- }) {};
27
- /**
28
- * SQLite infrastructure failed while operating the Submission Ledger. Surfaces at the
29
- * SubmissionLedger port as the typed `LedgerError` with this error preserved as its cause,
30
- * so the adapter-level tag is never erased.
31
- */
32
- var SqliteLedgerError = class extends Schema.TaggedError()("SqliteLedgerError", {
33
- cause: Schema.optionalKey(Schema.Defect()),
34
- message: Schema.String,
35
- operation: Schema.String
36
- }) {};
37
- /**
38
- * A canonical batch retry conflicts with existing append state. Tail conflicts carry the
39
- * actual committed tail as a diagnostic resume hint.
40
- */
41
- var SqliteAppendConflict = class extends Schema.TaggedError()("SqliteAppendConflict", {
42
- message: Schema.String,
43
- reason: Schema.Literals([
44
- "batch-digest",
45
- "record-identity",
46
- "tail"
47
- ]),
48
- actualTailSequence: Schema.optionalKey(CanonicalSequence),
49
- actualTailDigest: Schema.optionalKey(Schema.String)
50
- }) {};
51
- /**
52
- * A producer epoch does not match the Thread's current writer registration. Appends
53
- * require the exact registered epoch, so both older and newer unregistered epochs are fenced;
54
- * a newer epoch takes over by materializing first.
55
- */
56
- var SqliteFenceRejected = class extends Schema.TaggedError()("SqliteFenceRejected", {
57
- actualEpoch: ProducerEpoch,
58
- message: Schema.String,
59
- producerEpoch: ProducerEpoch
60
- }) {};
61
- /**
62
- * A write transaction could not acquire the SQLite write lock within the configured busy
63
- * timeout (SQLITE_BUSY / SQLITE_LOCKED). Another transiently coexisting owner is writing;
64
- * the operation did not mutate canonical state and is safe to retry.
65
- */
66
- var SqliteWriteContention = class extends Schema.TaggedError()("SqliteWriteContention", {
67
- cause: Schema.optionalKey(Schema.Defect()),
68
- message: Schema.String,
69
- operation: Schema.String
70
- }) {};
71
- /** A checkpoint conflicts with a previously stored checkpoint at the same offset. */
72
- var SqliteCheckpointConflict = class extends Schema.TaggedError()("SqliteCheckpointConflict", { message: Schema.String }) {};
73
- const SqliteStorageFailpointLocation = Schema.Literals([
74
- "materialize:before",
75
- "materialize:after",
76
- "append:before",
77
- "append:after-batch-insert",
78
- "append:after-record-insert",
79
- "append:after-tail-update",
80
- "append:after",
81
- "export:after-thread-read",
82
- "save-checkpoint:before",
83
- "save-checkpoint:after",
84
- "ledger:admit:before",
85
- "ledger:admit:after",
86
- "ledger:mark-ready:before",
87
- "ledger:mark-ready:after",
88
- "ledger:claim:before",
89
- "ledger:claim:after",
90
- "ledger:mark-input-applied:before",
91
- "ledger:mark-input-applied:after",
92
- "ledger:renew:before",
93
- "ledger:renew:after",
94
- "ledger:reserve-settlement:before",
95
- "ledger:reserve-settlement:after",
96
- "ledger:finalize-settlement:before",
97
- "ledger:finalize-settlement:after",
98
- "ledger:request-abort:before",
99
- "ledger:request-abort:after",
100
- "ledger:release:before",
101
- "ledger:release:after",
102
- "ledger:claim-joining:before",
103
- "ledger:claim-joining:after",
104
- "ledger:mark-joined:before",
105
- "ledger:mark-joined:after",
106
- "ledger:revert-joining:before",
107
- "ledger:revert-joining:after",
108
- "ledger:suspend:before",
109
- "ledger:suspend:after",
110
- "ledger:approval-decision:before",
111
- "ledger:approval-decision:after",
112
- "ledger:mark-unknown:before",
113
- "ledger:mark-unknown:after",
114
- "ledger:unknown-resolution:before",
115
- "ledger:unknown-resolution:after",
116
- "ledger:child-reservation:before",
117
- "ledger:child-reservation:after",
118
- "ledger:child-attach:before",
119
- "ledger:child-attach:after",
120
- "ledger:child-release-pending:before",
121
- "ledger:child-release-pending:after",
122
- "ledger:child-release:before",
123
- "ledger:child-release:after",
124
- "ledger:child-settled:before",
125
- "ledger:child-settled:after"
126
- ]);
127
- /** Deterministic test-only fault or pause injected at a SQLite operation boundary. */
128
- var SqliteStorageFailpointError = class extends Schema.TaggedError()("SqliteStorageFailpointError", { location: SqliteStorageFailpointLocation }) {
129
- get message() {
130
- return `Injected SQLite storage failure at ${this.location}.`;
131
- }
132
- };
133
- //#endregion
134
- //#region src/migrations.ts
135
- const CurrentSqliteStorageVersion = 7;
136
- /** Initialize empty storage with the complete current schema. */
137
- const sqliteMigrations = SqliteMigrator.fromRecord({ "1_current_thread_storage": Effect.gen(function* () {
138
- const sql = yield* SqlClient.SqlClient;
139
- yield* sql`
140
- CREATE TABLE effect_agent_threads (
141
- thread_id TEXT PRIMARY KEY NOT NULL,
142
- created_at TEXT NOT NULL,
143
- tail_sequence INTEGER NOT NULL,
144
- tail_digest TEXT NOT NULL,
145
- producer_epoch INTEGER NOT NULL
146
- )
147
- `.withoutTransform;
148
- yield* sql`
149
- CREATE TABLE effect_agent_canonical_batches (
150
- thread_id TEXT NOT NULL,
151
- batch_id TEXT NOT NULL,
152
- first_sequence INTEGER NOT NULL,
153
- last_sequence INTEGER NOT NULL,
154
- batch_digest TEXT NOT NULL,
155
- tail_digest TEXT NOT NULL,
156
- batch_json TEXT NOT NULL,
157
- PRIMARY KEY (thread_id, batch_id),
158
- FOREIGN KEY (thread_id)
159
- REFERENCES effect_agent_threads(thread_id)
160
- ON DELETE RESTRICT
161
- )
162
- `.withoutTransform;
163
- yield* sql`
164
- CREATE TABLE effect_agent_canonical_records (
165
- thread_id TEXT NOT NULL,
166
- sequence INTEGER NOT NULL,
167
- record_id TEXT NOT NULL,
168
- batch_id TEXT NOT NULL,
169
- record_json TEXT NOT NULL,
170
- PRIMARY KEY (thread_id, sequence),
171
- UNIQUE (thread_id, record_id),
172
- FOREIGN KEY (thread_id, batch_id)
173
- REFERENCES effect_agent_canonical_batches(thread_id, batch_id)
174
- ON DELETE RESTRICT
175
- )
176
- `.withoutTransform;
177
- yield* sql`
178
- CREATE INDEX effect_agent_canonical_records_batch
179
- ON effect_agent_canonical_records (thread_id, batch_id, sequence)
180
- `.withoutTransform;
181
- yield* sql`
182
- CREATE TABLE effect_agent_checkpoints (
183
- thread_id TEXT NOT NULL,
184
- through_sequence INTEGER NOT NULL,
185
- tail_digest TEXT NOT NULL,
186
- checkpoint_json TEXT NOT NULL,
187
- PRIMARY KEY (thread_id, through_sequence),
188
- FOREIGN KEY (thread_id)
189
- REFERENCES effect_agent_threads(thread_id)
190
- ON DELETE RESTRICT
191
- )
192
- `.withoutTransform;
193
- yield* sql`
194
- CREATE TABLE effect_agent_submissions (
195
- submission_id TEXT PRIMARY KEY NOT NULL,
196
- thread_id TEXT NOT NULL,
197
- queue_sequence INTEGER NOT NULL,
198
- principal TEXT NOT NULL,
199
- idempotency_key TEXT NOT NULL,
200
- agent_id TEXT NOT NULL,
201
- agent_digests_json TEXT NOT NULL,
202
- deployment_id TEXT NOT NULL,
203
- input_json TEXT NOT NULL,
204
- input_digest TEXT NOT NULL,
205
- receipt_id TEXT NOT NULL,
206
- state TEXT NOT NULL,
207
- settled_outcome TEXT,
208
- created_at TEXT NOT NULL,
209
- ready_at TEXT,
210
- input_applied_record_id TEXT,
211
- input_applied_sequence INTEGER,
212
- joined_host_submission_id TEXT,
213
- suspended_reason_json TEXT,
214
- suspended_at TEXT,
215
- unknown_reason TEXT,
216
- unknown_tool_call_ids_json TEXT,
217
- parent_submission_id TEXT,
218
- parent_tool_call_id TEXT,
219
- UNIQUE (thread_id, principal, idempotency_key),
220
- UNIQUE (thread_id, queue_sequence)
221
- )
222
- `.withoutTransform;
223
- yield* sql`
224
- CREATE TABLE effect_agent_submission_ownership (
225
- submission_id TEXT PRIMARY KEY NOT NULL,
226
- attempt_id TEXT NOT NULL,
227
- ownership_token TEXT NOT NULL,
228
- producer_epoch INTEGER NOT NULL,
229
- owner_producer_id TEXT NOT NULL,
230
- lease_expires_at TEXT NOT NULL,
231
- FOREIGN KEY (submission_id)
232
- REFERENCES effect_agent_submissions(submission_id)
233
- ON DELETE RESTRICT
234
- )
235
- `.withoutTransform;
236
- yield* sql`
237
- CREATE TABLE effect_agent_attempts (
238
- attempt_id TEXT PRIMARY KEY NOT NULL,
239
- submission_id TEXT NOT NULL,
240
- thread_id TEXT NOT NULL,
241
- owner_producer_id TEXT NOT NULL,
242
- producer_epoch INTEGER NOT NULL,
243
- claimed_at TEXT NOT NULL,
244
- FOREIGN KEY (submission_id)
245
- REFERENCES effect_agent_submissions(submission_id)
246
- ON DELETE RESTRICT
247
- )
248
- `.withoutTransform;
249
- yield* sql`
250
- CREATE TABLE effect_agent_settlement_reservations (
251
- submission_id TEXT PRIMARY KEY NOT NULL,
252
- settlement_id TEXT NOT NULL,
253
- outcome TEXT NOT NULL,
254
- record_id TEXT NOT NULL,
255
- record_json TEXT NOT NULL,
256
- record_digest TEXT NOT NULL,
257
- reserved_at TEXT NOT NULL,
258
- finalized_at TEXT,
259
- FOREIGN KEY (submission_id)
260
- REFERENCES effect_agent_submissions(submission_id)
261
- ON DELETE RESTRICT
262
- )
263
- `.withoutTransform;
264
- yield* sql`
265
- CREATE TABLE effect_agent_abort_intents (
266
- submission_id TEXT PRIMARY KEY NOT NULL,
267
- author TEXT NOT NULL,
268
- reason TEXT NOT NULL,
269
- requested_at TEXT NOT NULL,
270
- canonical_record_id TEXT,
271
- FOREIGN KEY (submission_id)
272
- REFERENCES effect_agent_submissions(submission_id)
273
- ON DELETE RESTRICT
274
- )
275
- `.withoutTransform;
276
- yield* sql`
277
- CREATE INDEX effect_agent_submissions_joined_host
278
- ON effect_agent_submissions (joined_host_submission_id)
279
- `.withoutTransform;
280
- yield* sql`
281
- CREATE TABLE effect_agent_approval_decisions (
282
- submission_id TEXT NOT NULL,
283
- tool_call_id TEXT NOT NULL,
284
- decision TEXT NOT NULL,
285
- resolver TEXT NOT NULL,
286
- reason TEXT NOT NULL,
287
- decided_at TEXT NOT NULL,
288
- PRIMARY KEY (submission_id, tool_call_id),
289
- FOREIGN KEY (submission_id)
290
- REFERENCES effect_agent_submissions(submission_id)
291
- ON DELETE RESTRICT
292
- )
293
- `.withoutTransform;
294
- yield* sql`
295
- CREATE TABLE effect_agent_unknown_resolutions (
296
- submission_id TEXT NOT NULL,
297
- tool_call_id TEXT NOT NULL,
298
- author TEXT NOT NULL,
299
- reason TEXT NOT NULL,
300
- resolution_json TEXT NOT NULL,
301
- resolved_at TEXT NOT NULL,
302
- PRIMARY KEY (submission_id, tool_call_id),
303
- FOREIGN KEY (submission_id)
304
- REFERENCES effect_agent_submissions(submission_id)
305
- ON DELETE RESTRICT
306
- )
307
- `.withoutTransform;
308
- yield* sql`
309
- CREATE INDEX effect_agent_submissions_parent
310
- ON effect_agent_submissions (parent_submission_id)
311
- `.withoutTransform;
312
- yield* sql`
313
- CREATE TABLE effect_agent_child_reservations (
314
- reservation_id TEXT PRIMARY KEY NOT NULL,
315
- parent_submission_id TEXT NOT NULL,
316
- parent_tool_call_id TEXT NOT NULL,
317
- child_submission_id TEXT,
318
- status TEXT NOT NULL,
319
- allocation_json TEXT NOT NULL,
320
- allocation_digest TEXT NOT NULL,
321
- accounting_json TEXT,
322
- reserved_at TEXT NOT NULL,
323
- release_began_at TEXT,
324
- released_at TEXT,
325
- UNIQUE (parent_submission_id, parent_tool_call_id),
326
- FOREIGN KEY (parent_submission_id)
327
- REFERENCES effect_agent_submissions(submission_id)
328
- ON DELETE RESTRICT
329
- )
330
- `.withoutTransform;
331
- yield* sql`
332
- CREATE TABLE effect_agent_schedules (
333
- tenant_id TEXT NOT NULL,
334
- owner_id TEXT NOT NULL,
335
- schedule_id TEXT NOT NULL,
336
- deadline_at_millis INTEGER,
337
- record_json TEXT NOT NULL,
338
- PRIMARY KEY (tenant_id, owner_id, schedule_id)
339
- )
340
- `.withoutTransform;
341
- yield* sql`
342
- CREATE INDEX effect_agent_schedules_deadline
343
- ON effect_agent_schedules (deadline_at_millis, tenant_id, owner_id, schedule_id)
344
- WHERE deadline_at_millis IS NOT NULL
345
- `.withoutTransform;
346
- yield* sql`
347
- CREATE INDEX effect_agent_schedules_owner_deadline
348
- ON effect_agent_schedules (tenant_id, owner_id, deadline_at_millis, schedule_id)
349
- WHERE deadline_at_millis IS NOT NULL
350
- `.withoutTransform;
351
- yield* sql`
352
- CREATE TABLE effect_agent_subscription_sequences (
353
- tenant_id TEXT NOT NULL,
354
- source_address TEXT NOT NULL,
355
- sequence INTEGER NOT NULL,
356
- event_scan_cursor TEXT NOT NULL,
357
- delivery_scan_cursor TEXT NOT NULL,
358
- recovery_scan_cursor INTEGER NOT NULL,
359
- PRIMARY KEY (tenant_id, source_address)
360
- )
361
- `.withoutTransform;
362
- yield* sql`
363
- CREATE TABLE effect_agent_subscriptions (
364
- tenant_id TEXT NOT NULL,
365
- source_address TEXT NOT NULL,
366
- owner_id TEXT NOT NULL,
367
- subscription_id TEXT NOT NULL,
368
- ordinal INTEGER NOT NULL,
369
- source_name TEXT NOT NULL,
370
- source_version TEXT NOT NULL,
371
- matching_key TEXT NOT NULL,
372
- state TEXT NOT NULL,
373
- expires_at_millis INTEGER NOT NULL,
374
- recovery_at_millis INTEGER,
375
- record_json TEXT NOT NULL,
376
- PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id),
377
- UNIQUE (tenant_id, source_address, ordinal)
378
- )
379
- `.withoutTransform;
380
- yield* sql`CREATE INDEX effect_agent_subscriptions_owner ON effect_agent_subscriptions (tenant_id, source_address, owner_id, ordinal)`.withoutTransform;
381
- yield* sql`CREATE INDEX effect_agent_subscriptions_candidates ON effect_agent_subscriptions (tenant_id, source_address, source_name, source_version, matching_key, ordinal)`.withoutTransform;
382
- yield* sql`CREATE INDEX effect_agent_subscriptions_recovery ON effect_agent_subscriptions (tenant_id, source_address, recovery_at_millis, ordinal) WHERE recovery_at_millis IS NOT NULL`.withoutTransform;
383
- yield* sql`
384
- CREATE TABLE effect_agent_subscription_events (
385
- tenant_id TEXT NOT NULL,
386
- source_address TEXT NOT NULL,
387
- event_id TEXT NOT NULL,
388
- source_name TEXT NOT NULL,
389
- source_version TEXT NOT NULL,
390
- matching_key TEXT NOT NULL,
391
- payload_digest TEXT NOT NULL,
392
- cutoff INTEGER NOT NULL,
393
- cursor INTEGER NOT NULL,
394
- routing_complete INTEGER NOT NULL,
395
- next_attempt_at_millis INTEGER NOT NULL,
396
- record_json TEXT NOT NULL,
397
- PRIMARY KEY (tenant_id, source_address, event_id)
398
- )
399
- `.withoutTransform;
400
- yield* sql`CREATE INDEX effect_agent_subscription_events_pending ON effect_agent_subscription_events (tenant_id, source_address, routing_complete, next_attempt_at_millis, event_id)`.withoutTransform;
401
- yield* sql`
402
- CREATE TABLE effect_agent_subscription_deliveries (
403
- tenant_id TEXT NOT NULL,
404
- source_address TEXT NOT NULL,
405
- owner_id TEXT NOT NULL,
406
- subscription_id TEXT NOT NULL,
407
- event_id TEXT NOT NULL,
408
- delivery_key TEXT NOT NULL,
409
- state TEXT NOT NULL,
410
- next_attempt_at_millis INTEGER NOT NULL,
411
- record_json TEXT NOT NULL,
412
- PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id, event_id),
413
- UNIQUE (tenant_id, source_address, delivery_key)
414
- )
415
- `.withoutTransform;
416
- yield* sql`CREATE INDEX effect_agent_subscription_deliveries_pending ON effect_agent_subscription_deliveries (tenant_id, source_address, state, next_attempt_at_millis, delivery_key)`.withoutTransform;
417
- yield* sql`CREATE INDEX effect_agent_subscription_deliveries_registration ON effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, delivery_key)`.withoutTransform;
418
- yield* sql`PRAGMA user_version = 7`.withoutTransform;
419
- }) });
420
- //#endregion
421
- //#region src/sqlite-activity-store.ts
422
- const STORAGE_VERSION = 1;
423
- const METADATA_COMPONENT = "activity";
424
- const STATE_TABLE = "effect_agent_activity_processor_state_v1";
425
- const StoredJson$1 = Schema.String.check(Schema.isMaxLength(16777216));
426
- const sameKey = Schema.toEquivalence(ActivityProcessorKey);
427
- const sameWork = Schema.toEquivalence(PreparedActivity);
428
- var ActivityMetadataRow = class extends Schema.Class("@effect-agent/storage-sqlite/ActivityMetadataRow")({ version: Schema.Int }) {};
429
- var ActivityTableRow = class extends Schema.Class("@effect-agent/storage-sqlite/ActivityTableRow")({ name: Schema.NonEmptyString }) {};
430
- var ActivityStateRow = class extends Schema.Class("@effect-agent/storage-sqlite/ActivityStateRow")({
431
- processor_id: ActivityProcessorKey.fields.processorId,
432
- processor_version: ActivityProcessorKey.fields.processorVersion,
433
- thread_id: ActivityProcessorKey.fields.threadId,
434
- format_version: Schema.Int,
435
- through_sequence: ActivityProgress.fields.throughSequence,
436
- epoch: ActivityProgress.fields.epoch,
437
- owner: ActivityProgress.fields.owner,
438
- lease_expires_at: ActivityProgress.fields.leaseExpiresAt,
439
- progress_json: StoredJson$1
440
- }) {};
441
- var ActivityChangeCountRow = class extends Schema.Class("@effect-agent/storage-sqlite/ActivityChangeCountRow")({ changed: Schema.Int }) {};
442
- const StoredVersionHeader = Schema.Struct({ version: Schema.Int });
443
- const storeError$1 = (operation, reason = "unavailable") => ActivityStoreError.make({
444
- operation,
445
- reason
446
- });
447
- const query = (effect, operation) => effect.pipe(Effect.mapError(() => storeError$1(operation)));
448
- const decodeRows$3 = Effect.fn("SqliteActivityStore.decodeRows")(function* (schema, rows, operation) {
449
- return yield* Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(Effect.mapError(() => storeError$1(operation, "corrupt")));
450
- });
451
- const decodeInput$1 = Effect.fn("SqliteActivityStore.decodeInput")(function* (schema, value, operation) {
452
- return yield* Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => storeError$1(operation, "invalid-input")));
453
- });
454
- const encodeProgress = Effect.fn("SqliteActivityStore.encodeProgress")(function* (progress, operation) {
455
- return yield* Schema.encodeEffect(Schema.fromJsonString(ActivityProgress))(progress).pipe(Effect.mapError(() => storeError$1(operation, "corrupt")), Effect.flatMap((encoded) => Schema.decodeEffect(StoredJson$1)(encoded).pipe(Effect.mapError(() => storeError$1(operation, "invalid-input")))));
456
- });
457
- const decodeProgress = Effect.fn("SqliteActivityStore.decodeProgress")(function* (value, operation) {
458
- if ((yield* Schema.decodeEffect(Schema.fromJsonString(StoredVersionHeader))(value).pipe(Effect.mapError(() => storeError$1(operation, "corrupt")))).version !== STORAGE_VERSION) return yield* storeError$1(operation, "incompatible");
459
- const progress = yield* Schema.decodeEffect(Schema.fromJsonString(ActivityProgress))(value).pipe(Effect.mapError(() => storeError$1(operation, "corrupt")));
460
- if ((yield* encodeProgress(progress, operation)) !== value) return yield* storeError$1(operation, "corrupt");
461
- return progress;
462
- });
463
- const validateProgress = Effect.fn("SqliteActivityStore.validateProgress")(function* (progress, operation) {
464
- if (progress.epoch < 1 || progress.owner === null && progress.leaseExpiresAt !== 0 || progress.pending !== null && (!sameKey(progress.pending.key, progress.key) || progress.pending.sequence !== progress.throughSequence + 1)) return yield* storeError$1(operation, "corrupt");
465
- return progress;
466
- });
467
- const makeClaim = (progress) => progress.owner === null ? null : ActivityClaim.make({
468
- key: progress.key,
469
- owner: progress.owner,
470
- epoch: progress.epoch,
471
- throughSequence: progress.throughSequence,
472
- leaseExpiresAt: progress.leaseExpiresAt,
473
- pending: progress.pending
474
- });
475
- const ownershipLost = (claim) => ActivityOwnershipLost.make({
476
- key: claim.key,
477
- owner: claim.owner,
478
- epoch: claim.epoch
479
- });
480
- const makeActivityStore = Effect.fn("SqliteActivityStore.make")(function* () {
481
- const sql = yield* SqlClient.SqlClient;
482
- const failpoint = yield* ActivityMutationFailpoint;
483
- yield* failpoint.hit("activity:initialize:before");
484
- yield* sql.withTransaction(Effect.gen(function* () {
485
- yield* sql`
486
- CREATE TABLE IF NOT EXISTS effect_agent_activity_metadata (
487
- component TEXT PRIMARY KEY NOT NULL,
488
- version INTEGER NOT NULL
489
- )
490
- `;
491
- const metadataRows = yield* sql`
492
- SELECT version FROM effect_agent_activity_metadata
493
- WHERE component = ${METADATA_COMPONENT}
494
- `;
495
- const metadata = yield* decodeRows$3(ActivityMetadataRow, metadataRows, "decode activity schema version");
496
- if (metadata.length > 1) return yield* storeError$1("decode activity schema version", "corrupt");
497
- const currentVersion = metadata[0]?.version;
498
- if (currentVersion !== void 0 && currentVersion !== STORAGE_VERSION) return yield* storeError$1("initialize activity schema", "incompatible");
499
- if (currentVersion === void 0) {
500
- const tableRows = yield* sql`
501
- SELECT name FROM sqlite_master
502
- WHERE type = 'table' AND name = ${STATE_TABLE}
503
- `;
504
- if ((yield* decodeRows$3(ActivityTableRow, tableRows, "inspect activity schema")).length > 0) return yield* storeError$1("initialize activity schema", "incompatible");
505
- yield* sql`
506
- CREATE TABLE effect_agent_activity_processor_state_v1 (
507
- processor_id TEXT NOT NULL,
508
- processor_version TEXT NOT NULL,
509
- thread_id TEXT NOT NULL,
510
- format_version INTEGER NOT NULL,
511
- through_sequence INTEGER NOT NULL,
512
- epoch INTEGER NOT NULL,
513
- owner TEXT,
514
- lease_expires_at REAL NOT NULL,
515
- progress_json TEXT NOT NULL,
516
- PRIMARY KEY (processor_id, processor_version, thread_id)
517
- )
518
- `;
519
- yield* sql`
520
- INSERT INTO effect_agent_activity_metadata (component, version)
521
- VALUES (${METADATA_COMPONENT}, ${STORAGE_VERSION})
522
- `;
523
- }
524
- yield* sql`
525
- SELECT processor_id, processor_version, thread_id, format_version,
526
- through_sequence, epoch, owner, lease_expires_at, progress_json
527
- FROM effect_agent_activity_processor_state_v1
528
- LIMIT 0
529
- `;
530
- })).pipe(Effect.catchTag("SqlError", () => Effect.fail(storeError$1("initialize activity schema"))));
531
- yield* failpoint.hit("activity:initialize:after");
532
- const readProgress = Effect.fn("SqliteActivityStore.readProgress")(function* (key, operation) {
533
- const rawRows = yield* query(sql`
534
- SELECT processor_id, processor_version, thread_id, format_version,
535
- through_sequence, epoch, owner, lease_expires_at, progress_json
536
- FROM effect_agent_activity_processor_state_v1
537
- WHERE processor_id = ${key.processorId}
538
- AND processor_version = ${key.processorVersion}
539
- AND thread_id = ${key.threadId}
540
- `, operation);
541
- const rows = yield* decodeRows$3(ActivityStateRow, rawRows, operation);
542
- if (rows.length === 0) return null;
543
- if (rows.length !== 1) return yield* storeError$1(operation, "corrupt");
544
- const row = rows[0];
545
- if (row.format_version !== STORAGE_VERSION) return yield* storeError$1(operation, "incompatible");
546
- const progress = yield* decodeProgress(row.progress_json, operation);
547
- yield* validateProgress(progress, operation);
548
- if (!sameKey(progress.key, key) || row.processor_id !== key.processorId || row.processor_version !== key.processorVersion || row.thread_id !== key.threadId || row.through_sequence !== progress.throughSequence || row.epoch !== progress.epoch || row.owner !== progress.owner || row.lease_expires_at !== progress.leaseExpiresAt) return yield* storeError$1(operation, "corrupt");
549
- return progress;
550
- });
551
- const checkChanged = Effect.fn("SqliteActivityStore.checkChanged")(function* (operation) {
552
- const rawRows = yield* query(sql`SELECT changes() AS changed`, operation);
553
- const rows = yield* decodeRows$3(ActivityChangeCountRow, rawRows, operation);
554
- if (rows.length !== 1 || rows[0].changed !== 1) return yield* storeError$1(operation, "corrupt");
555
- });
556
- const insertProgress = Effect.fn("SqliteActivityStore.insertProgress")(function* (progress, operation) {
557
- const progressJson = yield* encodeProgress(progress, operation);
558
- yield* sql`
559
- INSERT INTO effect_agent_activity_processor_state_v1 (
560
- processor_id, processor_version, thread_id, format_version, through_sequence,
561
- epoch, owner, lease_expires_at, progress_json
562
- ) VALUES (
563
- ${progress.key.processorId}, ${progress.key.processorVersion}, ${progress.key.threadId},
564
- ${STORAGE_VERSION}, ${progress.throughSequence}, ${progress.epoch}, ${progress.owner},
565
- ${progress.leaseExpiresAt}, ${progressJson}
566
- )
567
- `;
568
- yield* checkChanged(operation);
569
- });
570
- const updateProgress = Effect.fn("SqliteActivityStore.updateProgress")(function* (current, next, operation) {
571
- const progressJson = yield* encodeProgress(next, operation);
572
- yield* sql`
573
- UPDATE effect_agent_activity_processor_state_v1
574
- SET format_version = ${STORAGE_VERSION},
575
- through_sequence = ${next.throughSequence},
576
- epoch = ${next.epoch},
577
- owner = ${next.owner},
578
- lease_expires_at = ${next.leaseExpiresAt},
579
- progress_json = ${progressJson}
580
- WHERE processor_id = ${current.key.processorId}
581
- AND processor_version = ${current.key.processorVersion}
582
- AND thread_id = ${current.key.threadId}
583
- AND through_sequence = ${current.throughSequence}
584
- AND epoch = ${current.epoch}
585
- `;
586
- yield* checkChanged(operation);
587
- });
588
- const requireLive = Effect.fn("SqliteActivityStore.requireLive")(function* (progress, claim, requireSequence) {
589
- const now = yield* Clock.currentTimeMillis;
590
- if (progress === null || !sameKey(progress.key, claim.key) || progress.owner !== claim.owner || progress.epoch !== claim.epoch || progress.leaseExpiresAt <= now || requireSequence && progress.throughSequence !== claim.throughSequence) return yield* ownershipLost(claim);
591
- return progress;
592
- });
593
- const inspect = Effect.fn("SqliteActivityStore.inspect")(function* (key) {
594
- const decodedKey = yield* decodeInput$1(ActivityProcessorKey, key, "inspect activity progress");
595
- return yield* readProgress(decodedKey, "inspect activity progress");
596
- });
597
- const claim = Effect.fn("SqliteActivityStore.claim")(function* (request) {
598
- const operation = "claim activity progress";
599
- const decoded = yield* decodeInput$1(ActivityClaimRequest, request, operation);
600
- yield* failpoint.hit("activity:claim:before");
601
- const claimed = yield* sql.withTransaction(Effect.gen(function* () {
602
- const current = yield* readProgress(decoded.key, operation);
603
- const now = yield* Clock.currentTimeMillis;
604
- if (current !== null && current.owner !== null && current.leaseExpiresAt > now) return yield* ActivityBusy.make({
605
- key: decoded.key,
606
- leaseExpiresAt: current.leaseExpiresAt
607
- });
608
- const next = yield* Schema.decodeUnknownEffect(ActivityProgress)({
609
- version: STORAGE_VERSION,
610
- key: decoded.key,
611
- throughSequence: current?.throughSequence ?? 0,
612
- epoch: (current?.epoch ?? 0) + 1,
613
- owner: decoded.owner,
614
- leaseExpiresAt: now + decoded.leaseMillis,
615
- pending: current?.pending ?? null,
616
- advancedAt: current?.advancedAt ?? null
617
- }).pipe(Effect.mapError(() => storeError$1(operation, "corrupt")));
618
- if (current === null) yield* insertProgress(next, operation);
619
- else yield* updateProgress(current, next, operation);
620
- yield* failpoint.hit("activity:claim:after-state");
621
- const result = makeClaim(next);
622
- if (result === null) return yield* storeError$1(operation, "corrupt");
623
- return result;
624
- })).pipe(Effect.catchTag("SqlError", () => Effect.fail(storeError$1(operation))));
625
- yield* failpoint.hit("activity:claim:after");
626
- return claimed;
627
- });
628
- const prepare = Effect.fn("SqliteActivityStore.prepare")(function* (request) {
629
- const operation = "prepare activity output";
630
- const claim = yield* decodeInput$1(ActivityClaim, request.claim, operation);
631
- const work = yield* decodeInput$1(PreparedActivity, request.work, operation);
632
- yield* failpoint.hit("activity:prepare:before");
633
- const result = yield* sql.withTransaction(Effect.gen(function* () {
634
- const current = yield* requireLive(yield* readProgress(claim.key, operation), claim, true);
635
- if (current.pending !== null) {
636
- if (sameWork(current.pending, work)) return {
637
- work: current.pending,
638
- changed: false
639
- };
640
- return yield* ActivityWorkConflict.make({
641
- key: claim.key,
642
- workId: work.workId
643
- });
644
- }
645
- if (!sameKey(work.key, claim.key) || work.sequence !== current.throughSequence + 1) return yield* ActivityWorkConflict.make({
646
- key: claim.key,
647
- workId: work.workId
648
- });
649
- const next = ActivityProgress.make({
650
- ...current,
651
- pending: work
652
- });
653
- yield* updateProgress(current, next, operation);
654
- yield* failpoint.hit("activity:prepare:after-state");
655
- return {
656
- work,
657
- changed: true
658
- };
659
- })).pipe(Effect.catchTag("SqlError", () => Effect.fail(storeError$1(operation))));
660
- if (result.changed) yield* failpoint.hit("activity:prepare:after");
661
- return result.work;
662
- });
663
- const advance = Effect.fn("SqliteActivityStore.advance")(function* (request) {
664
- const operation = "advance activity progress";
665
- const claim = yield* decodeInput$1(ActivityClaim, request.claim, operation);
666
- const workId = yield* decodeInput$1(Digest, request.workId, operation);
667
- yield* failpoint.hit("activity:advance:before");
668
- const nextClaim = yield* sql.withTransaction(Effect.gen(function* () {
669
- const current = yield* requireLive(yield* readProgress(claim.key, operation), claim, true);
670
- if (current.pending === null || current.pending.workId !== workId) return yield* ActivityWorkConflict.make({
671
- key: claim.key,
672
- workId
673
- });
674
- const next = ActivityProgress.make({
675
- ...current,
676
- throughSequence: current.pending.sequence,
677
- pending: null,
678
- advancedAt: yield* Clock.currentTimeMillis
679
- });
680
- yield* updateProgress(current, next, operation);
681
- yield* failpoint.hit("activity:advance:after-state");
682
- const result = makeClaim(next);
683
- if (result === null) return yield* storeError$1(operation, "corrupt");
684
- return result;
685
- })).pipe(Effect.catchTag("SqlError", () => Effect.fail(storeError$1(operation))));
686
- yield* failpoint.hit("activity:advance:after");
687
- return nextClaim;
688
- });
689
- const release = Effect.fn("SqliteActivityStore.release")(function* (claim) {
690
- const operation = "release activity claim";
691
- const decoded = yield* decodeInput$1(ActivityClaim, claim, operation);
692
- yield* failpoint.hit("activity:release:before");
693
- yield* sql.withTransaction(Effect.gen(function* () {
694
- const current = yield* readProgress(decoded.key, operation);
695
- if (current === null || current.owner !== decoded.owner || current.epoch !== decoded.epoch) return yield* ownershipLost(decoded);
696
- const next = ActivityProgress.make({
697
- ...current,
698
- owner: null,
699
- leaseExpiresAt: 0
700
- });
701
- yield* updateProgress(current, next, operation);
702
- yield* failpoint.hit("activity:release:after-state");
703
- })).pipe(Effect.catchTag("SqlError", () => Effect.fail(storeError$1(operation))));
704
- yield* failpoint.hit("activity:release:after");
705
- });
706
- return ActivityProcessorStore.of({
707
- inspect,
708
- claim,
709
- prepare,
710
- advance,
711
- release
712
- });
713
- });
714
- /** SQLite activity progress with mutation failpoints kept injectable for recovery tests. */
715
- const activityProcessorStoreLayerWithFailpoints = Layer.effect(ActivityProcessorStore, makeActivityStore());
716
- /** SQLite activity progress with the production no-op mutation failpoint. */
717
- const activityProcessorStoreLayer = activityProcessorStoreLayerWithFailpoints.pipe(Layer.provide(ActivityMutationFailpoint.layer));
718
- //#endregion
719
- //#region src/sqlite-storage-config.ts
720
- const ObservationPollInterval = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
721
- const BusyTimeoutMillis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
722
- const OwnershipLeaseMillis = Schema.Int.check(Schema.isGreaterThan(0));
723
- /**
724
- * Validated construction configuration consumed by the SQLite storage Layer. The database
725
- * identity itself belongs to the SqlClient Layer; duplicating it here could silently diverge
726
- * from the connection actually in use.
727
- */
728
- var SqliteStorageConfigValue = class extends Schema.Class("@effect-agent/storage-sqlite/SqliteStorageConfigValue")({
729
- observationPollInterval: ObservationPollInterval,
730
- /** Bounded SQLITE_BUSY retry window for write-lock acquisition, in milliseconds. */
731
- busyTimeout: BusyTimeoutMillis,
732
- /**
733
- * Submission ownership lease duration in milliseconds (D5). The lease is a liveness hint
734
- * that makes an abandoned claim reclaimable; correctness never depends on it because every
735
- * canonical append is fenced by producer epoch. Convenience layers default this to
736
- * `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/thread`.
737
- */
738
- ownershipLeaseDuration: OwnershipLeaseMillis,
739
- /**
740
- * Re-verify every stored payload and digest chain while opening the store. Per-operation
741
- * Schema decoding and the digest chain already fail clearly on corrupt rows, so the full
742
- * scan is an explicit opt-in integrity audit rather than a startup requirement.
743
- */
744
- verifyOnOpen: Schema.Boolean
745
- }) {};
746
- /** Explicit SQLite storage configuration authority. */
747
- var SqliteStorageConfig = class extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageConfig") {};
748
- //#endregion
749
- //#region src/sqlite-journal.ts
750
- const BoundedStoredText$1 = Schema.String.check(Schema.isMaxLength(16777216));
751
- const BoundedIdentifier$1 = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
752
- const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
753
- const MAX_RECORDS_PER_THREAD = 65536;
754
- const MAX_STORED_TEXT_BYTES = 16777216;
755
- const MAX_IDENTIFIER_LENGTH = 1024;
756
- const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
757
- var SqliteVersionRow = class extends Schema.Class("SqliteVersionRow")({ user_version: NonNegativeInt }) {};
758
- var SqliteJournalModeRow = class extends Schema.Class("SqliteJournalModeRow")({ journal_mode: Schema.NonEmptyString.check(Schema.isMaxLength(32)) }) {};
759
- var SqliteNameRow = class extends Schema.Class("SqliteNameRow")({ name: BoundedIdentifier$1 }) {};
760
- var ThreadRow = class extends Schema.Class("ThreadRow")({
761
- thread_id: BoundedIdentifier$1,
762
- created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
763
- producer_epoch: ProducerEpoch,
764
- tail_digest: BoundedStoredText$1,
765
- tail_sequence: CanonicalSequence
766
- }) {};
767
- var BatchRow = class extends Schema.Class("BatchRow")({
768
- batch_digest: BoundedStoredText$1,
769
- batch_id: BoundedIdentifier$1,
770
- batch_json: BoundedStoredText$1,
771
- thread_id: BoundedIdentifier$1,
772
- first_sequence: CanonicalSequence,
773
- last_sequence: CanonicalSequence,
774
- tail_digest: BoundedStoredText$1
775
- }) {};
776
- var RecordRow = class extends Schema.Class("RecordRow")({
777
- batch_id: BoundedIdentifier$1,
778
- thread_id: BoundedIdentifier$1,
779
- record_id: BoundedIdentifier$1,
780
- record_json: BoundedStoredText$1,
781
- sequence: CanonicalSequence
782
- }) {};
783
- var CheckpointRow = class extends Schema.Class("CheckpointRow")({
784
- checkpoint_json: BoundedStoredText$1,
785
- thread_id: BoundedIdentifier$1,
786
- tail_digest: BoundedStoredText$1,
787
- through_sequence: CanonicalSequence
788
- }) {};
789
- var RawRecord = class extends Schema.Class("@effect-agent/storage-sqlite/RawRecord")({
790
- recordId: BoundedIdentifier$1,
791
- recordJson: BoundedStoredText$1
792
- }) {};
793
- var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-sqlite/RawAppendRequest")({
794
- batchDigest: BoundedStoredText$1,
795
- batchId: BoundedIdentifier$1,
796
- batchJson: BoundedStoredText$1,
797
- threadId: BoundedIdentifier$1,
798
- expectedTailDigest: BoundedStoredText$1,
799
- expectedTailSequence: CanonicalSequence,
800
- producerEpoch: ProducerEpoch,
801
- records: Schema.NonEmptyArray(RawRecord).check(Schema.isMaxLength(256)),
802
- tailDigest: BoundedStoredText$1
803
- }) {};
804
- var RawAppendResult = class extends Schema.Class("@effect-agent/storage-sqlite/RawAppendResult")({
805
- firstSequence: CanonicalSequence,
806
- lastSequence: CanonicalSequence,
807
- replayed: Schema.Boolean,
808
- tailDigest: BoundedStoredText$1
809
- }) {};
810
- var RawReadRequest = class extends Schema.Class("@effect-agent/storage-sqlite/RawReadRequest")({
811
- threadId: BoundedIdentifier$1,
812
- fromSequenceExclusive: CanonicalSequence,
813
- limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
814
- }) {};
815
- var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-sqlite/RawCheckpoint")({
816
- checkpointJson: BoundedStoredText$1,
817
- threadId: BoundedIdentifier$1,
818
- tailDigest: BoundedStoredText$1,
819
- throughSequence: CanonicalSequence
820
- }) {};
821
- var RawThreadExport = class extends Schema.Class("@effect-agent/storage-sqlite/RawThreadExport")({
822
- batches: Schema.Array(BatchRow),
823
- checkpoints: Schema.Array(CheckpointRow),
824
- thread: ThreadRow,
825
- records: Schema.Array(RecordRow)
826
- }) {};
827
- const storageError = (operation) => (error) => SqliteStorageError.make({
828
- cause: error,
829
- operation,
830
- message: error.message
831
- });
832
- /** Decode raw SQLite rows against a Schema, reporting failures as typed corruption. */
833
- const decodeRows$2 = Effect.fn("SqliteJournal.decodeRows")((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
834
- table,
835
- rowKey,
836
- message: String(error)
837
- }))));
838
- /** Decode exactly one raw SQLite row against a Schema, reporting failures as typed corruption. */
839
- const decodeSingleRow = Effect.fn("SqliteJournal.decodeSingleRow")((schema, table, rowKey, rows) => decodeRows$2(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(SqliteStorageCorruptionError.make({
840
- table,
841
- rowKey,
842
- message: `Expected exactly one row but found ${decoded.length}.`
843
- })))));
844
- const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function* () {
845
- const sql = yield* SqlClient.SqlClient;
846
- const { hit: failpoint } = yield* SqliteStorageFailpoint;
847
- const { busyTimeout } = yield* SqliteStorageConfig;
848
- yield* sql`PRAGMA foreign_keys = ON`.pipe(Effect.mapError(storageError("enable foreign keys")));
849
- yield* sql.unsafe(`PRAGMA busy_timeout = ${busyTimeout}`).pipe(Effect.mapError(storageError("configure busy timeout")));
850
- const journalModeRows = yield* sql`PRAGMA journal_mode`.pipe(Effect.mapError(storageError("read journal mode")));
851
- const journalMode = yield* decodeSingleRow(Schema.Array(SqliteJournalModeRow), "pragma_journal_mode", "singleton", journalModeRows);
852
- if (journalMode.journal_mode.toLowerCase() !== "wal") return yield* SqliteStorageCompatibilityError.make({
853
- actualVersion: 0,
854
- supportedVersion: 7,
855
- message: `SQLite WAL mode is required; the database reported ${journalMode.journal_mode}.`
856
- });
857
- const versionRows = yield* sql`PRAGMA user_version`.pipe(Effect.mapError(storageError("read storage version")));
858
- const version = yield* decodeSingleRow(Schema.Array(SqliteVersionRow), "pragma_user_version", "singleton", versionRows);
859
- if (version.user_version !== 0 && version.user_version !== 7) return yield* SqliteStorageCompatibilityError.make({
860
- actualVersion: version.user_version,
861
- supportedVersion: 7,
862
- message: `The SQLite file uses private-development storage version ${version.user_version}; this build supports exactly version 7. Reset the database file explicitly; automatic stored-data migrations are not provided during private development.`
863
- });
864
- if (version.user_version === 0) {
865
- const existingRows = yield* sql`
866
- SELECT name
867
- FROM sqlite_master
868
- WHERE type = 'table'
869
- AND name LIKE 'effect_agent_%'
870
- ORDER BY name
871
- `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
872
- if ((yield* decodeRows$2(Schema.Array(SqliteNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* SqliteStorageCompatibilityError.make({
873
- actualVersion: 0,
874
- supportedVersion: 7,
875
- message: "The SQLite file contains unversioned Effect Agent tables. Reset it explicitly; refusing to mutate ambiguous stored data."
876
- });
877
- yield* SqliteMigrator.run({ loader: sqliteMigrations }).pipe(Effect.mapError((error) => SqliteStorageError.make({
878
- cause: error,
879
- operation: "initialize current storage",
880
- message: error.message
881
- })));
882
- }
883
- const requiredRows = yield* sql`
884
- SELECT name
885
- FROM sqlite_master
886
- WHERE type = 'table'
887
- AND name IN (
888
- 'effect_agent_threads',
889
- 'effect_agent_canonical_batches',
890
- 'effect_agent_canonical_records',
891
- 'effect_agent_checkpoints',
892
- 'effect_agent_submissions',
893
- 'effect_agent_submission_ownership',
894
- 'effect_agent_attempts',
895
- 'effect_agent_settlement_reservations',
896
- 'effect_agent_abort_intents',
897
- 'effect_agent_approval_decisions',
898
- 'effect_agent_unknown_resolutions',
899
- 'effect_agent_schedules'
900
- )
901
- ORDER BY name
902
- `.pipe(Effect.mapError(storageError("verify storage tables")));
903
- if ((yield* decodeRows$2(Schema.Array(SqliteNameRow), "sqlite_master", "required_tables", requiredRows)).length !== 12) return yield* SqliteStorageCompatibilityError.make({
904
- actualVersion: 7,
905
- supportedVersion: 7,
906
- message: "The SQLite file claims the current format but is missing required tables. Reset the corrupt private-development data."
907
- });
908
- const classifyWriteFailure = (operation) => (error) => error.reason._tag === "LockTimeoutError" ? SqliteWriteContention.make({
909
- cause: error,
910
- operation,
911
- message: `Another producer holds the SQLite write lock; ${operation} is safe to retry.`
912
- }) : storageError(operation)(error);
913
- /**
914
- * Runs one journal write transaction under `BEGIN IMMEDIATE`. SQLite's deferred `BEGIN`
915
- * would let a read-then-write transaction start as a reader and fail with
916
- * SQLITE_BUSY_SNAPSHOT on upgrade, which `busy_timeout` never retries. Taking the write
917
- * lock up front keeps cross-owner contention inside the bounded busy retry; a lock
918
- * timeout is classified as the retryable SqliteWriteContention. A failed `BEGIN` leaves
919
- * no transaction, so no rollback is attempted for it.
920
- *
921
- * Journal write transactions are always top level. Nesting one inside another would
922
- * deadlock the single-connection client, so new journal operations must not wrap this
923
- * helper inside another transaction.
924
- */
925
- const withWriteTransaction = (operation) => (effect) => Effect.uninterruptibleMask((restore) => Effect.scoped(Effect.gen(function* () {
926
- const connection = yield* sql.reserve.pipe(Effect.mapError(classifyWriteFailure(operation)));
927
- yield* connection.executeUnprepared("BEGIN IMMEDIATE", [], void 0).pipe(Effect.mapError(classifyWriteFailure(operation)));
928
- const exit = yield* restore(Effect.provideService(effect, sql.transactionService, [connection, 0])).pipe(Effect.exit);
929
- if (Exit.isSuccess(exit)) {
930
- yield* connection.executeUnprepared("COMMIT", [], void 0).pipe(Effect.mapError(classifyWriteFailure(operation)));
931
- return exit.value;
932
- }
933
- yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], void 0));
934
- return yield* exit;
935
- })).pipe(Effect.withSpan("SqliteJournal.withWriteTransaction", { attributes: { operation } })));
936
- /**
937
- * Runs a read-only snapshot under a deferred transaction. Effect's SQLite client now
938
- * starts every writable-client `withTransaction` with `BEGIN IMMEDIATE`, which is the
939
- * right default for mutations but would make exports take the write lock and block a
940
- * concurrent append. Reserving the connection and beginning explicitly preserves the
941
- * adapter's snapshot-with-concurrent-writer contract. As with the write helper, a failed
942
- * `BEGIN` is reported directly because there is no transaction to roll back.
943
- */
944
- const withReadTransaction = (operation) => (effect) => Effect.uninterruptibleMask((restore) => Effect.scoped(Effect.gen(function* () {
945
- const connection = yield* sql.reserve.pipe(Effect.mapError(storageError(operation)));
946
- yield* connection.executeUnprepared("BEGIN", [], void 0).pipe(Effect.mapError(storageError(operation)));
947
- const exit = yield* restore(Effect.provideService(effect, sql.transactionService, [connection, 0])).pipe(Effect.exit);
948
- if (Exit.isSuccess(exit)) {
949
- yield* connection.executeUnprepared("COMMIT", [], void 0).pipe(Effect.mapError(storageError(operation)));
950
- return exit.value;
951
- }
952
- yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], void 0));
953
- return yield* exit;
954
- })).pipe(Effect.withSpan("SqliteJournal.withReadTransaction", { attributes: { operation } })));
955
- const materialize = Effect.fn("SqliteJournal.materialize")(function* (threadId, createdAt, emptyTailDigest, producerEpoch) {
956
- if (threadId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(emptyTailDigest) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
957
- operation: "materialize thread",
958
- message: "Thread identity or initial digest exceeds the SQLite storage bounds."
959
- });
960
- yield* withWriteTransaction("materialize transaction")(Effect.gen(function* () {
961
- const existingRows = yield* sql`
962
- SELECT
963
- thread_id,
964
- created_at,
965
- tail_sequence,
966
- tail_digest,
967
- producer_epoch
968
- FROM effect_agent_threads
969
- WHERE thread_id = ${threadId}
970
- `.pipe(Effect.mapError(storageError("read materialized thread")));
971
- const existing = yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, existingRows);
972
- if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
973
- table: "effect_agent_threads",
974
- rowKey: threadId,
975
- message: "A thread primary key returned more than one row."
976
- });
977
- if (existing.length === 0) {
978
- yield* sql`
979
- INSERT INTO effect_agent_threads (
980
- thread_id,
981
- created_at,
982
- tail_sequence,
983
- tail_digest,
984
- producer_epoch
985
- ) VALUES (
986
- ${threadId},
987
- ${createdAt},
988
- 0,
989
- ${emptyTailDigest},
990
- ${producerEpoch}
991
- )
992
- `.pipe(Effect.mapError(storageError("materialize thread")));
993
- return;
994
- }
995
- if (producerEpoch < existing[0].producer_epoch) return yield* SqliteFenceRejected.make({
996
- producerEpoch,
997
- actualEpoch: existing[0].producer_epoch,
998
- message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
999
- });
1000
- if (producerEpoch > existing[0].producer_epoch) yield* sql`
1001
- UPDATE effect_agent_threads
1002
- SET producer_epoch = ${producerEpoch}
1003
- WHERE thread_id = ${threadId}
1004
- `.pipe(Effect.mapError(storageError("advance materialization epoch")));
1005
- }));
1006
- });
1007
- const getThread = Effect.fn("SqliteJournal.getThread")(function* (threadId) {
1008
- const rows = yield* sql`
1009
- SELECT
1010
- thread_id,
1011
- created_at,
1012
- tail_sequence,
1013
- tail_digest,
1014
- producer_epoch
1015
- FROM effect_agent_threads
1016
- WHERE thread_id = ${threadId}
1017
- `.pipe(Effect.mapError(storageError("read thread")));
1018
- return yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
1019
- });
1020
- const append = Effect.fn("SqliteJournal.append")(function* (request) {
1021
- if (request.threadId.length > MAX_IDENTIFIER_LENGTH || request.batchId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(request.batchJson) > MAX_STORED_TEXT_BYTES || storedTextBytes(request.batchDigest) > MAX_STORED_TEXT_BYTES || storedTextBytes(request.tailDigest) > MAX_STORED_TEXT_BYTES || request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(record.recordJson) > MAX_STORED_TEXT_BYTES)) return yield* SqliteStorageError.make({
1022
- operation: "append canonical batch",
1023
- message: "Canonical identifiers or encoded JSON exceed the SQLite storage bounds."
1024
- });
1025
- return yield* withWriteTransaction("append transaction")(Effect.gen(function* () {
1026
- const recordIds = request.records.map((record) => record.recordId);
1027
- if (new Set(recordIds).size !== recordIds.length) return yield* SqliteAppendConflict.make({
1028
- message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
1029
- reason: "record-identity"
1030
- });
1031
- const threadRows = yield* sql`
1032
- SELECT
1033
- thread_id,
1034
- created_at,
1035
- tail_sequence,
1036
- tail_digest,
1037
- producer_epoch
1038
- FROM effect_agent_threads
1039
- WHERE thread_id = ${request.threadId}
1040
- `.pipe(Effect.mapError(storageError("read append tail")));
1041
- const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", request.threadId, threadRows);
1042
- if (request.producerEpoch !== thread.producer_epoch) return yield* SqliteFenceRejected.make({
1043
- producerEpoch: request.producerEpoch,
1044
- actualEpoch: thread.producer_epoch,
1045
- message: `Producer epoch ${request.producerEpoch} is not the current epoch ${thread.producer_epoch}.`
1046
- });
1047
- const batchRows = yield* sql`
1048
- SELECT
1049
- thread_id,
1050
- batch_id,
1051
- first_sequence,
1052
- last_sequence,
1053
- batch_digest,
1054
- tail_digest,
1055
- batch_json
1056
- FROM effect_agent_canonical_batches
1057
- WHERE thread_id = ${request.threadId}
1058
- AND batch_id = ${request.batchId}
1059
- `.pipe(Effect.mapError(storageError("read idempotent batch")));
1060
- const batches = yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.threadId}/${request.batchId}`, batchRows);
1061
- if (batches.length > 1) return yield* SqliteStorageCorruptionError.make({
1062
- table: "effect_agent_canonical_batches",
1063
- rowKey: `${request.threadId}/${request.batchId}`,
1064
- message: "A canonical batch primary key returned more than one row."
1065
- });
1066
- if (batches.length === 1) {
1067
- const existing = batches[0];
1068
- if (existing.batch_digest !== request.batchDigest) return yield* SqliteAppendConflict.make({
1069
- message: `Batch ${request.batchId} already exists with different canonical content.`,
1070
- reason: "batch-digest"
1071
- });
1072
- return RawAppendResult.make({
1073
- firstSequence: existing.first_sequence,
1074
- lastSequence: existing.last_sequence,
1075
- replayed: true,
1076
- tailDigest: existing.tail_digest
1077
- });
1078
- }
1079
- if (request.expectedTailSequence !== thread.tail_sequence || request.expectedTailDigest !== thread.tail_digest) return yield* SqliteAppendConflict.make({
1080
- message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${thread.tail_sequence}/${thread.tail_digest}.`,
1081
- reason: "tail",
1082
- actualTailSequence: thread.tail_sequence,
1083
- actualTailDigest: thread.tail_digest
1084
- });
1085
- if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) return yield* SqliteStorageError.make({
1086
- operation: "append canonical batch",
1087
- message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`
1088
- });
1089
- const existingRecordRows = yield* sql`
1090
- SELECT
1091
- thread_id,
1092
- sequence,
1093
- record_id,
1094
- batch_id,
1095
- record_json
1096
- FROM effect_agent_canonical_records
1097
- WHERE thread_id = ${request.threadId}
1098
- AND record_id IN ${sql.in(recordIds)}
1099
- ORDER BY sequence
1100
- `.pipe(Effect.mapError(storageError("check canonical record identities")));
1101
- const existingRecords = yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/record_ids`, existingRecordRows);
1102
- if (existingRecords.length > 0) return yield* SqliteAppendConflict.make({
1103
- message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
1104
- reason: "record-identity"
1105
- });
1106
- const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(thread.tail_sequence + 1).pipe(Effect.mapError((error) => SqliteStorageError.make({
1107
- cause: error,
1108
- operation: "append canonical batch",
1109
- message: error.message
1110
- })));
1111
- const lastSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(firstSequence + request.records.length - 1).pipe(Effect.mapError((error) => SqliteStorageError.make({
1112
- cause: error,
1113
- operation: "append canonical batch",
1114
- message: error.message
1115
- })));
1116
- yield* sql`
1117
- INSERT INTO effect_agent_canonical_batches (
1118
- thread_id,
1119
- batch_id,
1120
- first_sequence,
1121
- last_sequence,
1122
- batch_digest,
1123
- tail_digest,
1124
- batch_json
1125
- ) VALUES (
1126
- ${request.threadId},
1127
- ${request.batchId},
1128
- ${firstSequence},
1129
- ${lastSequence},
1130
- ${request.batchDigest},
1131
- ${request.tailDigest},
1132
- ${request.batchJson}
1133
- )
1134
- `.pipe(Effect.mapError(storageError("insert canonical batch")));
1135
- yield* failpoint("append:after-batch-insert");
1136
- yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
1137
- yield* sql`
1138
- INSERT INTO effect_agent_canonical_records (
1139
- thread_id,
1140
- sequence,
1141
- record_id,
1142
- batch_id,
1143
- record_json
1144
- ) VALUES (
1145
- ${request.threadId},
1146
- ${firstSequence + index},
1147
- ${record.recordId},
1148
- ${request.batchId},
1149
- ${record.recordJson}
1150
- )
1151
- `.pipe(Effect.mapError(storageError("insert canonical record")));
1152
- yield* failpoint("append:after-record-insert");
1153
- }), { discard: true });
1154
- yield* sql`
1155
- UPDATE effect_agent_threads
1156
- SET
1157
- tail_sequence = ${lastSequence},
1158
- tail_digest = ${request.tailDigest},
1159
- producer_epoch = ${request.producerEpoch}
1160
- WHERE thread_id = ${request.threadId}
1161
- `.pipe(Effect.mapError(storageError("advance thread tail")));
1162
- yield* failpoint("append:after-tail-update");
1163
- return RawAppendResult.make({
1164
- firstSequence,
1165
- lastSequence,
1166
- replayed: false,
1167
- tailDigest: request.tailDigest
1168
- });
1169
- }));
1170
- });
1171
- const read = Effect.fn("SqliteJournal.read")(function* (request) {
1172
- const rows = yield* sql`
1173
- SELECT
1174
- thread_id,
1175
- sequence,
1176
- record_id,
1177
- batch_id,
1178
- record_json
1179
- FROM effect_agent_canonical_records
1180
- WHERE thread_id = ${request.threadId}
1181
- AND sequence > ${request.fromSequenceExclusive}
1182
- ORDER BY sequence
1183
- LIMIT ${request.limit}
1184
- `.pipe(Effect.mapError(storageError("read canonical records")));
1185
- return yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}>${request.fromSequenceExclusive}`, rows);
1186
- });
1187
- const exportThread = Effect.fn("SqliteJournal.exportThread")(function* (threadId) {
1188
- return yield* withReadTransaction("export transaction")(Effect.gen(function* () {
1189
- const threadRows = yield* sql`
1190
- SELECT
1191
- thread_id,
1192
- created_at,
1193
- tail_sequence,
1194
- tail_digest,
1195
- producer_epoch
1196
- FROM effect_agent_threads
1197
- WHERE thread_id = ${threadId}
1198
- `.pipe(Effect.mapError(storageError("export thread")));
1199
- const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", threadId, threadRows);
1200
- yield* failpoint("export:after-thread-read");
1201
- const batchRows = yield* sql`
1202
- SELECT
1203
- thread_id,
1204
- batch_id,
1205
- first_sequence,
1206
- last_sequence,
1207
- batch_digest,
1208
- tail_digest,
1209
- batch_json
1210
- FROM effect_agent_canonical_batches
1211
- WHERE thread_id = ${threadId}
1212
- ORDER BY first_sequence
1213
- `.pipe(Effect.mapError(storageError("export canonical batches")));
1214
- const recordRows = yield* sql`
1215
- SELECT
1216
- thread_id,
1217
- sequence,
1218
- record_id,
1219
- batch_id,
1220
- record_json
1221
- FROM effect_agent_canonical_records
1222
- WHERE thread_id = ${threadId}
1223
- ORDER BY sequence
1224
- `.pipe(Effect.mapError(storageError("export canonical records")));
1225
- const checkpointRows = yield* sql`
1226
- SELECT
1227
- thread_id,
1228
- through_sequence,
1229
- tail_digest,
1230
- checkpoint_json
1231
- FROM effect_agent_checkpoints
1232
- WHERE thread_id = ${threadId}
1233
- ORDER BY through_sequence
1234
- `.pipe(Effect.mapError(storageError("export checkpoints")));
1235
- return RawThreadExport.make({
1236
- thread,
1237
- batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", threadId, batchRows),
1238
- records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", threadId, recordRows),
1239
- checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", threadId, checkpointRows)
1240
- });
1241
- }));
1242
- });
1243
- const saveCheckpoint = Effect.fn("SqliteJournal.saveCheckpoint")(function* (checkpoint) {
1244
- if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(checkpoint.checkpointJson) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
1245
- operation: "save checkpoint",
1246
- message: "Checkpoint identity or encoded JSON exceeds the SQLite storage bounds."
1247
- });
1248
- yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
1249
- const threadRows = yield* sql`
1250
- SELECT
1251
- thread_id,
1252
- created_at,
1253
- tail_sequence,
1254
- tail_digest,
1255
- producer_epoch
1256
- FROM effect_agent_threads
1257
- WHERE thread_id = ${checkpoint.threadId}
1258
- `.pipe(Effect.mapError(storageError("read checkpoint tail")));
1259
- const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", checkpoint.threadId, threadRows);
1260
- if (checkpoint.throughSequence > thread.tail_sequence) return yield* SqliteCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${thread.tail_sequence}.` });
1261
- const checkpointRows = yield* sql`
1262
- SELECT
1263
- thread_id,
1264
- through_sequence,
1265
- tail_digest,
1266
- checkpoint_json
1267
- FROM effect_agent_checkpoints
1268
- WHERE thread_id = ${checkpoint.threadId}
1269
- AND through_sequence = ${checkpoint.throughSequence}
1270
- `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
1271
- const existing = yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.threadId}/${checkpoint.throughSequence}`, checkpointRows);
1272
- if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
1273
- table: "effect_agent_checkpoints",
1274
- rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
1275
- message: "A checkpoint primary key returned more than one row."
1276
- });
1277
- if (existing.length === 1) {
1278
- 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." });
1279
- return;
1280
- }
1281
- yield* sql`
1282
- INSERT INTO effect_agent_checkpoints (
1283
- thread_id,
1284
- through_sequence,
1285
- tail_digest,
1286
- checkpoint_json
1287
- ) VALUES (
1288
- ${checkpoint.threadId},
1289
- ${checkpoint.throughSequence},
1290
- ${checkpoint.tailDigest},
1291
- ${checkpoint.checkpointJson}
1292
- )
1293
- `.pipe(Effect.mapError(storageError("insert checkpoint")));
1294
- }));
1295
- });
1296
- const loadCheckpoint = Effect.fn("SqliteJournal.loadCheckpoint")(function* (threadId, atOrBeforeSequence) {
1297
- const rows = yield* sql`
1298
- SELECT
1299
- thread_id,
1300
- through_sequence,
1301
- tail_digest,
1302
- checkpoint_json
1303
- FROM effect_agent_checkpoints
1304
- WHERE thread_id = ${threadId}
1305
- AND through_sequence <= ${atOrBeforeSequence}
1306
- ORDER BY through_sequence DESC
1307
- LIMIT 1
1308
- `.pipe(Effect.mapError(storageError("load checkpoint")));
1309
- return yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${threadId}<=${atOrBeforeSequence}`, rows);
1310
- });
1311
- return {
1312
- append,
1313
- exportThread,
1314
- getThread,
1315
- getTailDigestAt: Effect.fn("SqliteJournal.getTailDigestAt")(function* (threadId, sequence) {
1316
- if (sequence === 0) {
1317
- const threads = yield* getThread(threadId);
1318
- return threads.length === 0 ? [] : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : void 0].filter((value) => value !== void 0);
1319
- }
1320
- const rows = yield* sql`
1321
- SELECT
1322
- thread_id,
1323
- batch_id,
1324
- first_sequence,
1325
- last_sequence,
1326
- batch_digest,
1327
- tail_digest,
1328
- batch_json
1329
- FROM effect_agent_canonical_batches
1330
- WHERE thread_id = ${threadId}
1331
- AND last_sequence = ${sequence}
1332
- `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
1333
- return (yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${threadId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
1334
- }),
1335
- loadCheckpoint,
1336
- materialize,
1337
- read,
1338
- saveCheckpoint,
1339
- scanStoredPayloads: Effect.fn("SqliteJournal.scanStoredPayloads")(function* () {
1340
- return yield* withReadTransaction("startup scan transaction")(Effect.gen(function* () {
1341
- const threads = yield* sql`
1342
- SELECT
1343
- thread_id,
1344
- created_at,
1345
- tail_sequence,
1346
- tail_digest,
1347
- producer_epoch
1348
- FROM effect_agent_threads
1349
- ORDER BY thread_id
1350
- `.pipe(Effect.mapError(storageError("scan threads")));
1351
- const batches = yield* sql`
1352
- SELECT
1353
- thread_id,
1354
- batch_id,
1355
- first_sequence,
1356
- last_sequence,
1357
- batch_digest,
1358
- tail_digest,
1359
- batch_json
1360
- FROM effect_agent_canonical_batches
1361
- ORDER BY thread_id, first_sequence
1362
- `.pipe(Effect.mapError(storageError("scan canonical batches")));
1363
- const records = yield* sql`
1364
- SELECT
1365
- thread_id,
1366
- sequence,
1367
- record_id,
1368
- batch_id,
1369
- record_json
1370
- FROM effect_agent_canonical_records
1371
- ORDER BY thread_id, sequence
1372
- `.pipe(Effect.mapError(storageError("scan canonical records")));
1373
- const checkpoints = yield* sql`
1374
- SELECT
1375
- thread_id,
1376
- through_sequence,
1377
- tail_digest,
1378
- checkpoint_json
1379
- FROM effect_agent_checkpoints
1380
- ORDER BY thread_id, through_sequence
1381
- `.pipe(Effect.mapError(storageError("scan checkpoints")));
1382
- return {
1383
- threads: yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", "startup_scan", threads),
1384
- batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
1385
- records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
1386
- checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
1387
- };
1388
- }));
1389
- }),
1390
- withWriteTransaction
1391
- };
1392
- });
1393
- //#endregion
1394
- //#region src/sqlite-thread-store.ts
1395
- const OffsetText = Schema.String.check(Schema.isMaxLength(4096));
1396
- const SQLITE_OFFSET_PREFIX = "effect-agent-sqlite@1:";
1397
- const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
1398
- const isDigest = Schema.is(Digest);
1399
- const isSqliteFenceRejected = Schema.is(SqliteFenceRejected);
1400
- const isSqliteAppendConflict = Schema.is(SqliteAppendConflict);
1401
- const isSqliteCheckpointConflict = Schema.is(SqliteCheckpointConflict);
1402
- const storeError = (operation, error) => ThreadStoreError.make({
1403
- cause: error,
1404
- operation,
1405
- message: error.message
1406
- });
1407
- const schemaStoreError = (operation, error) => ThreadStoreError.make({
1408
- cause: error,
1409
- operation,
1410
- message: error.message
1411
- });
1412
- const makeOffset = Effect.fn("SqliteThreadStore.makeOffset")(function* (threadId, sequence) {
1413
- return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.flatMap((validatedSequence) => Schema.decodeUnknownEffect(ObservationOffset)(`${SQLITE_OFFSET_PREFIX}${encodeURIComponent(threadId)}:${validatedSequence}`)), Effect.mapError((error) => schemaStoreError("encode observation offset", error)));
1414
- });
1415
- const parseOffset = Effect.fn("SqliteThreadStore.parseOffset")(function* (threadId, offset) {
1416
- if (offset === void 0) return ZERO_CANONICAL_SEQUENCE;
1417
- const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
1418
- const threadPrefix = `${SQLITE_OFFSET_PREFIX}${encodeURIComponent(threadId)}:`;
1419
- if (!text.startsWith(threadPrefix)) return yield* ThreadStoreError.make({
1420
- operation: "decode observation offset",
1421
- message: "The observation offset belongs to a different adapter, storage version, or Thread."
1422
- });
1423
- const sequenceText = text.slice(threadPrefix.length);
1424
- if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield* ThreadStoreError.make({
1425
- operation: "decode observation offset",
1426
- message: "The observation offset is malformed."
1427
- });
1428
- return yield* Schema.decodeUnknownEffect(CanonicalSequence)(Number(sequenceText)).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
1429
- });
1430
- const mapFence = (threadId, error) => FenceRejected.make({
1431
- threadId,
1432
- actualEpoch: error.actualEpoch,
1433
- attemptedEpoch: error.producerEpoch
1434
- });
1435
- const encodeCanonicalRecord = Effect.fn("SqliteThreadStore.encodeCanonicalRecord")(function* (record) {
1436
- return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(record).pipe(Effect.mapError((error) => schemaStoreError("encode canonical record", error)));
1437
- });
1438
- const encodeCanonicalBatch = Effect.fn("SqliteThreadStore.encodeCanonicalBatch")(function* (batch) {
1439
- return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalBatch))(batch).pipe(Effect.mapError((error) => schemaStoreError("encode canonical batch", error)));
1440
- });
1441
- const encodeCheckpoint = Effect.fn("SqliteThreadStore.encodeCheckpoint")(function* (checkpoint) {
1442
- return yield* Schema.encodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint).pipe(Effect.mapError((error) => schemaStoreError("encode checkpoint", error)));
1443
- });
1444
- const decodeEnvelope = Effect.fn("SqliteThreadStore.decodeEnvelope")(function* (row) {
1445
- const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) => ThreadStoreError.make({
1446
- operation: "decode canonical record",
1447
- message: error.message
1448
- })));
1449
- const threadId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.threadId)(row.thread_id).pipe(Effect.mapError((error) => schemaStoreError("decode thread identity", error)));
1450
- const offset = yield* makeOffset(threadId, row.sequence);
1451
- const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(row.batch_id).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
1452
- return CanonicalRecordEnvelope.make({
1453
- threadId,
1454
- batchId,
1455
- sequence: row.sequence,
1456
- offset,
1457
- record
1458
- });
1459
- });
1460
- const decodeCheckpoint = Effect.fn("SqliteThreadStore.decodeCheckpoint")(function* (checkpointJson) {
1461
- return yield* Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpointJson).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint", error)));
1462
- });
1463
- const requireThread = Effect.fn("SqliteThreadStore.requireThread")(function* (journal, threadId) {
1464
- const rows = yield* journal.getThread(threadId).pipe(Effect.mapError((error) => storeError("read thread", error)));
1465
- if (rows.length === 0) return yield* ThreadNotMaterialized.make({ threadId });
1466
- return rows[0];
1467
- });
1468
- const tailDigestAt = Effect.fn("SqliteThreadStore.tailDigestAt")(function* (journal, threadId, sequence) {
1469
- if (sequence === 0) return EMPTY_TAIL_DIGEST;
1470
- const digests = yield* journal.getTailDigestAt(threadId, sequence).pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
1471
- if (digests.length !== 1) return yield* CheckpointRejected.make({
1472
- threadId,
1473
- reason: "digest-mismatch"
1474
- });
1475
- return yield* Schema.decodeUnknownEffect(Digest)(digests[0]).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)));
1476
- });
1477
- const groupByKey = (rows, key) => {
1478
- const grouped = /* @__PURE__ */ new Map();
1479
- for (const row of rows) {
1480
- const existing = grouped.get(key(row));
1481
- if (existing === void 0) grouped.set(key(row), [row]);
1482
- else existing.push(row);
1483
- }
1484
- return grouped;
1485
- };
1486
- /**
1487
- * Opt-in full integrity audit (`verifyOnOpen`). Every stored payload is decoded, re-encoded,
1488
- * and re-digested against the canonical chain. Routine opens skip this scan: per-operation
1489
- * Schema decoding plus the digest chain already fail clearly on corrupt rows.
1490
- */
1491
- const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads")(function* (journal, crypto) {
1492
- const stored = yield* journal.scanStoredPayloads();
1493
- const batches = yield* Effect.forEach(stored.batches, (batch) => Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(Effect.map((decoded) => ({
1494
- decoded,
1495
- row: batch
1496
- })), Effect.mapError((error) => SqliteStorageCorruptionError.make({
1497
- table: "effect_agent_canonical_batches",
1498
- rowKey: `${batch.thread_id}/${batch.batch_id}`,
1499
- message: error.message
1500
- }))));
1501
- const records = yield* Effect.forEach(stored.records, (record) => Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(Effect.map((decoded) => ({
1502
- decoded,
1503
- row: record
1504
- })), Effect.mapError((error) => SqliteStorageCorruptionError.make({
1505
- table: "effect_agent_canonical_records",
1506
- rowKey: `${record.thread_id}/${record.sequence}`,
1507
- message: error.message
1508
- }))));
1509
- const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(Effect.map((decoded) => ({
1510
- decoded,
1511
- row: checkpoint
1512
- })), Effect.mapError((error) => SqliteStorageCorruptionError.make({
1513
- table: "effect_agent_checkpoints",
1514
- rowKey: `${checkpoint.thread_id}/${checkpoint.through_sequence}`,
1515
- message: error.message
1516
- }))));
1517
- const batchesByThread = groupByKey(batches, ({ row }) => row.thread_id);
1518
- const recordsByThread = groupByKey(records, ({ row }) => row.thread_id);
1519
- const checkpointsByThread = groupByKey(checkpoints, ({ row }) => row.thread_id);
1520
- const materializedIds = new Set(stored.threads.map((thread) => thread.thread_id));
1521
- for (const thread of stored.threads) {
1522
- const threadBatches = batchesByThread.get(thread.thread_id) ?? [];
1523
- const threadRecords = recordsByThread.get(thread.thread_id) ?? [];
1524
- const threadCheckpoints = checkpointsByThread.get(thread.thread_id) ?? [];
1525
- const recordsByBatch = groupByKey(threadRecords, ({ row }) => row.batch_id);
1526
- let previousDigest = EMPTY_TAIL_DIGEST;
1527
- let expectedSequence = 1;
1528
- const tailDigests = /* @__PURE__ */ new Map([[0, EMPTY_TAIL_DIGEST]]);
1529
- for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
1530
- const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
1531
- if (canonicalBatch.batchId !== batchRow.batch_id || batchRow.first_sequence !== expectedSequence || batchRow.last_sequence !== batchRow.first_sequence + canonicalBatch.records.length - 1) return yield* SqliteStorageCorruptionError.make({
1532
- table: "effect_agent_canonical_batches",
1533
- rowKey: key,
1534
- message: "Canonical batch identity, sequence, or record count is inconsistent."
1535
- });
1536
- const digest = yield* digestCanonicalBatch(previousDigest, canonicalBatch).pipe(Effect.provideService(Crypto.Crypto, crypto), Effect.mapError((error) => SqliteStorageCorruptionError.make({
1537
- table: "effect_agent_canonical_batches",
1538
- rowKey: key,
1539
- message: error.message
1540
- })));
1541
- if (batchRow.batch_digest !== digest || batchRow.tail_digest !== digest) return yield* SqliteStorageCorruptionError.make({
1542
- table: "effect_agent_canonical_batches",
1543
- rowKey: key,
1544
- message: "Canonical batch digest does not match its decoded content and prior tail."
1545
- });
1546
- const batchRecords = recordsByBatch.get(batchRow.batch_id) ?? [];
1547
- if (batchRecords.length !== canonicalBatch.records.length) return yield* SqliteStorageCorruptionError.make({
1548
- table: "effect_agent_canonical_records",
1549
- rowKey: key,
1550
- message: "Canonical batch and record-table counts differ."
1551
- });
1552
- for (let index = 0; index < canonicalBatch.records.length; index++) {
1553
- const expectedRecord = canonicalBatch.records[index];
1554
- const storedRecord = batchRecords[index];
1555
- const expectedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(expectedRecord).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
1556
- table: "effect_agent_canonical_batches",
1557
- rowKey: key,
1558
- message: error.message
1559
- })));
1560
- const storedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(storedRecord.decoded).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
1561
- table: "effect_agent_canonical_records",
1562
- rowKey: `${key}/${storedRecord.row.sequence}`,
1563
- message: error.message
1564
- })));
1565
- if (storedRecord.row.sequence !== batchRow.first_sequence + index || storedRecord.row.record_id !== expectedRecord.recordId || expectedJson !== storedJson) return yield* SqliteStorageCorruptionError.make({
1566
- table: "effect_agent_canonical_records",
1567
- rowKey: `${key}/${storedRecord.row.sequence}`,
1568
- message: "Canonical record identity, sequence, or payload differs from its batch."
1569
- });
1570
- }
1571
- previousDigest = digest;
1572
- expectedSequence = batchRow.last_sequence + 1;
1573
- tailDigests.set(batchRow.last_sequence, digest);
1574
- }
1575
- if (threadRecords.length !== thread.tail_sequence || thread.tail_sequence !== expectedSequence - 1 || thread.tail_digest !== previousDigest) return yield* SqliteStorageCorruptionError.make({
1576
- table: "effect_agent_threads",
1577
- rowKey: thread.thread_id,
1578
- message: "Thread tail does not match its canonical batch chain."
1579
- });
1580
- for (const checkpoint of threadCheckpoints) if (checkpoint.decoded.threadId !== thread.thread_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({
1581
- table: "effect_agent_checkpoints",
1582
- rowKey: `${thread.thread_id}/${checkpoint.row.through_sequence}`,
1583
- message: "Checkpoint identity or digest is not bound to a canonical batch tail."
1584
- });
1585
- }
1586
- if (batches.some(({ row }) => !materializedIds.has(row.thread_id)) || records.some(({ row }) => !materializedIds.has(row.thread_id)) || checkpoints.some(({ row }) => !materializedIds.has(row.thread_id))) return yield* SqliteStorageCorruptionError.make({
1587
- table: "effect_agent_threads",
1588
- rowKey: "startup_scan",
1589
- message: "Canonical rows exist without a materialized Thread."
1590
- });
1591
- });
1592
- const makeServices$1 = Effect.fn("SqliteThreadStore.makeServices")(function* () {
1593
- const config = yield* SqliteStorageConfig;
1594
- const failpoint = yield* SqliteStorageFailpoint;
1595
- const crypto = yield* Crypto.Crypto;
1596
- const journal = yield* initializeSqliteJournal();
1597
- if (config.verifyOnOpen) yield* decodeStartupPayloads(journal, crypto);
1598
- const provideCrypto = (effect) => Effect.provideService(effect, Crypto.Crypto, crypto);
1599
- const hitFailpoint = Effect.fn("SqliteThreadStore.hitFailpoint")((location) => failpoint.hit(location).pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))));
1600
- const materialize = Effect.fn("SqliteThreadStore.materialize")(function* (request) {
1601
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadMaterialization))(request).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
1602
- const now = yield* Clock.currentTimeMillis;
1603
- yield* hitFailpoint("materialize:before");
1604
- yield* journal.materialize(validated.threadId, new Date(now).toISOString(), EMPTY_TAIL_DIGEST, validated.producerEpoch).pipe(Effect.mapError((error) => error._tag === "SqliteFenceRejected" ? mapFence(validated.threadId, error) : storeError("materialize thread", error)));
1605
- yield* hitFailpoint("materialize:after");
1606
- });
1607
- const append = Effect.fn("SqliteThreadStore.append")(function* (request) {
1608
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
1609
- yield* requireThread(journal, validated.threadId);
1610
- const tailDigest = yield* provideCrypto(digestCanonicalBatch(validated.expectedTailDigest, validated.batch)).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
1611
- const batchJson = yield* encodeCanonicalBatch(validated.batch);
1612
- const rawRecords = yield* Effect.forEach(validated.batch.records, (record) => encodeCanonicalRecord(record).pipe(Effect.map((recordJson) => ({
1613
- recordId: record.recordId,
1614
- recordJson
1615
- }))));
1616
- const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
1617
- threadId: validated.threadId,
1618
- batchId: validated.batch.batchId,
1619
- batchDigest: tailDigest,
1620
- batchJson,
1621
- expectedTailSequence: validated.expectedTailSequence,
1622
- expectedTailDigest: validated.expectedTailDigest,
1623
- producerEpoch: validated.producerEpoch,
1624
- records: rawRecords,
1625
- tailDigest
1626
- }).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
1627
- yield* hitFailpoint("append:before");
1628
- const result = yield* journal.append(rawRequest).pipe(Effect.mapError((error) => {
1629
- if (isSqliteFenceRejected(error)) return mapFence(validated.threadId, error);
1630
- if (isSqliteAppendConflict(error)) return error.actualTailSequence !== void 0 && isDigest(error.actualTailDigest) ? AppendConflict.make({
1631
- threadId: validated.threadId,
1632
- batchId: validated.batch.batchId,
1633
- reason: error.reason,
1634
- actualTailSequence: error.actualTailSequence,
1635
- actualTailDigest: error.actualTailDigest
1636
- }) : AppendConflict.make({
1637
- threadId: validated.threadId,
1638
- batchId: validated.batch.batchId,
1639
- reason: error.reason
1640
- });
1641
- return storeError("append canonical batch", error);
1642
- }), Effect.flatMap((result) => Schema.decodeUnknownEffect(AppendResult)(result).pipe(Effect.mapError((error) => schemaStoreError("decode append result", error)))));
1643
- yield* hitFailpoint("append:after");
1644
- return result;
1645
- });
1646
- const loadRecords = Effect.fn("SqliteThreadStore.loadRecords")(function* (request) {
1647
- const rows = yield* journal.read(request).pipe(Effect.mapError((error) => storeError("read canonical records", error)));
1648
- return yield* Effect.forEach(rows, decodeEnvelope);
1649
- });
1650
- const readEffect = Effect.fn("SqliteThreadStore.read")(function* (request) {
1651
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadRead))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread read", error)));
1652
- yield* requireThread(journal, validated.threadId);
1653
- const records = yield* loadRecords(RawReadRequest.make({
1654
- threadId: validated.threadId,
1655
- fromSequenceExclusive: validated.afterSequence ?? ZERO_CANONICAL_SEQUENCE,
1656
- limit: validated.limit
1657
- }));
1658
- return Stream.fromIterable(records);
1659
- });
1660
- const read = (request) => Stream.unwrap(readEffect(request));
1661
- const observeEffect = Effect.fn("SqliteThreadStore.observe")(function* (request) {
1662
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadObservation))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread observation", error)));
1663
- yield* requireThread(journal, validated.threadId);
1664
- const initialSequence = yield* parseOffset(validated.threadId, validated.afterOffset);
1665
- const cursor = yield* Ref.make(initialSequence);
1666
- const poll = Effect.fn("SqliteThreadStore.observePoll")(function* () {
1667
- const fromSequenceExclusive = yield* Ref.get(cursor);
1668
- const records = yield* loadRecords(RawReadRequest.make({
1669
- threadId: validated.threadId,
1670
- fromSequenceExclusive,
1671
- limit: 1024
1672
- }));
1673
- if (records.length === 0) {
1674
- yield* Effect.sleep(config.observationPollInterval);
1675
- return [];
1676
- }
1677
- yield* Ref.set(cursor, records[records.length - 1].sequence);
1678
- return records;
1679
- });
1680
- return Stream.fromIterableEffectRepeat(poll());
1681
- });
1682
- const observe = (request) => Stream.unwrap(observeEffect(request));
1683
- const exportThread = Effect.fn("SqliteThreadStore.export")(function* (request) {
1684
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadExportRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread export", error)));
1685
- yield* requireThread(journal, validated.threadId);
1686
- const exported = yield* journal.exportThread(validated.threadId).pipe(Effect.mapError((error) => storeError("export thread", error)));
1687
- const records = yield* Effect.forEach(exported.records, decodeEnvelope);
1688
- if (records.length > 65536) return yield* ThreadStoreError.make({
1689
- operation: "decode thread export",
1690
- message: "The thread exceeds the current export record limit."
1691
- });
1692
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
1693
- return ThreadExport.make({
1694
- format: "effect-agent/thread@1",
1695
- threadId: validated.threadId,
1696
- tailSequence: exported.thread.tail_sequence,
1697
- tailDigest,
1698
- records
1699
- });
1700
- });
1701
- const inspectTail = Effect.fn("SqliteThreadStore.inspectTail")(function* (request) {
1702
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadTailRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
1703
- const thread = yield* requireThread(journal, validated.threadId);
1704
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode tail digest", error)));
1705
- return ThreadTail.make({
1706
- threadId: validated.threadId,
1707
- tailSequence: thread.tail_sequence,
1708
- tailDigest,
1709
- producerEpoch: thread.producer_epoch
1710
- });
1711
- });
1712
- const saveCheckpoint = Effect.fn("SqliteThreadStore.saveCheckpoint")(function* (request) {
1713
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
1714
- const thread = yield* requireThread(journal, validated.checkpoint.threadId);
1715
- if (validated.checkpoint.throughSequence > thread.tail_sequence) return yield* CheckpointRejected.make({
1716
- threadId: validated.checkpoint.threadId,
1717
- reason: "ahead-of-tail"
1718
- });
1719
- if ((yield* tailDigestAt(journal, validated.checkpoint.threadId, validated.checkpoint.throughSequence)) !== validated.checkpoint.tailDigest) return yield* CheckpointRejected.make({
1720
- threadId: validated.checkpoint.threadId,
1721
- reason: "digest-mismatch"
1722
- });
1723
- const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
1724
- const raw = RawCheckpoint.make({
1725
- threadId: validated.checkpoint.threadId,
1726
- throughSequence: validated.checkpoint.throughSequence,
1727
- tailDigest: validated.checkpoint.tailDigest,
1728
- checkpointJson
1729
- });
1730
- yield* hitFailpoint("save-checkpoint:before");
1731
- yield* journal.saveCheckpoint(raw).pipe(Effect.mapError((error) => isSqliteCheckpointConflict(error) ? CheckpointRejected.make({
1732
- threadId: validated.checkpoint.threadId,
1733
- reason: "digest-mismatch"
1734
- }) : storeError("save checkpoint", error)));
1735
- yield* hitFailpoint("save-checkpoint:after");
1736
- });
1737
- const loadCheckpoint = Effect.fn("SqliteThreadStore.loadCheckpoint")(function* (request) {
1738
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
1739
- const thread = yield* requireThread(journal, validated.threadId);
1740
- const rows = yield* journal.loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence).pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
1741
- if (rows.length === 0) return Option.none();
1742
- if (rows.length !== 1) return yield* ThreadStoreError.make({
1743
- operation: "load checkpoint",
1744
- message: `Expected at most one checkpoint row but found ${rows.length}.`
1745
- });
1746
- const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
1747
- if ((yield* tailDigestAt(journal, checkpoint.threadId, checkpoint.throughSequence)) !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
1748
- threadId: checkpoint.threadId,
1749
- reason: "digest-mismatch"
1750
- });
1751
- return Option.some(checkpoint);
1752
- });
1753
- const threadStore = ThreadStore.of({
1754
- append,
1755
- export: exportThread,
1756
- inspectTail,
1757
- materialize,
1758
- observe,
1759
- read,
1760
- checkpoints: {
1761
- save: saveCheckpoint,
1762
- load: loadCheckpoint
1763
- }
1764
- });
1765
- return Context.make(ThreadStore, threadStore);
1766
- });
1767
- /**
1768
- * SQLite Thread Store implementation with configuration, failpoint, SQL, and Crypto
1769
- * authority kept visible in its input channel.
1770
- */
1771
- const threadStoreLayer = Layer.effectContext(makeServices$1());
1772
- /**
1773
- * Validated SQLite storage configuration Layer with the documented defaults applied. Shared
1774
- * by the ThreadStore and SubmissionLedger convenience layers so their defaults cannot
1775
- * drift.
1776
- */
1777
- const storageConfigLayer = (options) => Layer.effect(SqliteStorageConfig)(Schema.decodeUnknownEffect(SqliteStorageConfigValue)({
1778
- observationPollInterval: options.observationPollInterval ?? 25,
1779
- busyTimeout: options.busyTimeout ?? 5e3,
1780
- ownershipLeaseDuration: options.ownershipLeaseDuration ?? Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION),
1781
- verifyOnOpen: options.verifyOnOpen ?? false
1782
- }).pipe(Effect.mapError((error) => SqliteStorageError.make({
1783
- cause: error,
1784
- operation: "configure SQLite storage",
1785
- message: error.message
1786
- }))));
1787
- /** The failpoint Layer selected by convenience options: explicit handler or the no-op default. */
1788
- const storageFailpointLayer = (options) => options.failpoint === void 0 ? SqliteStorageFailpoint.layer : Layer.succeed(SqliteStorageFailpoint)({ hit: options.failpoint });
1789
- /**
1790
- * A composition-root convenience Layer for canonical Threads. Durable accepted work is
1791
- * served by the separate SubmissionLedger port.
1792
- */
1793
- const layer = (options) => Layer.unwrap(Effect.map(SqliteStorageConfig, (config) => threadStoreLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(SqliteStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ filename: options.filename }), NodeCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
1794
- /** Create an adapter-owned resumable observation offset for a known canonical sequence. */
1795
- const observationOffsetAt = makeOffset;
1796
- //#endregion
1797
- //#region src/sqlite-ledger.ts
1798
- const BoundedStoredText = Schema.String.check(Schema.isMaxLength(16777216));
1799
- const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
1800
- const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
1801
- const SCAN_PAGE_SIZE = 256;
1802
- const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
1803
- const RESUME_IMMEDIATELY = "resume-immediately";
1804
- const SUSPENDED = "suspended";
1805
- const NOT_WAITING = "not-waiting";
1806
- const STILL_WAITING = "still-waiting";
1807
- const WOKEN = "woken";
1808
- var SubmissionRow = class extends Schema.Class("SubmissionRow")({
1809
- submission_id: BoundedIdentifier,
1810
- thread_id: BoundedIdentifier,
1811
- queue_sequence: QueueSequence,
1812
- principal: BoundedIdentifier,
1813
- idempotency_key: BoundedIdentifier,
1814
- agent_id: BoundedIdentifier,
1815
- agent_digests_json: BoundedStoredText,
1816
- deployment_id: BoundedIdentifier,
1817
- input_json: BoundedStoredText,
1818
- input_digest: Digest,
1819
- receipt_id: BoundedIdentifier,
1820
- state: SubmissionState,
1821
- settled_outcome: Schema.NullOr(SettlementOutcome),
1822
- created_at: BoundedTimestamp,
1823
- ready_at: Schema.NullOr(BoundedTimestamp),
1824
- input_applied_record_id: Schema.NullOr(BoundedIdentifier),
1825
- input_applied_sequence: Schema.NullOr(CanonicalSequence),
1826
- joined_host_submission_id: Schema.NullOr(BoundedIdentifier),
1827
- suspended_reason_json: Schema.NullOr(BoundedStoredText),
1828
- suspended_at: Schema.NullOr(BoundedTimestamp),
1829
- unknown_reason: Schema.NullOr(BoundedStoredText),
1830
- unknown_tool_call_ids_json: Schema.NullOr(BoundedStoredText),
1831
- parent_submission_id: Schema.NullOr(BoundedIdentifier),
1832
- parent_tool_call_id: Schema.NullOr(BoundedIdentifier)
1833
- }) {};
1834
- var ChildReservationRow = class extends Schema.Class("ChildReservationRow")({
1835
- reservation_id: BoundedIdentifier,
1836
- parent_submission_id: BoundedIdentifier,
1837
- parent_tool_call_id: BoundedIdentifier,
1838
- child_submission_id: Schema.NullOr(BoundedIdentifier),
1839
- status: ChildReservationStatus,
1840
- allocation_json: BoundedStoredText,
1841
- allocation_digest: Digest,
1842
- accounting_json: Schema.NullOr(BoundedStoredText),
1843
- reserved_at: BoundedTimestamp,
1844
- release_began_at: Schema.NullOr(BoundedTimestamp),
1845
- released_at: Schema.NullOr(BoundedTimestamp)
1846
- }) {};
1847
- var ApprovalDecisionRow = class extends Schema.Class("ApprovalDecisionRow")({
1848
- submission_id: BoundedIdentifier,
1849
- tool_call_id: BoundedIdentifier,
1850
- decision: ApprovalDecision,
1851
- resolver: BoundedIdentifier,
1852
- reason: BoundedStoredText,
1853
- decided_at: BoundedTimestamp
1854
- }) {};
1855
- var UnknownResolutionRow = class extends Schema.Class("UnknownResolutionRow")({
1856
- submission_id: BoundedIdentifier,
1857
- tool_call_id: BoundedIdentifier,
1858
- author: BoundedIdentifier,
1859
- reason: BoundedStoredText,
1860
- resolution_json: BoundedStoredText,
1861
- resolved_at: BoundedTimestamp
1862
- }) {};
1863
- var OwnershipRow = class extends Schema.Class("OwnershipRow")({
1864
- submission_id: BoundedIdentifier,
1865
- attempt_id: BoundedIdentifier,
1866
- ownership_token: BoundedIdentifier,
1867
- producer_epoch: ProducerEpoch,
1868
- owner_producer_id: BoundedIdentifier,
1869
- lease_expires_at: BoundedTimestamp
1870
- }) {};
1871
- var ReservationRow = class extends Schema.Class("ReservationRow")({
1872
- submission_id: BoundedIdentifier,
1873
- settlement_id: BoundedIdentifier,
1874
- outcome: SettlementOutcome,
1875
- record_id: BoundedIdentifier,
1876
- record_json: BoundedStoredText,
1877
- record_digest: Digest,
1878
- reserved_at: BoundedTimestamp,
1879
- finalized_at: Schema.NullOr(BoundedTimestamp)
1880
- }) {};
1881
- var AbortIntentRow = class extends Schema.Class("AbortIntentRow")({
1882
- submission_id: BoundedIdentifier,
1883
- author: BoundedIdentifier,
1884
- reason: BoundedStoredText,
1885
- requested_at: BoundedTimestamp,
1886
- canonical_record_id: Schema.NullOr(BoundedIdentifier)
1887
- }) {};
1888
- var MaxQueueSequenceRow = class extends Schema.Class("MaxQueueSequenceRow")({ max_queue_sequence: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) }) {};
1889
- var CanonicalRecordIdRow = class extends Schema.Class("CanonicalRecordIdRow")({ record_id: BoundedIdentifier }) {};
1890
- const SUBMISSION_COLUMNS = `
1891
- submission_id,
1892
- thread_id,
1893
- queue_sequence,
1894
- principal,
1895
- idempotency_key,
1896
- agent_id,
1897
- agent_digests_json,
1898
- deployment_id,
1899
- input_json,
1900
- input_digest,
1901
- receipt_id,
1902
- state,
1903
- settled_outcome,
1904
- created_at,
1905
- ready_at,
1906
- input_applied_record_id,
1907
- input_applied_sequence,
1908
- joined_host_submission_id,
1909
- suspended_reason_json,
1910
- suspended_at,
1911
- unknown_reason,
1912
- unknown_tool_call_ids_json,
1913
- parent_submission_id,
1914
- parent_tool_call_id
1915
- `;
1916
- const CHILD_RESERVATION_COLUMNS = `
1917
- reservation_id,
1918
- parent_submission_id,
1919
- parent_tool_call_id,
1920
- child_submission_id,
1921
- status,
1922
- allocation_json,
1923
- allocation_digest,
1924
- accounting_json,
1925
- reserved_at,
1926
- release_began_at,
1927
- released_at
1928
- `;
1929
- /** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
1930
- const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
1931
- const ToolCallIdList = Schema.Array(ToolCallIdSchema);
1932
- const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
1933
- const encodeDefinitionDigestsText = Schema.encodeEffect(Schema.fromJsonString(DefinitionDigests));
1934
- const encodeRecordEnvelopeText = Schema.encodeEffect(Schema.fromJsonString(RecordEnvelope));
1935
- const decodeRecordEnvelopeText = Schema.decodeEffect(Schema.fromJsonString(RecordEnvelope));
1936
- const encodeSuspensionReasonText = Schema.encodeEffect(Schema.fromJsonString(SuspensionReason));
1937
- const encodeUnknownResolutionText = Schema.encodeEffect(Schema.fromJsonString(UnknownResolution));
1938
- const encodeToolCallIdsText = Schema.encodeEffect(Schema.fromJsonString(ToolCallIdList));
1939
- const decodeToolCallIdsText = Schema.decodeEffect(Schema.fromJsonString(ToolCallIdList));
1940
- const parseStoredJsonText = Schema.decodeEffect(Schema.fromJsonString(Schema.Json));
1941
- const decodeAdmissionResult = Schema.decodeUnknownEffect(AdmissionResult);
1942
- const decodeClaim = Schema.decodeUnknownEffect(Claim);
1943
- const decodeOwnershipRenewal = Schema.decodeUnknownEffect(OwnershipRenewal);
1944
- const decodeSettlement = Schema.decodeUnknownEffect(Settlement);
1945
- const decodeAbortIntent = Schema.decodeUnknownEffect(AbortIntent);
1946
- const decodeOwnershipSnapshot = Schema.decodeUnknownEffect(OwnershipSnapshot);
1947
- const decodeInputAppliedMarker = Schema.decodeUnknownEffect(InputAppliedMarker);
1948
- const decodeSubmissionSnapshotUnknown = Schema.decodeUnknownEffect(SubmissionSnapshot);
1949
- const decodeSubmissionId = Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId);
1950
- const decodeQueueSequence = Schema.decodeUnknownEffect(QueueSequence);
1951
- const decodeUtcInstant = Schema.decodeUnknownEffect(Schema.DateTimeUtcFromString);
1952
- const decodeJoiningClaim = Schema.decodeUnknownEffect(JoiningClaim);
1953
- const decodeJoinSnapshot = Schema.decodeUnknownEffect(JoinSnapshot);
1954
- const decodeSuspensionSnapshot = Schema.decodeUnknownEffect(SuspensionSnapshot);
1955
- const decodeApprovalDecisionIntent = Schema.decodeUnknownEffect(ApprovalDecisionIntent);
1956
- const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResolutionIntent);
1957
- const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
1958
- const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(ChildBudgetReservationSnapshot);
1959
- const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
1960
- const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
1961
- const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
1962
- const isSqliteTransactionFailure = Schema.is(Schema.Union([SqliteStorageError, SqliteWriteContention]));
1963
- /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
1964
- const internalFailure = (operation) => (error) => LedgerError.make({
1965
- operation,
1966
- message: error.message,
1967
- cause: error
1968
- });
1969
- /** Classify raw SQL failures: write-lock timeouts stay retryable typed contention. */
1970
- const sqlFailure = (operation) => (error) => {
1971
- const internal = error.reason._tag === "LockTimeoutError" ? SqliteWriteContention.make({
1972
- cause: error,
1973
- operation,
1974
- message: `Another producer holds the SQLite write lock; ${operation} is safe to retry.`
1975
- }) : SqliteLedgerError.make({
1976
- cause: error,
1977
- operation,
1978
- message: error.message
1979
- });
1980
- return internalFailure(operation)(internal);
1981
- };
1982
- const corruptionFailure = (operation, table, rowKey, message) => internalFailure(operation)(SqliteStorageCorruptionError.make({
1983
- table,
1984
- rowKey,
1985
- message
1986
- }));
1987
- const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function* () {
1988
- const config = yield* SqliteStorageConfig;
1989
- const failpoint = yield* SqliteStorageFailpoint;
1990
- const sql = yield* SqlClient.SqlClient;
1991
- const crypto = yield* Crypto.Crypto;
1992
- const journal = yield* initializeSqliteJournal();
1993
- const hitFailpoint = (location, operation) => failpoint.hit(location).pipe(Effect.mapError((error) => internalFailure(operation)(error)));
1994
- /**
1995
- * Run one ledger mutation under the journal's `BEGIN IMMEDIATE` write transaction so
1996
- * ownership-token and epoch checks are atomic with their writes (DUR-006). Transaction
1997
- * acquisition failures surface as LedgerError carrying the typed retryable
1998
- * SqliteWriteContention (or SqliteStorageError) as cause.
1999
- */
2000
- const inWriteTransaction = (operation, effect) => journal.withWriteTransaction(operation)(effect).pipe(Effect.mapError((error) => isSqliteTransactionFailure(error) ? internalFailure(operation)(error) : error));
2001
- const mintIdentifier = (prefix, operation) => crypto.randomUUIDv7.pipe(Effect.map((uuid) => `${prefix}-${uuid}`), Effect.mapError((error) => internalFailure(operation)(error)));
2002
- const currentInstant = Effect.map(Clock.currentTimeMillis, (millis) => ({
2003
- millis,
2004
- iso: new Date(millis).toISOString()
2005
- }));
2006
- const timestampMillis = (operation, rowKey) => (timestamp) => decodeUtcInstant(timestamp).pipe(Effect.map(DateTime.toEpochMillis), Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submission_ownership", rowKey, error.message)));
2007
- const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
2008
- const readSubmission = Effect.fn("SqliteSubmissionLedger.readSubmission")(function* (operation, submissionId) {
2009
- const rows = yield* sql`
2010
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2011
- FROM effect_agent_submissions
2012
- WHERE submission_id = ${submissionId}
2013
- `.pipe(Effect.mapError(sqlFailure(operation)));
2014
- const decoded = yield* decodeSubmissionRows(operation, submissionId, rows);
2015
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", submissionId, "A submission primary key returned more than one row.");
2016
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
2017
- });
2018
- const requireSubmission = Effect.fn("SqliteSubmissionLedger.requireSubmission")(function* (operation, submissionId) {
2019
- const submission = yield* readSubmission(operation, submissionId);
2020
- if (Option.isNone(submission)) return yield* LedgerError.make({
2021
- operation,
2022
- message: `Unknown submission ${submissionId}.`
2023
- });
2024
- return submission.value;
2025
- });
2026
- const readOwnership = Effect.fn("SqliteSubmissionLedger.readOwnership")(function* (operation, submissionId) {
2027
- const rows = yield* sql`
2028
- SELECT
2029
- submission_id,
2030
- attempt_id,
2031
- ownership_token,
2032
- producer_epoch,
2033
- owner_producer_id,
2034
- lease_expires_at
2035
- FROM effect_agent_submission_ownership
2036
- WHERE submission_id = ${submissionId}
2037
- `.pipe(Effect.mapError(sqlFailure(operation)));
2038
- const decoded = yield* decodeRows$2(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
2039
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submission_ownership", submissionId, "An ownership primary key returned more than one row.");
2040
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
2041
- });
2042
- const threadEpoch = Effect.fn("SqliteSubmissionLedger.threadEpoch")(function* (operation, threadId) {
2043
- const threads = yield* journal.getThread(threadId).pipe(Effect.mapError(internalFailure(operation)));
2044
- return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
2045
- });
2046
- /**
2047
- * Verify inside the surrounding write transaction that the presented token still owns the
2048
- * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
2049
- * Thread's current producer epoch (DUR-006).
2050
- */
2051
- const requireOwnership = Effect.fn("SqliteSubmissionLedger.requireOwnership")(function* (operation, submission, ownershipToken) {
2052
- const ownership = yield* readOwnership(operation, submission.submission_id);
2053
- if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
2054
- const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
2055
- const submissionId = yield* Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId)(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
2056
- return yield* OwnershipLost.make({
2057
- submissionId,
2058
- actualEpoch
2059
- });
2060
- }
2061
- return ownership.value;
2062
- });
2063
- const decodeSubmissionSnapshot = Effect.fn("SqliteSubmissionLedger.decodeSubmissionSnapshot")(function* (operation, row) {
2064
- const agentDigests = yield* parseStoredJsonText(row.agent_digests_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
2065
- const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
2066
- 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.");
2067
- return yield* decodeSubmissionSnapshotUnknown({
2068
- submissionId: row.submission_id,
2069
- threadId: row.thread_id,
2070
- queueSequence: row.queue_sequence,
2071
- principal: row.principal,
2072
- idempotencyKey: row.idempotency_key,
2073
- agentId: row.agent_id,
2074
- agentDigests,
2075
- deploymentId: row.deployment_id,
2076
- inputPayload,
2077
- inputDigest: row.input_digest,
2078
- receiptId: row.receipt_id,
2079
- state: row.state,
2080
- createdAt: row.created_at,
2081
- ...row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome },
2082
- ...row.ready_at === null ? {} : { readyAt: row.ready_at },
2083
- ...row.parent_submission_id === null || row.parent_tool_call_id === null ? {} : { parentLinkage: {
2084
- parentSubmissionId: row.parent_submission_id,
2085
- parentToolCallId: row.parent_tool_call_id
2086
- } }
2087
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
2088
- });
2089
- const readReservation = Effect.fn("SqliteSubmissionLedger.readReservation")(function* (operation, submissionId) {
2090
- const rows = yield* sql`
2091
- SELECT
2092
- submission_id,
2093
- settlement_id,
2094
- outcome,
2095
- record_id,
2096
- record_json,
2097
- record_digest,
2098
- reserved_at,
2099
- finalized_at
2100
- FROM effect_agent_settlement_reservations
2101
- WHERE submission_id = ${submissionId}
2102
- `.pipe(Effect.mapError(sqlFailure(operation)));
2103
- const decoded = yield* decodeRows$2(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
2104
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", submissionId, "A settlement reservation primary key returned more than one row.");
2105
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
2106
- });
2107
- const readAbortIntent = Effect.fn("SqliteSubmissionLedger.readAbortIntent")(function* (operation, submissionId) {
2108
- const rows = yield* sql`
2109
- SELECT
2110
- submission_id,
2111
- author,
2112
- reason,
2113
- requested_at,
2114
- canonical_record_id
2115
- FROM effect_agent_abort_intents
2116
- WHERE submission_id = ${submissionId}
2117
- `.pipe(Effect.mapError(sqlFailure(operation)));
2118
- const decoded = yield* decodeRows$2(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
2119
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_abort_intents", submissionId, "An abort intent primary key returned more than one row.");
2120
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
2121
- });
2122
- const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
2123
- const readChildReservation = Effect.fn("SqliteSubmissionLedger.readChildReservation")(function* (operation, reservationId) {
2124
- const rows = yield* sql`
2125
- SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
2126
- FROM effect_agent_child_reservations
2127
- WHERE reservation_id = ${reservationId}
2128
- `.pipe(Effect.mapError(sqlFailure(operation)));
2129
- const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
2130
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_child_reservations", reservationId, "A child reservation primary key returned more than one row.");
2131
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
2132
- });
2133
- const readChildReservationForCall = Effect.fn("SqliteSubmissionLedger.readChildReservationForCall")(function* (operation, parentSubmissionId, parentToolCallId) {
2134
- const rows = yield* sql`
2135
- SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
2136
- FROM effect_agent_child_reservations
2137
- WHERE parent_submission_id = ${parentSubmissionId}
2138
- AND parent_tool_call_id = ${parentToolCallId}
2139
- `.pipe(Effect.mapError(sqlFailure(operation)));
2140
- const decoded = yield* decodeChildReservationRows(operation, `${parentSubmissionId}/${parentToolCallId}`, rows);
2141
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_child_reservations", `${parentSubmissionId}/${parentToolCallId}`, "A parent Tool Call returned more than one child reservation.");
2142
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
2143
- });
2144
- const childReservationSnapshotFromRow = Effect.fn("SqliteSubmissionLedger.childReservationSnapshotFromRow")(function* (operation, row) {
2145
- const rowFailure = (error) => corruptionFailure(operation, "effect_agent_child_reservations", row.reservation_id, error.message);
2146
- const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(Effect.mapError(rowFailure));
2147
- const accounting = row.accounting_json === null ? void 0 : yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
2148
- return yield* decodeChildReservationSnapshotUnknown({
2149
- reservationId: row.reservation_id,
2150
- parentSubmissionId: row.parent_submission_id,
2151
- parentToolCallId: row.parent_tool_call_id,
2152
- status: row.status,
2153
- allocation,
2154
- allocationDigest: row.allocation_digest,
2155
- reservedAt: row.reserved_at,
2156
- ...row.child_submission_id === null ? {} : { childSubmissionId: row.child_submission_id },
2157
- ...accounting === void 0 ? {} : { accounting },
2158
- ...row.release_began_at === null ? {} : { releaseBeganAt: row.release_began_at },
2159
- ...row.released_at === null ? {} : { releasedAt: row.released_at }
2160
- }).pipe(Effect.mapError(rowFailure));
2161
- });
2162
- const readApprovalDecisions = Effect.fn("SqliteSubmissionLedger.readApprovalDecisions")(function* (operation, submissionId) {
2163
- const rows = yield* sql`
2164
- SELECT
2165
- submission_id,
2166
- tool_call_id,
2167
- decision,
2168
- resolver,
2169
- reason,
2170
- decided_at
2171
- FROM effect_agent_approval_decisions
2172
- WHERE submission_id = ${submissionId}
2173
- ORDER BY tool_call_id ASC
2174
- `.pipe(Effect.mapError(sqlFailure(operation)));
2175
- return yield* decodeRows$2(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
2176
- });
2177
- const approvalIntentFromRow = Effect.fn("SqliteSubmissionLedger.approvalIntentFromRow")(function* (operation, row) {
2178
- return yield* decodeApprovalDecisionIntent({
2179
- submissionId: row.submission_id,
2180
- toolCallId: row.tool_call_id,
2181
- decision: row.decision,
2182
- resolver: row.resolver,
2183
- reason: row.reason,
2184
- decidedAt: row.decided_at
2185
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_approval_decisions", `${row.submission_id}/${row.tool_call_id}`, error.message)));
2186
- });
2187
- const readUnknownResolutions = Effect.fn("SqliteSubmissionLedger.readUnknownResolutions")(function* (operation, submissionId) {
2188
- const rows = yield* sql`
2189
- SELECT
2190
- submission_id,
2191
- tool_call_id,
2192
- author,
2193
- reason,
2194
- resolution_json,
2195
- resolved_at
2196
- FROM effect_agent_unknown_resolutions
2197
- WHERE submission_id = ${submissionId}
2198
- ORDER BY tool_call_id ASC
2199
- `.pipe(Effect.mapError(sqlFailure(operation)));
2200
- return yield* decodeRows$2(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
2201
- });
2202
- const unknownResolutionIntentFromRow = Effect.fn("SqliteSubmissionLedger.unknownResolutionIntentFromRow")(function* (operation, row) {
2203
- 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)));
2204
- return yield* decodeUnknownResolutionIntent({
2205
- submissionId: row.submission_id,
2206
- toolCallId: row.tool_call_id,
2207
- author: row.author,
2208
- reason: row.reason,
2209
- resolution,
2210
- resolvedAt: row.resolved_at
2211
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_unknown_resolutions", `${row.submission_id}/${row.tool_call_id}`, error.message)));
2212
- });
2213
- /** The Submission's marked-unknown open Tool Call identities, empty when never marked. */
2214
- const storedUnknownToolCallIds = Effect.fn("SqliteSubmissionLedger.storedUnknownToolCallIds")(function* (operation, submission) {
2215
- if (submission.unknown_tool_call_ids_json === null) return [];
2216
- return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", submission.submission_id, error.message)));
2217
- });
2218
- /**
2219
- * Canonical history is the abort authority (DUR-015): the intent's canonicalRecordId is
2220
- * derived from the shared canonical-records table using the deterministic abort record
2221
- * identity, never from a cached ledger marker.
2222
- */
2223
- const canonicalAbortRecordId = Effect.fn("SqliteSubmissionLedger.canonicalAbortRecordId")(function* (operation, threadId, submissionId) {
2224
- const recordId = submissionAbortRecordId(submissionId);
2225
- const rows = yield* sql`
2226
- SELECT record_id
2227
- FROM effect_agent_canonical_records
2228
- WHERE thread_id = ${threadId}
2229
- AND record_id = ${recordId}
2230
- `.pipe(Effect.mapError(sqlFailure(operation)));
2231
- return (yield* decodeRows$2(Schema.Array(CanonicalRecordIdRow), "effect_agent_canonical_records", `${threadId}/${recordId}`, rows).pipe(Effect.mapError(internalFailure(operation)))).length === 0 ? void 0 : recordId;
2232
- });
2233
- const abortIntentFromRow = Effect.fn("SqliteSubmissionLedger.abortIntentFromRow")(function* (operation, submission, submissionId, row) {
2234
- const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, submissionId);
2235
- return yield* decodeAbortIntent({
2236
- submissionId: row.submission_id,
2237
- author: row.author,
2238
- reason: row.reason,
2239
- requestedAt: row.requested_at,
2240
- ...canonicalRecordId === void 0 ? {} : { canonicalRecordId }
2241
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_abort_intents", row.submission_id, error.message)));
2242
- });
2243
- const capabilities = Effect.succeed(LedgerCapabilities.make({ durability: "durable-node" }));
2244
- const admit = Effect.fn("SqliteSubmissionLedger.admit")(function* (request) {
2245
- const operation = "ledger admit";
2246
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AdmissionRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2247
- const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(Effect.mapError(internalFailure(operation)));
2248
- const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(Effect.mapError(internalFailure(operation)));
2249
- const mintedSubmissionId = yield* mintIdentifier("submission", operation);
2250
- const mintedReceiptId = yield* mintIdentifier("receipt", operation);
2251
- yield* hitFailpoint("ledger:admit:before", operation);
2252
- const result = yield* inWriteTransaction(operation, Effect.gen(function* () {
2253
- const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
2254
- const existingRows = yield* sql`
2255
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2256
- FROM effect_agent_submissions
2257
- WHERE thread_id = ${validated.threadId}
2258
- AND principal = ${validated.principal}
2259
- AND idempotency_key = ${validated.idempotencyKey}
2260
- `.pipe(Effect.mapError(sqlFailure(operation)));
2261
- const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
2262
- if (existing.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", keyRowKey, "An admission idempotency key returned more than one row.");
2263
- if (existing.length === 1) {
2264
- 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;
2265
- if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) return yield* AdmissionConflict.make({
2266
- threadId: validated.threadId,
2267
- principal: validated.principal,
2268
- idempotencyKey: validated.idempotencyKey,
2269
- existingInputDigest: existing[0].input_digest,
2270
- attemptedInputDigest: validated.inputDigest
2271
- });
2272
- return yield* decodeAdmissionResult({
2273
- submissionId: existing[0].submission_id,
2274
- receiptId: existing[0].receipt_id,
2275
- queueSequence: existing[0].queue_sequence,
2276
- state: existing[0].state,
2277
- replayed: true
2278
- }).pipe(Effect.mapError(internalFailure(operation)));
2279
- }
2280
- const maxRows = yield* sql`
2281
- SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
2282
- FROM effect_agent_submissions
2283
- WHERE thread_id = ${validated.threadId}
2284
- `.pipe(Effect.mapError(sqlFailure(operation)));
2285
- const decodedMax = yield* decodeRows$2(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.threadId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
2286
- const queueSequence = yield* decodeQueueSequence((decodedMax[0]?.max_queue_sequence ?? 0) + 1).pipe(Effect.mapError(internalFailure(operation)));
2287
- const now = yield* currentInstant;
2288
- yield* sql`
2289
- INSERT INTO effect_agent_submissions (
2290
- submission_id,
2291
- thread_id,
2292
- queue_sequence,
2293
- principal,
2294
- idempotency_key,
2295
- agent_id,
2296
- agent_digests_json,
2297
- deployment_id,
2298
- input_json,
2299
- input_digest,
2300
- receipt_id,
2301
- state,
2302
- created_at,
2303
- parent_submission_id,
2304
- parent_tool_call_id
2305
- ) VALUES (
2306
- ${mintedSubmissionId},
2307
- ${validated.threadId},
2308
- ${queueSequence},
2309
- ${validated.principal},
2310
- ${validated.idempotencyKey},
2311
- ${validated.agentId},
2312
- ${agentDigestsJson},
2313
- ${validated.deploymentId},
2314
- ${inputJson},
2315
- ${validated.inputDigest},
2316
- ${mintedReceiptId},
2317
- 'admitted',
2318
- ${now.iso},
2319
- ${validated.parentLinkage?.parentSubmissionId ?? null},
2320
- ${validated.parentLinkage?.parentToolCallId ?? null}
2321
- )
2322
- `.pipe(Effect.mapError(sqlFailure(operation)));
2323
- return yield* decodeAdmissionResult({
2324
- submissionId: mintedSubmissionId,
2325
- receiptId: mintedReceiptId,
2326
- queueSequence,
2327
- state: "admitted",
2328
- replayed: false
2329
- }).pipe(Effect.mapError(internalFailure(operation)));
2330
- }));
2331
- yield* hitFailpoint("ledger:admit:after", operation);
2332
- return result;
2333
- });
2334
- const markReady = Effect.fn("SqliteSubmissionLedger.markReady")(function* (request) {
2335
- const operation = "ledger mark ready";
2336
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkReadyRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2337
- yield* hitFailpoint("ledger:mark-ready:before", operation);
2338
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2339
- if ((yield* requireSubmission(operation, validated.submissionId)).state !== "admitted") return;
2340
- const now = yield* currentInstant;
2341
- yield* sql`
2342
- UPDATE effect_agent_submissions
2343
- SET state = 'ready', ready_at = ${now.iso}
2344
- WHERE submission_id = ${validated.submissionId}
2345
- `.pipe(Effect.mapError(sqlFailure(operation)));
2346
- }));
2347
- yield* hitFailpoint("ledger:mark-ready:after", operation);
2348
- });
2349
- const lookup = Effect.fn("SqliteSubmissionLedger.lookup")(function* (request) {
2350
- const operation = "ledger lookup";
2351
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookup))(request).pipe(Effect.mapError(internalFailure(operation)));
2352
- if (validated._tag === "SubmissionLookupById") {
2353
- const row = yield* readSubmission(operation, validated.submissionId);
2354
- if (Option.isNone(row)) return Option.none();
2355
- return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
2356
- }
2357
- const rows = yield* sql`
2358
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2359
- FROM effect_agent_submissions
2360
- WHERE thread_id = ${validated.threadId}
2361
- AND principal = ${validated.principal}
2362
- AND idempotency_key = ${validated.idempotencyKey}
2363
- `.pipe(Effect.mapError(sqlFailure(operation)));
2364
- const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2365
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, "An admission idempotency key returned more than one row.");
2366
- if (decoded.length === 0) return Option.none();
2367
- return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
2368
- });
2369
- const resolveAdmission = Effect.fn("SqliteSubmissionLedger.resolveAdmission")(function* (request) {
2370
- const operation = "ledger resolve admission";
2371
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookupByKey))(request).pipe(Effect.mapError(internalFailure(operation)));
2372
- const rows = yield* sql`
2373
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2374
- FROM effect_agent_submissions
2375
- WHERE thread_id = ${validated.threadId}
2376
- AND principal = ${validated.principal}
2377
- AND idempotency_key = ${validated.idempotencyKey}
2378
- `.pipe(Effect.mapError(sqlFailure(operation)));
2379
- const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2380
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, "An admission idempotency key returned more than one row.");
2381
- if (decoded.length === 0) return AdmissionNotAdmitted.make();
2382
- return AdmissionAdmitted.make({ submission: yield* decodeSubmissionSnapshot(operation, decoded[0]) });
2383
- });
2384
- const claim = Effect.fn("SqliteSubmissionLedger.claim")(function* (request) {
2385
- const operation = "ledger claim";
2386
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2387
- const attemptId = yield* mintIdentifier("attempt", operation);
2388
- const ownershipToken = yield* mintIdentifier("owner", operation);
2389
- yield* hitFailpoint("ledger:claim:before", operation);
2390
- const claimed = yield* inWriteTransaction(operation, Effect.gen(function* () {
2391
- const headRows = yield* sql`
2392
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2393
- FROM effect_agent_submissions
2394
- WHERE thread_id = ${validated.threadId}
2395
- AND state <> 'settled'
2396
- ORDER BY queue_sequence ASC
2397
- LIMIT 1
2398
- `.pipe(Effect.mapError(sqlFailure(operation)));
2399
- const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
2400
- if (heads.length === 0) return Option.none();
2401
- const head = heads[0];
2402
- if (head.state === "joining" || head.state === "joined" || head.state === "suspended" || head.state === "unknown" && Option.isNone(yield* readAbortIntent(operation, head.submission_id))) return Option.none();
2403
- const now = yield* currentInstant;
2404
- const ownership = yield* readOwnership(operation, head.submission_id);
2405
- if (Option.isSome(ownership)) {
2406
- if ((yield* timestampMillis(operation, head.submission_id)(ownership.value.lease_expires_at)) > now.millis) return Option.none();
2407
- }
2408
- const threads = yield* journal.getThread(head.thread_id).pipe(Effect.mapError(internalFailure(operation)));
2409
- let producerEpoch;
2410
- if (threads.length === 0) {
2411
- producerEpoch = 1;
2412
- yield* sql`
2413
- INSERT INTO effect_agent_threads (
2414
- thread_id,
2415
- created_at,
2416
- tail_sequence,
2417
- tail_digest,
2418
- producer_epoch
2419
- ) VALUES (
2420
- ${head.thread_id},
2421
- ${now.iso},
2422
- 0,
2423
- ${EMPTY_TAIL_DIGEST},
2424
- ${producerEpoch}
2425
- )
2426
- `.pipe(Effect.mapError(sqlFailure(operation)));
2427
- } else {
2428
- producerEpoch = threads[0].producer_epoch + 1;
2429
- yield* sql`
2430
- UPDATE effect_agent_threads
2431
- SET producer_epoch = ${producerEpoch}
2432
- WHERE thread_id = ${head.thread_id}
2433
- `.pipe(Effect.mapError(sqlFailure(operation)));
2434
- }
2435
- const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
2436
- yield* sql`
2437
- INSERT INTO effect_agent_submission_ownership (
2438
- submission_id,
2439
- attempt_id,
2440
- ownership_token,
2441
- producer_epoch,
2442
- owner_producer_id,
2443
- lease_expires_at
2444
- ) VALUES (
2445
- ${head.submission_id},
2446
- ${attemptId},
2447
- ${ownershipToken},
2448
- ${producerEpoch},
2449
- ${validated.producerId},
2450
- ${leaseExpiresAt}
2451
- )
2452
- ON CONFLICT (submission_id) DO UPDATE SET
2453
- attempt_id = excluded.attempt_id,
2454
- ownership_token = excluded.ownership_token,
2455
- producer_epoch = excluded.producer_epoch,
2456
- owner_producer_id = excluded.owner_producer_id,
2457
- lease_expires_at = excluded.lease_expires_at
2458
- `.pipe(Effect.mapError(sqlFailure(operation)));
2459
- yield* sql`
2460
- INSERT INTO effect_agent_attempts (
2461
- attempt_id,
2462
- submission_id,
2463
- thread_id,
2464
- owner_producer_id,
2465
- producer_epoch,
2466
- claimed_at
2467
- ) VALUES (
2468
- ${attemptId},
2469
- ${head.submission_id},
2470
- ${head.thread_id},
2471
- ${validated.producerId},
2472
- ${producerEpoch},
2473
- ${now.iso}
2474
- )
2475
- `.pipe(Effect.mapError(sqlFailure(operation)));
2476
- if (head.state === "ready") yield* sql`
2477
- UPDATE effect_agent_submissions
2478
- SET state = 'running'
2479
- WHERE submission_id = ${head.submission_id}
2480
- `.pipe(Effect.mapError(sqlFailure(operation)));
2481
- const inputPayload = yield* parseStoredJsonText(head.input_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", head.submission_id, error.message)));
2482
- return Option.some(yield* decodeClaim({
2483
- submissionId: head.submission_id,
2484
- attemptId,
2485
- ownershipToken,
2486
- producerEpoch,
2487
- leaseExpiresAt,
2488
- inputPayload
2489
- }).pipe(Effect.mapError(internalFailure(operation))));
2490
- }));
2491
- yield* hitFailpoint("ledger:claim:after", operation);
2492
- return claimed;
2493
- });
2494
- const renewOwnership = Effect.fn("SqliteSubmissionLedger.renewOwnership")(function* (request) {
2495
- const operation = "ledger renew ownership";
2496
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RenewOwnershipRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2497
- yield* hitFailpoint("ledger:renew:before", operation);
2498
- const renewal = yield* inWriteTransaction(operation, Effect.gen(function* () {
2499
- const submission = yield* requireSubmission(operation, validated.submissionId);
2500
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2501
- const now = yield* currentInstant;
2502
- const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
2503
- yield* sql`
2504
- UPDATE effect_agent_submission_ownership
2505
- SET lease_expires_at = ${leaseExpiresAt}
2506
- WHERE submission_id = ${validated.submissionId}
2507
- `.pipe(Effect.mapError(sqlFailure(operation)));
2508
- return yield* decodeOwnershipRenewal({
2509
- ownershipToken: validated.ownershipToken,
2510
- leaseExpiresAt
2511
- }).pipe(Effect.mapError(internalFailure(operation)));
2512
- }));
2513
- yield* hitFailpoint("ledger:renew:after", operation);
2514
- return renewal;
2515
- });
2516
- const releaseOwnership = Effect.fn("SqliteSubmissionLedger.releaseOwnership")(function* (request) {
2517
- const operation = "ledger release ownership";
2518
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseOwnershipRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2519
- yield* hitFailpoint("ledger:release:before", operation);
2520
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2521
- const submission = yield* requireSubmission(operation, validated.submissionId);
2522
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2523
- yield* sql`
2524
- DELETE FROM effect_agent_submission_ownership
2525
- WHERE submission_id = ${validated.submissionId}
2526
- `.pipe(Effect.mapError(sqlFailure(operation)));
2527
- if (submission.state === "running") yield* sql`
2528
- UPDATE effect_agent_submissions
2529
- SET state = 'ready'
2530
- WHERE submission_id = ${validated.submissionId}
2531
- `.pipe(Effect.mapError(sqlFailure(operation)));
2532
- }));
2533
- yield* hitFailpoint("ledger:release:after", operation);
2534
- });
2535
- const markInputApplied = Effect.fn("SqliteSubmissionLedger.markInputApplied")(function* (request) {
2536
- const operation = "ledger mark input applied";
2537
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkInputAppliedRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2538
- yield* hitFailpoint("ledger:mark-input-applied:before", operation);
2539
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2540
- const submission = yield* requireSubmission(operation, validated.submissionId);
2541
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2542
- if (submission.input_applied_record_id !== null) {
2543
- if (submission.input_applied_record_id === validated.recordId && submission.input_applied_sequence === validated.sequence) return;
2544
- return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A different canonical input marker is already recorded for this Submission.");
2545
- }
2546
- yield* sql`
2547
- UPDATE effect_agent_submissions
2548
- SET
2549
- input_applied_record_id = ${validated.recordId},
2550
- input_applied_sequence = ${validated.sequence},
2551
- state = CASE
2552
- WHEN state IN ('admitted', 'ready', 'running') THEN 'input-applied'
2553
- ELSE state
2554
- END
2555
- WHERE submission_id = ${validated.submissionId}
2556
- `.pipe(Effect.mapError(sqlFailure(operation)));
2557
- }));
2558
- yield* hitFailpoint("ledger:mark-input-applied:after", operation);
2559
- });
2560
- const reserveSettlement = Effect.fn("SqliteSubmissionLedger.reserveSettlement")(function* (request) {
2561
- const operation = "ledger reserve settlement";
2562
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementReservation))(request).pipe(Effect.mapError(internalFailure(operation)));
2563
- const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(Effect.mapError(internalFailure(operation)));
2564
- yield* hitFailpoint("ledger:reserve-settlement:before", operation);
2565
- const reserved = yield* inWriteTransaction(operation, Effect.gen(function* () {
2566
- const existing = yield* readReservation(operation, validated.submissionId);
2567
- if (Option.isSome(existing)) {
2568
- 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({
2569
- submissionId: validated.submissionId,
2570
- existingOutcome: existing.value.outcome
2571
- });
2572
- const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, error.message)));
2573
- return ReservedSettlement.make({
2574
- submissionId: validated.submissionId,
2575
- settlementId: validated.settlementId,
2576
- outcome: validated.outcome,
2577
- record,
2578
- recordDigest: validated.recordDigest,
2579
- replayed: true
2580
- });
2581
- }
2582
- const submission = yield* requireSubmission(operation, validated.submissionId);
2583
- if (submission.state === "settled") {
2584
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2585
- return yield* SettlementConflict.make({
2586
- submissionId: validated.submissionId,
2587
- existingOutcome: submission.settled_outcome
2588
- });
2589
- }
2590
- if (!(submission.state === "joined" && submission.joined_host_submission_id !== null)) {
2591
- let queuedAbortSettlement = false;
2592
- if (validated.outcome === "aborted" && (submission.state === "ready" || submission.state === "terminalizing")) {
2593
- const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
2594
- if (Option.isSome(abortIntent)) {
2595
- const ownership = yield* readOwnership(operation, validated.submissionId);
2596
- queuedAbortSettlement = Option.isNone(ownership);
2597
- }
2598
- }
2599
- if (!queuedAbortSettlement) yield* requireOwnership(operation, submission, validated.ownershipToken);
2600
- }
2601
- const now = yield* currentInstant;
2602
- yield* sql`
2603
- INSERT INTO effect_agent_settlement_reservations (
2604
- submission_id,
2605
- settlement_id,
2606
- outcome,
2607
- record_id,
2608
- record_json,
2609
- record_digest,
2610
- reserved_at
2611
- ) VALUES (
2612
- ${validated.submissionId},
2613
- ${validated.settlementId},
2614
- ${validated.outcome},
2615
- ${validated.record.recordId},
2616
- ${recordJson},
2617
- ${validated.recordDigest},
2618
- ${now.iso}
2619
- )
2620
- `.pipe(Effect.mapError(sqlFailure(operation)));
2621
- yield* sql`
2622
- UPDATE effect_agent_submissions
2623
- SET state = 'terminalizing'
2624
- WHERE submission_id = ${validated.submissionId}
2625
- `.pipe(Effect.mapError(sqlFailure(operation)));
2626
- return ReservedSettlement.make({
2627
- submissionId: validated.submissionId,
2628
- settlementId: validated.settlementId,
2629
- outcome: validated.outcome,
2630
- record: validated.record,
2631
- recordDigest: validated.recordDigest,
2632
- replayed: false
2633
- });
2634
- }));
2635
- yield* hitFailpoint("ledger:reserve-settlement:after", operation);
2636
- return reserved;
2637
- });
2638
- const finalizeSettlement = Effect.fn("SqliteSubmissionLedger.finalizeSettlement")(function* (request) {
2639
- const operation = "ledger finalize settlement";
2640
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementFinalization))(request).pipe(Effect.mapError(internalFailure(operation)));
2641
- yield* hitFailpoint("ledger:finalize-settlement:before", operation);
2642
- const settlement = yield* inWriteTransaction(operation, Effect.gen(function* () {
2643
- const reservation = yield* readReservation(operation, validated.submissionId);
2644
- if (Option.isNone(reservation)) return yield* LedgerError.make({
2645
- operation,
2646
- message: `No settlement reservation exists for submission ${validated.submissionId}.`
2647
- });
2648
- if (reservation.value.settlement_id !== validated.settlementId) return yield* SettlementConflict.make({
2649
- submissionId: validated.submissionId,
2650
- existingOutcome: reservation.value.outcome
2651
- });
2652
- const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, error.message)));
2653
- const settlementFailure = settlementFailureFromRecord(reservationRecord);
2654
- if (reservation.value.outcome === "failed" !== (settlementFailure !== void 0)) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, "The reserved outcome and canonical failure diagnostic disagree.");
2655
- const submission = yield* requireSubmission(operation, validated.submissionId);
2656
- if (submission.state === "settled") {
2657
- 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.");
2658
- return yield* decodeSettlement({
2659
- submissionId: validated.submissionId,
2660
- settlementId: validated.settlementId,
2661
- receiptId: submission.receipt_id,
2662
- outcome: reservation.value.outcome,
2663
- ...settlementFailure === void 0 ? {} : { failure: settlementFailure },
2664
- settledAt: reservation.value.finalized_at
2665
- }).pipe(Effect.mapError(internalFailure(operation)));
2666
- }
2667
- const now = yield* currentInstant;
2668
- yield* sql`
2669
- UPDATE effect_agent_submissions
2670
- SET state = 'settled', settled_outcome = ${reservation.value.outcome}
2671
- WHERE submission_id = ${validated.submissionId}
2672
- `.pipe(Effect.mapError(sqlFailure(operation)));
2673
- yield* sql`
2674
- UPDATE effect_agent_settlement_reservations
2675
- SET finalized_at = ${now.iso}
2676
- WHERE submission_id = ${validated.submissionId}
2677
- `.pipe(Effect.mapError(sqlFailure(operation)));
2678
- yield* sql`
2679
- DELETE FROM effect_agent_submission_ownership
2680
- WHERE submission_id = ${validated.submissionId}
2681
- `.pipe(Effect.mapError(sqlFailure(operation)));
2682
- return yield* decodeSettlement({
2683
- submissionId: validated.submissionId,
2684
- settlementId: validated.settlementId,
2685
- receiptId: submission.receipt_id,
2686
- outcome: reservation.value.outcome,
2687
- ...settlementFailure === void 0 ? {} : { failure: settlementFailure },
2688
- settledAt: now.iso
2689
- }).pipe(Effect.mapError(internalFailure(operation)));
2690
- }));
2691
- yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2692
- return settlement;
2693
- });
2694
- const requestAbort = Effect.fn("SqliteSubmissionLedger.requestAbort")(function* (request) {
2695
- const operation = "ledger request abort";
2696
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AbortCommand))(request).pipe(Effect.mapError(internalFailure(operation)));
2697
- yield* hitFailpoint("ledger:request-abort:before", operation);
2698
- const intent = yield* inWriteTransaction(operation, Effect.gen(function* () {
2699
- const submission = yield* requireSubmission(operation, validated.submissionId);
2700
- if (submission.state === "settled") {
2701
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2702
- return yield* SettlementConflict.make({
2703
- submissionId: validated.submissionId,
2704
- existingOutcome: submission.settled_outcome
2705
- });
2706
- }
2707
- if (submission.state === "joined") {
2708
- if (submission.joined_host_submission_id === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A joined Submission carries no host linkage.");
2709
- const hostSubmissionId = yield* decodeSubmissionId(submission.joined_host_submission_id).pipe(Effect.mapError(internalFailure(operation)));
2710
- return yield* JoinedToHost.make({
2711
- submissionId: validated.submissionId,
2712
- hostSubmissionId
2713
- });
2714
- }
2715
- const existing = yield* readAbortIntent(operation, validated.submissionId);
2716
- if (Option.isSome(existing)) return yield* abortIntentFromRow(operation, submission, validated.submissionId, existing.value);
2717
- const now = yield* currentInstant;
2718
- yield* sql`
2719
- INSERT INTO effect_agent_abort_intents (
2720
- submission_id,
2721
- author,
2722
- reason,
2723
- requested_at
2724
- ) VALUES (
2725
- ${validated.submissionId},
2726
- ${validated.author},
2727
- ${validated.reason},
2728
- ${now.iso}
2729
- )
2730
- `.pipe(Effect.mapError(sqlFailure(operation)));
2731
- const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, validated.submissionId);
2732
- return yield* decodeAbortIntent({
2733
- submissionId: validated.submissionId,
2734
- author: validated.author,
2735
- reason: validated.reason,
2736
- requestedAt: now.iso,
2737
- ...canonicalRecordId === void 0 ? {} : { canonicalRecordId }
2738
- }).pipe(Effect.mapError(internalFailure(operation)));
2739
- }));
2740
- yield* hitFailpoint("ledger:request-abort:after", operation);
2741
- return intent;
2742
- });
2743
- const claimJoining = Effect.fn("SqliteSubmissionLedger.claimJoining")(function* (request) {
2744
- const operation = "ledger claim joining";
2745
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2746
- yield* hitFailpoint("ledger:claim-joining:before", operation);
2747
- const claims = yield* inWriteTransaction(operation, Effect.gen(function* () {
2748
- const host = yield* requireSubmission(operation, validated.hostSubmissionId);
2749
- if (host.thread_id !== validated.threadId) return yield* LedgerError.make({
2750
- operation,
2751
- message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`
2752
- });
2753
- yield* requireOwnership(operation, host, validated.ownershipToken);
2754
- const laterRows = yield* sql`
2755
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2756
- FROM effect_agent_submissions
2757
- WHERE thread_id = ${validated.threadId}
2758
- AND queue_sequence > ${host.queue_sequence}
2759
- ORDER BY queue_sequence ASC
2760
- `.pipe(Effect.mapError(sqlFailure(operation)));
2761
- const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
2762
- const claimed = [];
2763
- for (const row of later) {
2764
- if (claimed.length >= validated.maxCount) break;
2765
- if ((row.state === "joining" || row.state === "joined") && row.joined_host_submission_id === validated.hostSubmissionId) continue;
2766
- if (row.state === "settled" && row.settled_outcome === "aborted") continue;
2767
- if (row.state !== "ready") break;
2768
- yield* sql`
2769
- UPDATE effect_agent_submissions
2770
- SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
2771
- WHERE submission_id = ${row.submission_id}
2772
- `.pipe(Effect.mapError(sqlFailure(operation)));
2773
- const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
2774
- claimed.push(yield* decodeJoiningClaim({
2775
- submissionId: row.submission_id,
2776
- queueSequence: row.queue_sequence,
2777
- inputPayload
2778
- }).pipe(Effect.mapError(internalFailure(operation))));
2779
- }
2780
- return claimed;
2781
- }));
2782
- yield* hitFailpoint("ledger:claim-joining:after", operation);
2783
- return claims;
2784
- });
2785
- const markJoined = Effect.fn("SqliteSubmissionLedger.markJoined")(function* (request) {
2786
- const operation = "ledger mark joined";
2787
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkJoinedRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2788
- yield* hitFailpoint("ledger:mark-joined:before", operation);
2789
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2790
- const submission = yield* requireSubmission(operation, validated.submissionId);
2791
- if (submission.joined_host_submission_id === null) return yield* LedgerError.make({
2792
- operation,
2793
- message: `Submission ${validated.submissionId} was never claimed for joining.`
2794
- });
2795
- const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
2796
- yield* requireOwnership(operation, host, validated.ownershipToken);
2797
- if (submission.input_applied_record_id !== null) {
2798
- if (submission.input_applied_record_id === validated.recordId && submission.input_applied_sequence === validated.sequence) return;
2799
- return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A different join marker is already recorded for this Submission.");
2800
- }
2801
- if (submission.state !== "joining" && submission.state !== "joined") return yield* LedgerError.make({
2802
- operation,
2803
- message: `Cannot mark submission ${validated.submissionId} joined from state ${submission.state}.`
2804
- });
2805
- yield* sql`
2806
- UPDATE effect_agent_submissions
2807
- SET
2808
- input_applied_record_id = ${validated.recordId},
2809
- input_applied_sequence = ${validated.sequence},
2810
- state = 'joined'
2811
- WHERE submission_id = ${validated.submissionId}
2812
- `.pipe(Effect.mapError(sqlFailure(operation)));
2813
- }));
2814
- yield* hitFailpoint("ledger:mark-joined:after", operation);
2815
- });
2816
- const revertJoining = Effect.fn("SqliteSubmissionLedger.revertJoining")(function* (request) {
2817
- const operation = "ledger revert joining";
2818
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RevertJoiningRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2819
- yield* hitFailpoint("ledger:revert-joining:before", operation);
2820
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2821
- if ((yield* requireSubmission(operation, validated.submissionId)).state !== "joining") return;
2822
- yield* sql`
2823
- UPDATE effect_agent_submissions
2824
- SET state = 'ready', joined_host_submission_id = NULL
2825
- WHERE submission_id = ${validated.submissionId}
2826
- `.pipe(Effect.mapError(sqlFailure(operation)));
2827
- }));
2828
- yield* hitFailpoint("ledger:revert-joining:after", operation);
2829
- });
2830
- const suspend = Effect.fn("SqliteSubmissionLedger.suspend")(function* (request) {
2831
- const operation = "ledger suspend";
2832
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SuspendRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2833
- const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(Effect.mapError(internalFailure(operation)));
2834
- yield* hitFailpoint("ledger:suspend:before", operation);
2835
- const outcome = yield* inWriteTransaction(operation, Effect.gen(function* () {
2836
- const submission = yield* requireSubmission(operation, validated.submissionId);
2837
- if (submission.state === "settled") {
2838
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2839
- return yield* SettlementConflict.make({
2840
- submissionId: validated.submissionId,
2841
- existingOutcome: submission.settled_outcome
2842
- });
2843
- }
2844
- const reservation = yield* readReservation(operation, validated.submissionId);
2845
- if (Option.isSome(reservation)) return yield* SettlementConflict.make({
2846
- submissionId: validated.submissionId,
2847
- existingOutcome: reservation.value.outcome
2848
- });
2849
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2850
- if (validated.reason._tag === "ApprovalPending") {
2851
- const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
2852
- const decided = new Set(decisions.map((row) => row.tool_call_id));
2853
- if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return RESUME_IMMEDIATELY;
2854
- } else {
2855
- let allSettled = true;
2856
- for (const child of validated.reason.children) {
2857
- const childRow = yield* readSubmission(operation, child.childSubmissionId);
2858
- if (Option.isNone(childRow) || childRow.value.state !== "settled") {
2859
- allSettled = false;
2860
- break;
2861
- }
2862
- }
2863
- if (allSettled) return RESUME_IMMEDIATELY;
2864
- }
2865
- const now = yield* currentInstant;
2866
- yield* sql`
2867
- UPDATE effect_agent_submissions
2868
- SET
2869
- state = 'suspended',
2870
- suspended_reason_json = ${reasonJson},
2871
- suspended_at = ${now.iso}
2872
- WHERE submission_id = ${validated.submissionId}
2873
- `.pipe(Effect.mapError(sqlFailure(operation)));
2874
- yield* sql`
2875
- DELETE FROM effect_agent_submission_ownership
2876
- WHERE submission_id = ${validated.submissionId}
2877
- `.pipe(Effect.mapError(sqlFailure(operation)));
2878
- return SUSPENDED;
2879
- }));
2880
- yield* hitFailpoint("ledger:suspend:after", operation);
2881
- return outcome;
2882
- });
2883
- /**
2884
- * Once every pending call of a recorded ApprovalPending suspension has a decision intent,
2885
- * the lane wakes: suspended → input-applied, suspension cleared (plan §2.6). A
2886
- * WaitingForChild suspension wakes only through recordChildSettled. Runs inside the caller's
2887
- * write transaction.
2888
- */
2889
- const wakeSuspendedIfCovered = Effect.fn("SqliteSubmissionLedger.wakeSuspendedIfCovered")(function* (operation, submission) {
2890
- if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
2891
- 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)));
2892
- if (reason._tag !== "ApprovalPending") return;
2893
- const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
2894
- const decided = new Set(decisions.map((row) => row.tool_call_id));
2895
- if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
2896
- yield* sql`
2897
- UPDATE effect_agent_submissions
2898
- SET
2899
- state = 'input-applied',
2900
- suspended_reason_json = NULL,
2901
- suspended_at = NULL
2902
- WHERE submission_id = ${submission.submission_id}
2903
- `.pipe(Effect.mapError(sqlFailure(operation)));
2904
- });
2905
- const recordApprovalDecision = Effect.fn("SqliteSubmissionLedger.recordApprovalDecision")(function* (command) {
2906
- const operation = "ledger record approval decision";
2907
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ApprovalDecisionCommand))(command).pipe(Effect.mapError(internalFailure(operation)));
2908
- yield* hitFailpoint("ledger:approval-decision:before", operation);
2909
- const intent = yield* inWriteTransaction(operation, Effect.gen(function* () {
2910
- const submission = yield* requireSubmission(operation, validated.submissionId);
2911
- if (submission.state === "settled") {
2912
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2913
- return yield* SettlementConflict.make({
2914
- submissionId: validated.submissionId,
2915
- existingOutcome: submission.settled_outcome
2916
- });
2917
- }
2918
- const existing = (yield* readApprovalDecisions(operation, validated.submissionId)).find((row) => row.tool_call_id === validated.toolCallId);
2919
- if (existing !== void 0) {
2920
- if (existing.decision !== validated.decision) return yield* ApprovalConflict.make({
2921
- submissionId: validated.submissionId,
2922
- toolCallId: validated.toolCallId,
2923
- existingDecision: existing.decision
2924
- });
2925
- return yield* approvalIntentFromRow(operation, existing);
2926
- }
2927
- const now = yield* currentInstant;
2928
- yield* sql`
2929
- INSERT INTO effect_agent_approval_decisions (
2930
- submission_id,
2931
- tool_call_id,
2932
- decision,
2933
- resolver,
2934
- reason,
2935
- decided_at
2936
- ) VALUES (
2937
- ${validated.submissionId},
2938
- ${validated.toolCallId},
2939
- ${validated.decision},
2940
- ${validated.resolver},
2941
- ${validated.reason},
2942
- ${now.iso}
2943
- )
2944
- `.pipe(Effect.mapError(sqlFailure(operation)));
2945
- yield* wakeSuspendedIfCovered(operation, submission);
2946
- return yield* decodeApprovalDecisionIntent({
2947
- submissionId: validated.submissionId,
2948
- toolCallId: validated.toolCallId,
2949
- decision: validated.decision,
2950
- resolver: validated.resolver,
2951
- reason: validated.reason,
2952
- decidedAt: now.iso
2953
- }).pipe(Effect.mapError(internalFailure(operation)));
2954
- }));
2955
- yield* hitFailpoint("ledger:approval-decision:after", operation);
2956
- return intent;
2957
- });
2958
- const markUnknown = Effect.fn("SqliteSubmissionLedger.markUnknown")(function* (request) {
2959
- const operation = "ledger mark unknown";
2960
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkUnknownRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2961
- yield* hitFailpoint("ledger:mark-unknown:before", operation);
2962
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2963
- const submission = yield* requireSubmission(operation, validated.submissionId);
2964
- if (submission.state === "settled") {
2965
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2966
- return yield* SettlementConflict.make({
2967
- submissionId: validated.submissionId,
2968
- existingOutcome: submission.settled_outcome
2969
- });
2970
- }
2971
- const reservation = yield* readReservation(operation, validated.submissionId);
2972
- if (Option.isSome(reservation)) return yield* SettlementConflict.make({
2973
- submissionId: validated.submissionId,
2974
- existingOutcome: reservation.value.outcome
2975
- });
2976
- const existingIds = yield* storedUnknownToolCallIds(operation, submission);
2977
- const known = new Set(existingIds);
2978
- const merged = [...existingIds, ...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId))];
2979
- const idsJson = yield* encodeToolCallIdsText(merged).pipe(Effect.mapError(internalFailure(operation)));
2980
- yield* sql`
2981
- UPDATE effect_agent_submissions
2982
- SET
2983
- state = 'unknown',
2984
- unknown_reason = ${submission.unknown_reason ?? validated.reason},
2985
- unknown_tool_call_ids_json = ${idsJson}
2986
- WHERE submission_id = ${validated.submissionId}
2987
- `.pipe(Effect.mapError(sqlFailure(operation)));
2988
- }));
2989
- yield* hitFailpoint("ledger:mark-unknown:after", operation);
2990
- });
2991
- const recordUnknownResolution = Effect.fn("SqliteSubmissionLedger.recordUnknownResolution")(function* (command) {
2992
- const operation = "ledger record unknown resolution";
2993
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(UnknownResolutionCommand))(command).pipe(Effect.mapError(internalFailure(operation)));
2994
- const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(Effect.mapError(internalFailure(operation)));
2995
- yield* hitFailpoint("ledger:unknown-resolution:before", operation);
2996
- const intent = yield* inWriteTransaction(operation, Effect.gen(function* () {
2997
- const submission = yield* requireSubmission(operation, validated.submissionId);
2998
- if (submission.state === "settled") {
2999
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
3000
- return yield* SettlementConflict.make({
3001
- submissionId: validated.submissionId,
3002
- existingOutcome: submission.settled_outcome
3003
- });
3004
- }
3005
- const existing = (yield* readUnknownResolutions(operation, validated.submissionId)).find((row) => row.tool_call_id === validated.toolCallId);
3006
- const existingIntent = existing === void 0 ? void 0 : yield* unknownResolutionIntentFromRow(operation, existing);
3007
- if (existingIntent !== void 0 && !equivalentUnknownResolution(existingIntent.resolution, validated.resolution)) return yield* UnknownResolutionConflict.make({
3008
- submissionId: validated.submissionId,
3009
- toolCallId: validated.toolCallId
3010
- });
3011
- let resolved;
3012
- if (existingIntent !== void 0) resolved = existingIntent;
3013
- else {
3014
- const now = yield* currentInstant;
3015
- yield* sql`
3016
- INSERT INTO effect_agent_unknown_resolutions (
3017
- submission_id,
3018
- tool_call_id,
3019
- author,
3020
- reason,
3021
- resolution_json,
3022
- resolved_at
3023
- ) VALUES (
3024
- ${validated.submissionId},
3025
- ${validated.toolCallId},
3026
- ${validated.author},
3027
- ${validated.reason},
3028
- ${resolutionJson},
3029
- ${now.iso}
3030
- )
3031
- `.pipe(Effect.mapError(sqlFailure(operation)));
3032
- const resolution = yield* parseStoredJsonText(resolutionJson).pipe(Effect.mapError(internalFailure(operation)));
3033
- resolved = yield* decodeUnknownResolutionIntent({
3034
- submissionId: validated.submissionId,
3035
- toolCallId: validated.toolCallId,
3036
- author: validated.author,
3037
- reason: validated.reason,
3038
- resolution,
3039
- resolvedAt: now.iso
3040
- }).pipe(Effect.mapError(internalFailure(operation)));
3041
- }
3042
- if (submission.state === "unknown" && submission.unknown_tool_call_ids_json !== null) {
3043
- const markedIds = yield* storedUnknownToolCallIds(operation, submission);
3044
- const covering = yield* readUnknownResolutions(operation, validated.submissionId);
3045
- const coveredIds = new Set(covering.map((row) => row.tool_call_id));
3046
- if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) yield* sql`
3047
- UPDATE effect_agent_submissions
3048
- SET
3049
- state = 'input-applied',
3050
- unknown_reason = NULL,
3051
- unknown_tool_call_ids_json = NULL
3052
- WHERE submission_id = ${validated.submissionId}
3053
- `.pipe(Effect.mapError(sqlFailure(operation)));
3054
- }
3055
- return resolved;
3056
- }));
3057
- yield* hitFailpoint("ledger:unknown-resolution:after", operation);
3058
- return intent;
3059
- });
3060
- const recordChildSettled = Effect.fn("SqliteSubmissionLedger.recordChildSettled")(function* (request) {
3061
- const operation = "ledger record child settled";
3062
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildSettledNotification))(request).pipe(Effect.mapError(internalFailure(operation)));
3063
- yield* hitFailpoint("ledger:child-settled:before", operation);
3064
- const outcome = yield* inWriteTransaction(operation, Effect.gen(function* () {
3065
- const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
3066
- const child = yield* readSubmission(operation, validated.childSubmissionId);
3067
- const childReservation = yield* readReservation(operation, validated.childSubmissionId);
3068
- if (!(Option.isSome(child) && (child.value.state === "settled" || child.value.state === "terminalizing" && Option.isSome(childReservation)))) return yield* LedgerError.make({
3069
- operation,
3070
- message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`
3071
- });
3072
- if (parent.state !== "suspended" || parent.suspended_reason_json === null) return NOT_WAITING;
3073
- 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)));
3074
- if (reason._tag !== "WaitingForChild") return NOT_WAITING;
3075
- if (!reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)) return NOT_WAITING;
3076
- for (const entry of reason.children) {
3077
- const listed = yield* readSubmission(operation, entry.childSubmissionId);
3078
- const reservation = yield* readReservation(operation, entry.childSubmissionId);
3079
- if (!(Option.isSome(listed) && (listed.value.state === "settled" || listed.value.state === "terminalizing" && Option.isSome(reservation)))) return STILL_WAITING;
3080
- }
3081
- yield* sql`
3082
- UPDATE effect_agent_submissions
3083
- SET
3084
- state = 'input-applied',
3085
- suspended_reason_json = NULL,
3086
- suspended_at = NULL
3087
- WHERE submission_id = ${validated.parentSubmissionId}
3088
- `.pipe(Effect.mapError(sqlFailure(operation)));
3089
- return WOKEN;
3090
- }));
3091
- yield* hitFailpoint("ledger:child-settled:after", operation);
3092
- return outcome;
3093
- });
3094
- const reserveChildBudget = Effect.fn("SqliteSubmissionLedger.reserveChildBudget")(function* (request) {
3095
- const operation = "ledger reserve child budget";
3096
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildBudgetReservationRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
3097
- const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(Effect.mapError(internalFailure(operation)));
3098
- yield* hitFailpoint("ledger:child-reservation:before", operation);
3099
- const reserved = yield* inWriteTransaction(operation, Effect.gen(function* () {
3100
- const existing = yield* readChildReservation(operation, validated.reservationId);
3101
- if (Option.isSome(existing)) {
3102
- const existingSnapshot = yield* childReservationSnapshotFromRow(operation, existing.value);
3103
- if (!(existing.value.parent_submission_id === validated.parentSubmissionId && existing.value.parent_tool_call_id === validated.parentToolCallId && existing.value.allocation_digest === validated.allocationDigest && equivalentPersistedJson(existingSnapshot.allocation, validated.allocation))) return yield* ChildReservationConflict.make({
3104
- reservationId: validated.reservationId,
3105
- status: existing.value.status,
3106
- message: "A reservation with this identity exists with a different parent Tool Call or allocation."
3107
- });
3108
- return ReservedChildBudget.make({
3109
- reservation: existingSnapshot,
3110
- replayed: true
3111
- });
3112
- }
3113
- const collision = yield* readChildReservationForCall(operation, validated.parentSubmissionId, validated.parentToolCallId);
3114
- if (Option.isSome(collision)) return yield* ChildReservationConflict.make({
3115
- reservationId: validated.reservationId,
3116
- status: collision.value.status,
3117
- message: `Parent Tool Call ${validated.parentToolCallId} already owns reservation ${collision.value.reservation_id}.`
3118
- });
3119
- const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
3120
- yield* requireOwnership(operation, parent, validated.ownershipToken);
3121
- const now = yield* currentInstant;
3122
- yield* sql`
3123
- INSERT INTO effect_agent_child_reservations (
3124
- reservation_id,
3125
- parent_submission_id,
3126
- parent_tool_call_id,
3127
- status,
3128
- allocation_json,
3129
- allocation_digest,
3130
- reserved_at
3131
- ) VALUES (
3132
- ${validated.reservationId},
3133
- ${validated.parentSubmissionId},
3134
- ${validated.parentToolCallId},
3135
- 'reserved',
3136
- ${allocationJson},
3137
- ${validated.allocationDigest},
3138
- ${now.iso}
3139
- )
3140
- `.pipe(Effect.mapError(sqlFailure(operation)));
3141
- const inserted = yield* readChildReservation(operation, validated.reservationId);
3142
- 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.");
3143
- return ReservedChildBudget.make({
3144
- reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
3145
- replayed: false
3146
- });
3147
- }));
3148
- yield* hitFailpoint("ledger:child-reservation:after", operation);
3149
- return reserved;
3150
- });
3151
- const attachChildToReservation = Effect.fn("SqliteSubmissionLedger.attachChildToReservation")(function* (request) {
3152
- const operation = "ledger attach child to reservation";
3153
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AttachChildToReservationRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
3154
- yield* hitFailpoint("ledger:child-attach:before", operation);
3155
- const attached = yield* inWriteTransaction(operation, Effect.gen(function* () {
3156
- const existing = yield* readChildReservation(operation, validated.reservationId);
3157
- if (Option.isNone(existing)) return yield* LedgerError.make({
3158
- operation,
3159
- message: `Unknown child reservation ${validated.reservationId}.`
3160
- });
3161
- if (existing.value.child_submission_id !== null) {
3162
- if (existing.value.child_submission_id === validated.childSubmissionId) return yield* childReservationSnapshotFromRow(operation, existing.value);
3163
- return yield* ChildReservationConflict.make({
3164
- reservationId: validated.reservationId,
3165
- status: existing.value.status,
3166
- message: `Reservation ${validated.reservationId} already records child ${existing.value.child_submission_id}.`
3167
- });
3168
- }
3169
- const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
3170
- yield* requireOwnership(operation, parent, validated.ownershipToken);
3171
- if (existing.value.status !== "reserved") return yield* ChildReservationConflict.make({
3172
- reservationId: validated.reservationId,
3173
- status: existing.value.status,
3174
- message: `Cannot attach a child to a ${existing.value.status} reservation.`
3175
- });
3176
- const child = yield* readSubmission(operation, validated.childSubmissionId);
3177
- if (Option.isNone(child)) return yield* LedgerError.make({
3178
- operation,
3179
- message: `Unknown child submission ${validated.childSubmissionId}.`
3180
- });
3181
- yield* sql`
3182
- UPDATE effect_agent_child_reservations
3183
- SET child_submission_id = ${validated.childSubmissionId}
3184
- WHERE reservation_id = ${validated.reservationId}
3185
- `.pipe(Effect.mapError(sqlFailure(operation)));
3186
- const updated = yield* readChildReservation(operation, validated.reservationId);
3187
- 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.");
3188
- return yield* childReservationSnapshotFromRow(operation, updated.value);
3189
- }));
3190
- yield* hitFailpoint("ledger:child-attach:after", operation);
3191
- return attached;
3192
- });
3193
- const beginChildBudgetRelease = Effect.fn("SqliteSubmissionLedger.beginChildBudgetRelease")(function* (request) {
3194
- const operation = "ledger begin child budget release";
3195
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(BeginChildBudgetReleaseRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
3196
- const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(Effect.mapError(internalFailure(operation)));
3197
- yield* hitFailpoint("ledger:child-release-pending:before", operation);
3198
- const frozen = yield* inWriteTransaction(operation, Effect.gen(function* () {
3199
- const existing = yield* readChildReservation(operation, validated.reservationId);
3200
- if (Option.isNone(existing)) return yield* LedgerError.make({
3201
- operation,
3202
- message: `Unknown child reservation ${validated.reservationId}.`
3203
- });
3204
- if (existing.value.status !== "reserved") {
3205
- const existingSnapshot = yield* childReservationSnapshotFromRow(operation, existing.value);
3206
- if (existingSnapshot.accounting !== void 0 && equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)) return existingSnapshot;
3207
- return yield* ChildReservationConflict.make({
3208
- reservationId: validated.reservationId,
3209
- status: existing.value.status,
3210
- message: "A different accounting decision is already frozen for this reservation."
3211
- });
3212
- }
3213
- const now = yield* currentInstant;
3214
- yield* sql`
3215
- UPDATE effect_agent_child_reservations
3216
- SET
3217
- status = 'releasePending',
3218
- accounting_json = ${accountingJson},
3219
- release_began_at = ${now.iso}
3220
- WHERE reservation_id = ${validated.reservationId}
3221
- `.pipe(Effect.mapError(sqlFailure(operation)));
3222
- const updated = yield* readChildReservation(operation, validated.reservationId);
3223
- 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.");
3224
- return yield* childReservationSnapshotFromRow(operation, updated.value);
3225
- }));
3226
- yield* hitFailpoint("ledger:child-release-pending:after", operation);
3227
- return frozen;
3228
- });
3229
- const releaseChildBudget = Effect.fn("SqliteSubmissionLedger.releaseChildBudget")(function* (request) {
3230
- const operation = "ledger release child budget";
3231
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseChildBudgetRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
3232
- yield* hitFailpoint("ledger:child-release:before", operation);
3233
- const released = yield* inWriteTransaction(operation, Effect.gen(function* () {
3234
- const existing = yield* readChildReservation(operation, validated.reservationId);
3235
- if (Option.isNone(existing)) return yield* LedgerError.make({
3236
- operation,
3237
- message: `Unknown child reservation ${validated.reservationId}.`
3238
- });
3239
- if (existing.value.status === "released") return yield* childReservationSnapshotFromRow(operation, existing.value);
3240
- if (existing.value.status !== "releasePending") return yield* ChildReservationConflict.make({
3241
- reservationId: validated.reservationId,
3242
- status: existing.value.status,
3243
- message: "Cannot release a reservation whose accounting decision is not frozen."
3244
- });
3245
- const now = yield* currentInstant;
3246
- yield* sql`
3247
- UPDATE effect_agent_child_reservations
3248
- SET status = 'released', released_at = ${now.iso}
3249
- WHERE reservation_id = ${validated.reservationId}
3250
- `.pipe(Effect.mapError(sqlFailure(operation)));
3251
- const updated = yield* readChildReservation(operation, validated.reservationId);
3252
- 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.");
3253
- return yield* childReservationSnapshotFromRow(operation, updated.value);
3254
- }));
3255
- yield* hitFailpoint("ledger:child-release:after", operation);
3256
- return released;
3257
- });
3258
- const scanPage = Effect.fn("SqliteSubmissionLedger.scanPage")(function* (cursor) {
3259
- const operation = "ledger scan nonterminal";
3260
- const rows = yield* (cursor === void 0 ? sql`
3261
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3262
- FROM effect_agent_submissions
3263
- WHERE state <> 'settled'
3264
- ORDER BY thread_id ASC, queue_sequence ASC
3265
- LIMIT ${SCAN_PAGE_SIZE}
3266
- ` : sql`
3267
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3268
- FROM effect_agent_submissions
3269
- WHERE state <> 'settled'
3270
- AND (
3271
- thread_id > ${cursor.threadId}
3272
- OR (
3273
- thread_id = ${cursor.threadId}
3274
- AND queue_sequence > ${cursor.queueSequence}
3275
- )
3276
- )
3277
- ORDER BY thread_id ASC, queue_sequence ASC
3278
- LIMIT ${SCAN_PAGE_SIZE}
3279
- `).pipe(Effect.mapError(sqlFailure(operation)));
3280
- const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
3281
- const snapshots = yield* Effect.forEach(decoded, (row) => decodeSubmissionSnapshot(operation, row));
3282
- const last = decoded[decoded.length - 1];
3283
- return [snapshots, last === void 0 || decoded.length < SCAN_PAGE_SIZE ? Option.none() : Option.some({
3284
- threadId: last.thread_id,
3285
- queueSequence: last.queue_sequence
3286
- })];
3287
- });
3288
- const scanNonterminal = Stream.paginate(void 0, scanPage);
3289
- const loadRecoverySnapshot = Effect.fn("SqliteSubmissionLedger.loadRecoverySnapshot")(function* (request) {
3290
- const operation = "ledger load recovery snapshot";
3291
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RecoverySnapshotRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
3292
- return yield* sql.withTransaction(Effect.gen(function* () {
3293
- const submissionRow = yield* requireSubmission(operation, validated.submissionId);
3294
- const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
3295
- let ownership;
3296
- const ownershipRow = yield* readOwnership(operation, validated.submissionId);
3297
- if (Option.isSome(ownershipRow)) ownership = yield* decodeOwnershipSnapshot({
3298
- attemptId: ownershipRow.value.attempt_id,
3299
- ownerProducerId: ownershipRow.value.owner_producer_id,
3300
- producerEpoch: ownershipRow.value.producer_epoch,
3301
- leaseExpiresAt: ownershipRow.value.lease_expires_at
3302
- }).pipe(Effect.mapError(internalFailure(operation)));
3303
- let inputApplied;
3304
- if (submissionRow.input_applied_record_id !== null && submissionRow.input_applied_sequence !== null) inputApplied = yield* decodeInputAppliedMarker({
3305
- recordId: submissionRow.input_applied_record_id,
3306
- sequence: submissionRow.input_applied_sequence
3307
- }).pipe(Effect.mapError(internalFailure(operation)));
3308
- let reservation;
3309
- const reservationRow = yield* readReservation(operation, validated.submissionId);
3310
- if (Option.isSome(reservationRow)) {
3311
- const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, error.message)));
3312
- const settlementId = yield* Schema.decodeUnknownEffect(SettlementReservationSnapshot.fields.settlementId)(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
3313
- reservation = SettlementReservationSnapshot.make({
3314
- settlementId,
3315
- outcome: reservationRow.value.outcome,
3316
- record,
3317
- recordDigest: reservationRow.value.record_digest,
3318
- finalized: reservationRow.value.finalized_at !== null
3319
- });
3320
- }
3321
- let abortIntent;
3322
- const abortRow = yield* readAbortIntent(operation, validated.submissionId);
3323
- if (Option.isSome(abortRow)) abortIntent = yield* abortIntentFromRow(operation, submissionRow, validated.submissionId, abortRow.value);
3324
- const joinRows = yield* sql`
3325
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3326
- FROM effect_agent_submissions
3327
- WHERE joined_host_submission_id = ${validated.submissionId}
3328
- ORDER BY queue_sequence ASC
3329
- `.pipe(Effect.mapError(sqlFailure(operation)));
3330
- const joinSubmissions = yield* decodeSubmissionRows(operation, validated.submissionId, joinRows);
3331
- const joins = yield* Effect.forEach(joinSubmissions, (row) => decodeJoinSnapshot({
3332
- submissionId: row.submission_id,
3333
- state: row.state,
3334
- hostSubmissionId: validated.submissionId
3335
- }).pipe(Effect.mapError(internalFailure(operation))));
3336
- let hostSubmissionId;
3337
- if (submissionRow.joined_host_submission_id !== null) hostSubmissionId = yield* decodeSubmissionId(submissionRow.joined_host_submission_id).pipe(Effect.mapError(internalFailure(operation)));
3338
- let suspension;
3339
- if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
3340
- const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, error.message)));
3341
- suspension = yield* decodeSuspensionSnapshot({
3342
- reason,
3343
- suspendedAt: submissionRow.suspended_at
3344
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, error.message)));
3345
- }
3346
- const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3347
- const approvalDecisions = yield* Effect.forEach(decisionRows, (row) => approvalIntentFromRow(operation, row));
3348
- const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3349
- const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) => unknownResolutionIntentFromRow(operation, row));
3350
- const childReservationRows = yield* sql`
3351
- SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
3352
- FROM effect_agent_child_reservations
3353
- WHERE parent_submission_id = ${validated.submissionId}
3354
- ORDER BY parent_tool_call_id ASC
3355
- `.pipe(Effect.mapError(sqlFailure(operation)));
3356
- const decodedChildReservations = yield* decodeChildReservationRows(operation, validated.submissionId, childReservationRows);
3357
- const childReservations = yield* Effect.forEach(decodedChildReservations, (row) => childReservationSnapshotFromRow(operation, row));
3358
- const childAttachments = [];
3359
- for (const row of decodedChildReservations) {
3360
- if (row.child_submission_id === null) continue;
3361
- const child = yield* readSubmission(operation, row.child_submission_id);
3362
- if (Option.isNone(child)) continue;
3363
- childAttachments.push(yield* decodeChildAttachmentSnapshot({
3364
- toolCallId: row.parent_tool_call_id,
3365
- childSubmissionId: row.child_submission_id,
3366
- childState: child.value.state,
3367
- ...child.value.settled_outcome === null ? {} : { childOutcome: child.value.settled_outcome }
3368
- }).pipe(Effect.mapError(internalFailure(operation))));
3369
- }
3370
- let parentLinkage;
3371
- if (submissionRow.parent_submission_id !== null && submissionRow.parent_tool_call_id !== null) parentLinkage = yield* decodeParentLinkage({
3372
- parentSubmissionId: submissionRow.parent_submission_id,
3373
- parentToolCallId: submissionRow.parent_tool_call_id
3374
- }).pipe(Effect.mapError(internalFailure(operation)));
3375
- return RecoverySnapshot.make({
3376
- submission,
3377
- joins,
3378
- approvalDecisions,
3379
- unknownResolutions,
3380
- childReservations,
3381
- childAttachments,
3382
- ...parentLinkage === void 0 ? {} : { parentLinkage },
3383
- ...hostSubmissionId === void 0 ? {} : { hostSubmissionId },
3384
- ...suspension === void 0 ? {} : { suspension },
3385
- ...ownership === void 0 ? {} : { ownership },
3386
- ...inputApplied === void 0 ? {} : { inputApplied },
3387
- ...reservation === void 0 ? {} : { reservation },
3388
- ...abortIntent === void 0 ? {} : { abortIntent }
3389
- });
3390
- })).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(sqlFailure(operation)(error))));
3391
- });
3392
- return Context.make(SubmissionLedger, SubmissionLedger.of({
3393
- capabilities,
3394
- admit,
3395
- markReady,
3396
- lookup,
3397
- resolveAdmission,
3398
- claim,
3399
- renewOwnership,
3400
- releaseOwnership,
3401
- markInputApplied,
3402
- reserveSettlement,
3403
- finalizeSettlement,
3404
- requestAbort,
3405
- claimJoining,
3406
- markJoined,
3407
- revertJoining,
3408
- suspend,
3409
- recordApprovalDecision,
3410
- markUnknown,
3411
- recordUnknownResolution,
3412
- recordChildSettled,
3413
- reserveChildBudget,
3414
- attachChildToReservation,
3415
- beginChildBudgetRelease,
3416
- releaseChildBudget,
3417
- scanNonterminal,
3418
- loadRecoverySnapshot
3419
- }));
3420
- });
3421
- /**
3422
- * SQLite SubmissionLedger implementation sharing the journal's database file, write
3423
- * transaction discipline, and producer-epoch fencing substrate. Configuration, failpoint,
3424
- * SQL, and Crypto authority stay visible in the input channel.
3425
- */
3426
- const submissionLedgerLayer = Layer.effectContext(makeServices());
3427
- /**
3428
- * A composition-root convenience Layer for the durable Submission Ledger. Point it at the
3429
- * same database file as the ThreadStore so claims fence the same producer epochs.
3430
- */
3431
- const ledgerLayer = (options) => Layer.unwrap(Effect.map(SqliteStorageConfig, (config) => submissionLedgerLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(SqliteStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ filename: options.filename }), NodeCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
3432
- //#endregion
3433
- //#region src/sqlite-schedule-store.ts
3434
- const StoredScheduleJson = Schema.String.check(Schema.isMaxLength(16777216));
3435
- const StoredDeadline = Schema.NullOr(ScheduleInstant);
3436
- var ScheduleRow = class extends Schema.Class("@effect-agent/storage-sqlite/ScheduleRow")({
3437
- tenant_id: ScheduleOwner.fields.tenantId,
3438
- owner_id: ScheduleOwner.fields.ownerId,
3439
- schedule_id: ScheduleId,
3440
- deadline_at_millis: StoredDeadline,
3441
- record_json: StoredScheduleJson
3442
- }) {};
3443
- const ScheduleDueRow = Schema.Struct({
3444
- tenant_id: ScheduleOwner.fields.tenantId,
3445
- owner_id: ScheduleOwner.fields.ownerId,
3446
- schedule_id: ScheduleId,
3447
- deadline_at_millis: ScheduleInstant
3448
- });
3449
- var ScheduleCountRow = class extends Schema.Class("@effect-agent/storage-sqlite/ScheduleCountRow")({ schedule_count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) }) {};
3450
- var ScheduleDeadlineRow = class extends Schema.Class("@effect-agent/storage-sqlite/ScheduleDeadlineRow")({ deadline_at_millis: StoredDeadline }) {};
3451
- const unavailable$1 = (operation) => ScheduleStorageError.make({
3452
- operation,
3453
- reason: "unavailable"
3454
- });
3455
- const corrupt$1 = (operation) => ScheduleStorageError.make({
3456
- operation,
3457
- reason: "corrupt"
3458
- });
3459
- const decodeRows$1 = Effect.fn("SqliteScheduleStore.decodeRows")(function* (schema, rows, operation) {
3460
- return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt$1(operation)));
3461
- });
3462
- const decodeRecord = Effect.fn("SqliteScheduleStore.decodeRecord")(function* (row) {
3463
- const record = yield* Schema.decodeEffect(Schema.fromJsonString(ScheduleRecord))(row.record_json).pipe(Effect.mapError(() => corrupt$1("decode schedule")));
3464
- if (record.owner.tenantId !== row.tenant_id || record.owner.ownerId !== row.owner_id || record.scheduleId !== row.schedule_id || scheduleDeadline(record) !== row.deadline_at_millis) return yield* corrupt$1("decode schedule identity");
3465
- return record;
3466
- });
3467
- const encodeRecord = Effect.fn("SqliteScheduleStore.encodeRecord")(function* (record) {
3468
- return yield* Schema.encodeEffect(Schema.fromJsonString(ScheduleRecord))(record).pipe(Effect.mapError(() => corrupt$1("encode schedule")));
3469
- });
3470
- const decodeInput = Effect.fn("SqliteScheduleStore.decodeInput")(function* (operation, schema, value) {
3471
- return yield* Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt$1(operation)));
3472
- });
3473
- const makeScheduleStore = Effect.gen(function* () {
3474
- const sql = yield* SqlClient.SqlClient;
3475
- const scheduleFailpoint = yield* ScheduleFailpoint;
3476
- yield* initializeSqliteJournal();
3477
- const readRows = Effect.fn("SqliteScheduleStore.readRows")(function* (key, operation) {
3478
- const rows = yield* sql`
3479
- SELECT tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
3480
- FROM effect_agent_schedules
3481
- WHERE tenant_id = ${key.owner.tenantId}
3482
- AND owner_id = ${key.owner.ownerId}
3483
- AND schedule_id = ${key.scheduleId}
3484
- `.pipe(Effect.mapError(() => unavailable$1(operation)));
3485
- return yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
3486
- });
3487
- const readOne = Effect.fn("SqliteScheduleStore.readOne")(function* (key, operation) {
3488
- const rows = yield* readRows(key, operation);
3489
- if (rows.length === 0) return null;
3490
- if (rows.length !== 1) return yield* corrupt$1(operation);
3491
- return yield* decodeRecord(rows[0]);
3492
- });
3493
- const insert = Effect.fn("SqliteScheduleStore.insert")(function* (record, ownerLimit) {
3494
- const operation = "insert schedule";
3495
- const canonical = yield* decodeInput(operation, ScheduleRecord, record);
3496
- const recordJson = yield* encodeRecord(canonical);
3497
- const result = yield* sql.withTransaction(Effect.gen(function* () {
3498
- const existing = yield* readOne(canonical, operation);
3499
- if (existing !== null) {
3500
- if (existing.creationFingerprint === canonical.creationFingerprint) return {
3501
- record: existing,
3502
- inserted: false
3503
- };
3504
- return yield* ScheduleConflict.make({
3505
- reason: "creation",
3506
- key: {
3507
- owner: canonical.owner,
3508
- scheduleId: canonical.scheduleId
3509
- }
3510
- });
3511
- }
3512
- const rawCounts = yield* sql`
3513
- SELECT COUNT(*) AS schedule_count
3514
- FROM effect_agent_schedules
3515
- WHERE tenant_id = ${canonical.owner.tenantId}
3516
- AND owner_id = ${canonical.owner.ownerId}
3517
- AND (json_extract(record_json, '$.pending') IS NOT NULL OR
3518
- (json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
3519
- `.pipe(Effect.mapError(() => unavailable$1(operation)));
3520
- const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
3521
- if (counts.length !== 1) return yield* corrupt$1(operation);
3522
- if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
3523
- yield* scheduleFailpoint.hit("schedule:insert:before");
3524
- yield* sql`
3525
- INSERT INTO effect_agent_schedules (
3526
- tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
3527
- ) VALUES (
3528
- ${canonical.owner.tenantId},
3529
- ${canonical.owner.ownerId},
3530
- ${canonical.scheduleId},
3531
- ${scheduleDeadline(canonical)},
3532
- ${recordJson}
3533
- )
3534
- `.pipe(Effect.mapError(() => unavailable$1(operation)));
3535
- return {
3536
- record: canonical,
3537
- inserted: true
3538
- };
3539
- })).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable$1(operation))));
3540
- if (result.inserted) yield* scheduleFailpoint.hit("schedule:insert:after");
3541
- return result.record;
3542
- });
3543
- const get = Effect.fn("SqliteScheduleStore.get")(function* (key) {
3544
- const decodedKey = yield* decodeInput("get schedule", ScheduleKey, key);
3545
- return yield* readOne(decodedKey, "get schedule");
3546
- });
3547
- const list = Effect.fn("SqliteScheduleStore.list")(function* (request) {
3548
- const operation = "list schedules";
3549
- const decodedRequest = yield* decodeInput(operation, SchedulePageRequest, request);
3550
- const rows = decodedRequest.after === void 0 ? yield* sql`
3551
- SELECT tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
3552
- FROM effect_agent_schedules
3553
- WHERE tenant_id = ${decodedRequest.owner.tenantId}
3554
- AND owner_id = ${decodedRequest.owner.ownerId}
3555
- ORDER BY schedule_id
3556
- LIMIT ${decodedRequest.limit + 1}
3557
- `.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
3558
- SELECT tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
3559
- FROM effect_agent_schedules
3560
- WHERE tenant_id = ${decodedRequest.owner.tenantId}
3561
- AND owner_id = ${decodedRequest.owner.ownerId}
3562
- AND schedule_id > ${decodedRequest.after}
3563
- ORDER BY schedule_id
3564
- LIMIT ${decodedRequest.limit + 1}
3565
- `.pipe(Effect.mapError(() => unavailable$1(operation)));
3566
- const decoded = yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
3567
- const records = yield* Effect.forEach(decoded, decodeRecord);
3568
- const hasNext = records.length > decodedRequest.limit;
3569
- const items = hasNext ? records.slice(0, decodedRequest.limit) : records;
3570
- return {
3571
- items,
3572
- next: hasNext ? items.at(-1)?.scheduleId ?? null : null
3573
- };
3574
- });
3575
- const change = Effect.fn("SqliteScheduleStore.change")(function* (key, change, ownerLimit = defaultSchedulingLimits.maxSchedulesPerOwner) {
3576
- const operation = "change schedule";
3577
- const decodedKey = yield* decodeInput(operation, ScheduleKey, key);
3578
- const decodedChange = yield* decodeInput(operation, ScheduleChange, change);
3579
- const result = yield* sql.withTransaction(Effect.gen(function* () {
3580
- const current = yield* readOne(decodedKey, operation);
3581
- if (current === null) return yield* ScheduleNotFound.make({ key: decodedKey });
3582
- const transition = applyScheduleChange(current, decodedChange);
3583
- if (Result.isFailure(transition)) return yield* transition.failure;
3584
- const next = transition.success;
3585
- if (!scheduleUsesCapacity(current) && scheduleUsesCapacity(next)) {
3586
- const rawCounts = yield* sql`
3587
- SELECT COUNT(*) AS schedule_count FROM effect_agent_schedules
3588
- WHERE tenant_id = ${key.owner.tenantId} AND owner_id = ${key.owner.ownerId}
3589
- AND (json_extract(record_json, '$.pending') IS NOT NULL OR
3590
- (json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
3591
- `.pipe(Effect.mapError(() => unavailable$1(operation)));
3592
- const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
3593
- if (counts.length !== 1) return yield* corrupt$1(operation);
3594
- if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
3595
- }
3596
- if (next === current) return {
3597
- record: current,
3598
- changed: false
3599
- };
3600
- const recordJson = yield* encodeRecord(next);
3601
- yield* scheduleFailpoint.hit(`schedule:${decodedChange._tag.toLowerCase()}:before`);
3602
- yield* sql`
3603
- UPDATE effect_agent_schedules
3604
- SET deadline_at_millis = ${scheduleDeadline(next)}, record_json = ${recordJson}
3605
- WHERE tenant_id = ${decodedKey.owner.tenantId}
3606
- AND owner_id = ${decodedKey.owner.ownerId}
3607
- AND schedule_id = ${decodedKey.scheduleId}
3608
- `.pipe(Effect.mapError(() => unavailable$1(operation)));
3609
- return {
3610
- record: next,
3611
- changed: true
3612
- };
3613
- })).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable$1(operation))));
3614
- if (result.changed) yield* scheduleFailpoint.hit(`schedule:${decodedChange._tag.toLowerCase()}:after`);
3615
- return result.record;
3616
- });
3617
- const due = Effect.fn("SqliteScheduleStore.due")(function* (nowMillis, limit, owner, after) {
3618
- const operation = "query due schedules";
3619
- const decodedOwner = owner === void 0 ? void 0 : yield* decodeInput(operation, ScheduleOwner, owner);
3620
- const cursor = after === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(Effect.mapError(() => corrupt$1(operation)));
3621
- const continuation = cursor === void 0 ? sql`1 = 1` : sql`
3622
- (deadline_at_millis, tenant_id, owner_id, schedule_id) >
3623
- (${cursor.deadlineAtMillis}, ${cursor.owner.tenantId}, ${cursor.owner.ownerId}, ${cursor.scheduleId})`;
3624
- const rows = decodedOwner === void 0 ? yield* sql`
3625
- SELECT tenant_id, owner_id, schedule_id, deadline_at_millis
3626
- FROM effect_agent_schedules
3627
- WHERE deadline_at_millis <= ${nowMillis} AND ${continuation}
3628
- ORDER BY deadline_at_millis, tenant_id, owner_id, schedule_id
3629
- LIMIT ${limit}
3630
- `.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
3631
- SELECT tenant_id, owner_id, schedule_id, deadline_at_millis
3632
- FROM effect_agent_schedules
3633
- WHERE tenant_id = ${decodedOwner.tenantId}
3634
- AND owner_id = ${decodedOwner.ownerId}
3635
- AND deadline_at_millis <= ${nowMillis} AND ${continuation}
3636
- ORDER BY deadline_at_millis, schedule_id
3637
- LIMIT ${limit}
3638
- `.pipe(Effect.mapError(() => unavailable$1(operation)));
3639
- return (yield* decodeRows$1(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
3640
- owner: {
3641
- tenantId: row.tenant_id,
3642
- ownerId: row.owner_id
3643
- },
3644
- scheduleId: row.schedule_id,
3645
- deadlineAtMillis: row.deadline_at_millis
3646
- }));
3647
- });
3648
- const nextDeadline = Effect.fn("SqliteScheduleStore.nextDeadline")(function* (owner) {
3649
- const operation = "query next schedule deadline";
3650
- const decodedOwner = owner === void 0 ? void 0 : yield* decodeInput(operation, ScheduleOwner, owner);
3651
- const rows = decodedOwner === void 0 ? yield* sql`
3652
- SELECT MIN(deadline_at_millis) AS deadline_at_millis
3653
- FROM effect_agent_schedules
3654
- WHERE deadline_at_millis IS NOT NULL
3655
- `.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
3656
- SELECT MIN(deadline_at_millis) AS deadline_at_millis
3657
- FROM effect_agent_schedules
3658
- WHERE tenant_id = ${decodedOwner.tenantId}
3659
- AND owner_id = ${decodedOwner.ownerId}
3660
- AND deadline_at_millis IS NOT NULL
3661
- `.pipe(Effect.mapError(() => unavailable$1(operation)));
3662
- const decoded = yield* decodeRows$1(Schema.Array(ScheduleDeadlineRow), rows, operation);
3663
- if (decoded.length !== 1) return yield* corrupt$1(operation);
3664
- return decoded[0].deadline_at_millis;
3665
- });
3666
- return ScheduleStore.of({
3667
- insert,
3668
- get,
3669
- list,
3670
- change,
3671
- due,
3672
- nextDeadline
3673
- });
3674
- });
3675
- /** SQLite implementation of the atomic ScheduleStore port. */
3676
- const scheduleStoreLayer = Layer.effect(ScheduleStore)(makeScheduleStore);
3677
- //#endregion
3678
- //#region src/sqlite-subscription-store.ts
3679
- const StoredJson = Schema.String.check(Schema.isMaxLength(16777216));
3680
- const CountRow = Schema.Struct({ count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) });
3681
- const SequenceRow = Schema.Struct({ sequence: Schema.Natural });
3682
- const ScanRow = Schema.Struct({
3683
- event_scan_cursor: Schema.String,
3684
- delivery_scan_cursor: Schema.String,
3685
- recovery_scan_cursor: Schema.Natural
3686
- });
3687
- const JsonRow = Schema.Struct({ record_json: StoredJson });
3688
- const RegistrationRow = Schema.Struct({
3689
- owner_id: Schema.String,
3690
- subscription_id: Schema.String,
3691
- ordinal: Schema.Natural,
3692
- source_name: Schema.String,
3693
- source_version: Schema.String,
3694
- matching_key: Schema.String,
3695
- state: SubscriptionRecord.fields.state,
3696
- expires_at_millis: Schema.Number,
3697
- recovery_at_millis: Schema.NullOr(Schema.Number),
3698
- record_json: StoredJson
3699
- });
3700
- const EventRow = Schema.Struct({
3701
- event_id: Schema.String,
3702
- source_name: Schema.String,
3703
- source_version: Schema.String,
3704
- matching_key: Schema.String,
3705
- payload_digest: Digest,
3706
- cutoff: Schema.Natural,
3707
- cursor: Schema.Natural,
3708
- routing_complete: Schema.Number,
3709
- next_attempt_at_millis: Schema.Number,
3710
- record_json: StoredJson
3711
- });
3712
- const DeliveryRow = Schema.Struct({
3713
- owner_id: Schema.String,
3714
- subscription_id: Schema.String,
3715
- event_id: Schema.String,
3716
- delivery_key: Schema.String,
3717
- state: SubscriptionDelivery.fields.state,
3718
- next_attempt_at_millis: Schema.Number,
3719
- record_json: StoredJson
3720
- });
3721
- const error = (reason, code) => SubscriptionError.make({
3722
- reason,
3723
- code
3724
- });
3725
- const unavailable = (operation) => error("storage", operation);
3726
- const corrupt = (operation) => error("corrupt", operation);
3727
- const bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
3728
- const validate = (schema, value, code) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => error("validation", code)));
3729
- const encode = (schema, value, code) => Schema.encodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
3730
- const decode = (schema, value, code) => Schema.decodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
3731
- const decodeRows = (schema, rows, code) => Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(Effect.mapError(() => corrupt(code)));
3732
- const sameDeliveryIdentity = (left, right) => subscriptionDeliveryKeyString(left.key) === subscriptionDeliveryKeyString(right.key) && left.deliveryId === right.deliveryId && left.source.name === right.source.name && left.source.version === right.source.version && left.threadId === right.threadId && left.admissionKey === right.admissionKey && left.subscriptionFingerprint === right.subscriptionFingerprint && left.eventDigest === right.eventDigest;
3733
- const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function* (owned) {
3734
- const partition = yield* validate(SourcePartition, owned, "partition");
3735
- const sql = yield* SqlClient.SqlClient;
3736
- const failpoint = yield* SubscriptionFailpoint;
3737
- yield* initializeSqliteJournal();
3738
- yield* sql`
3739
- INSERT INTO effect_agent_subscription_sequences (
3740
- tenant_id, source_address, sequence, event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
3741
- ) VALUES (${partition.tenantId}, ${partition.address}, 0, '', '', 0) ON CONFLICT DO NOTHING
3742
- `.pipe(Effect.mapError(() => unavailable("initialize subscription partition")));
3743
- const transact = (effect) => sql.withTransaction(effect).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable("transaction"))));
3744
- const query = (effect, code) => effect.pipe(Effect.mapError(() => unavailable(code)));
3745
- const requirePartition = (candidate, code) => sameSourcePartition(candidate, partition) ? Effect.void : Effect.fail(error("validation", code));
3746
- const requireKey = Effect.fn("SqliteSubscriptionStore.requireKey")(function* (input, code) {
3747
- const key = yield* validate(SubscriptionKey, input, code);
3748
- yield* requirePartition(key.partition, code);
3749
- return key;
3750
- });
3751
- const readRegistration = Effect.fn("SqliteSubscriptionStore.readRegistration")(function* (key, code) {
3752
- const rows = yield* query(sql`
3753
- SELECT owner_id, subscription_id, ordinal, source_name, source_version, matching_key, state,
3754
- expires_at_millis, recovery_at_millis, record_json FROM effect_agent_subscriptions
3755
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3756
- AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId}
3757
- `, code);
3758
- const decodedRows = yield* decodeRows(RegistrationRow, rows, code);
3759
- if (decodedRows.length > 1) return yield* corrupt(code);
3760
- const row = decodedRows[0];
3761
- if (row === void 0) return null;
3762
- const record = yield* decode(SubscriptionRecord, row.record_json, code);
3763
- if (!sameSourcePartition(record.key.partition, partition) || record.key.ownerId !== row.owner_id || record.key.subscriptionId !== row.subscription_id || record.ordinal !== row.ordinal || record.configuration.source.name !== row.source_name || record.configuration.source.version !== row.source_version || record.configuration.matchingKey !== row.matching_key || record.state !== row.state || record.configuration.expiresAtMillis !== row.expires_at_millis || (record.recovery?.nextAttemptAtMillis ?? null) !== row.recovery_at_millis) return yield* corrupt(`${code}-projection`);
3764
- return record;
3765
- });
3766
- const readEvent = Effect.fn("SqliteSubscriptionStore.readEvent")(function* (eventId, code) {
3767
- const rows = yield* query(sql`
3768
- SELECT event_id, source_name, source_version, matching_key, payload_digest, cutoff, cursor,
3769
- routing_complete, next_attempt_at_millis, record_json FROM effect_agent_subscription_events
3770
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${eventId}
3771
- `, code);
3772
- const decodedRows = yield* decodeRows(EventRow, rows, code);
3773
- if (decodedRows.length > 1) return yield* corrupt(code);
3774
- const row = decodedRows[0];
3775
- if (row === void 0) return null;
3776
- const event = yield* decode(AcceptedEvent, row.record_json, code);
3777
- if (!sameSourcePartition(event.partition, partition) || event.eventId !== row.event_id || event.source.name !== row.source_name || event.source.version !== row.source_version || event.matchingKey !== row.matching_key || event.payloadDigest !== row.payload_digest || event.cutoff !== row.cutoff || event.cursor !== row.cursor || (event.routingComplete ? 1 : 0) !== row.routing_complete || event.nextAttemptAtMillis !== row.next_attempt_at_millis) return yield* corrupt(`${code}-projection`);
3778
- return event;
3779
- });
3780
- const readDelivery = Effect.fn("SqliteSubscriptionStore.readDelivery")(function* (key, code) {
3781
- const rows = yield* query(sql`
3782
- SELECT owner_id, subscription_id, event_id, delivery_key, state, next_attempt_at_millis, record_json
3783
- FROM effect_agent_subscription_deliveries
3784
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3785
- AND owner_id=${key.subscription.ownerId} AND subscription_id=${key.subscription.subscriptionId}
3786
- AND event_id=${key.eventId}
3787
- `, code);
3788
- const decodedRows = yield* decodeRows(DeliveryRow, rows, code);
3789
- if (decodedRows.length > 1) return yield* corrupt(code);
3790
- const row = decodedRows[0];
3791
- if (row === void 0) return null;
3792
- const delivery = yield* decode(SubscriptionDelivery, row.record_json, code);
3793
- if (!sameSourcePartition(delivery.key.subscription.partition, partition) || delivery.key.subscription.ownerId !== row.owner_id || delivery.key.subscription.subscriptionId !== row.subscription_id || delivery.key.eventId !== row.event_id || subscriptionDeliveryKeyString(delivery.key) !== row.delivery_key || delivery.state !== row.state || delivery.retry.nextAttemptAtMillis !== row.next_attempt_at_millis) return yield* corrupt(`${code}-projection`);
3794
- return delivery;
3795
- });
3796
- const count = Effect.fn("SqliteSubscriptionStore.count")(function* (statement, code) {
3797
- const rows = yield* query(statement, code);
3798
- const decoded = yield* decodeRows(CountRow, rows, code);
3799
- if (decoded.length !== 1) return yield* corrupt(code);
3800
- return decoded[0].count;
3801
- });
3802
- const nextSequence = Effect.fn("SqliteSubscriptionStore.nextSequence")(function* () {
3803
- const rows = yield* query(sql`
3804
- UPDATE effect_agent_subscription_sequences SET sequence=sequence+1
3805
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3806
- RETURNING sequence
3807
- `, "advance subscription sequence");
3808
- const decoded = yield* decodeRows(SequenceRow, rows, "advance subscription sequence");
3809
- if (decoded.length !== 1) return yield* corrupt("subscription sequence");
3810
- return decoded[0].sequence;
3811
- });
3812
- const writeRegistration = Effect.fn("SqliteSubscriptionStore.writeRegistration")(function* (record) {
3813
- const json = yield* encode(SubscriptionRecord, record, "encode subscription");
3814
- yield* query(sql`
3815
- UPDATE effect_agent_subscriptions SET state=${record.state}, expires_at_millis=${record.configuration.expiresAtMillis},
3816
- recovery_at_millis=${record.recovery?.nextAttemptAtMillis ?? null}, record_json=${json}
3817
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3818
- AND owner_id=${record.key.ownerId} AND subscription_id=${record.key.subscriptionId}
3819
- `, "write subscription");
3820
- });
3821
- const writeEvent = Effect.fn("SqliteSubscriptionStore.writeEvent")(function* (event) {
3822
- const json = yield* encode(AcceptedEvent, event, "encode event");
3823
- yield* query(sql`
3824
- UPDATE effect_agent_subscription_events SET cursor=${event.cursor}, routing_complete=${event.routingComplete ? 1 : 0},
3825
- next_attempt_at_millis=${event.nextAttemptAtMillis}, record_json=${json}
3826
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${event.eventId}
3827
- `, "write event");
3828
- });
3829
- const writeDelivery = Effect.fn("SqliteSubscriptionStore.writeDelivery")(function* (delivery) {
3830
- const json = yield* encode(SubscriptionDelivery, delivery, "encode delivery");
3831
- yield* query(sql`
3832
- UPDATE effect_agent_subscription_deliveries SET state=${delivery.state},
3833
- next_attempt_at_millis=${delivery.retry.nextAttemptAtMillis}, record_json=${json}
3834
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3835
- AND owner_id=${delivery.key.subscription.ownerId} AND subscription_id=${delivery.key.subscription.subscriptionId}
3836
- AND event_id=${delivery.key.eventId}
3837
- `, "write delivery");
3838
- });
3839
- const register = Effect.fn("SqliteSubscriptionStore.register")(function* (input, inputLimits) {
3840
- const record = yield* validate(SubscriptionRecord, input, "register-record");
3841
- const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
3842
- yield* requirePartition(record.key.partition, "register-partition");
3843
- const result = yield* transact(Effect.gen(function* () {
3844
- const existing = yield* readRegistration(record.key, "register-existing");
3845
- if (existing !== null) {
3846
- if (existing.creationFingerprint !== record.creationFingerprint) return yield* error("conflict", "registration-identity");
3847
- return {
3848
- value: existing,
3849
- changed: false
3850
- };
3851
- }
3852
- if (bytes(record.configuration.context) > limits.maxContextBytes) return yield* error("capacity", "context-bytes");
3853
- if (bytes(record.configuration.parameters) > limits.maxPayloadBytes) return yield* error("capacity", "parameters-bytes");
3854
- if (record.configuration.expiresAtMillis - record.createdAtMillis > limits.maxLifetimeMillis) return yield* error("capacity", "lifetime");
3855
- if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count registrations")) >= limits.maxRegistrations) return yield* error("capacity", "registrations");
3856
- if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${record.key.ownerId}`, "count owner registrations")) >= limits.maxRegistrationsPerOwner) return yield* error("capacity", "owner-registrations");
3857
- const assigned = {
3858
- ...record,
3859
- ordinal: yield* nextSequence()
3860
- };
3861
- const json = yield* encode(SubscriptionRecord, assigned, "encode registration");
3862
- yield* failpoint.hit("subscription:register:before");
3863
- yield* query(sql`
3864
- INSERT INTO effect_agent_subscriptions (tenant_id, source_address, owner_id, subscription_id, ordinal,
3865
- source_name, source_version, matching_key, state, expires_at_millis, recovery_at_millis, record_json)
3866
- VALUES (${partition.tenantId}, ${partition.address}, ${assigned.key.ownerId}, ${assigned.key.subscriptionId}, ${assigned.ordinal},
3867
- ${assigned.configuration.source.name}, ${assigned.configuration.source.version}, ${assigned.configuration.matchingKey}, ${assigned.state},
3868
- ${assigned.configuration.expiresAtMillis}, ${assigned.recovery?.nextAttemptAtMillis ?? null}, ${json})
3869
- `, "insert registration");
3870
- return {
3871
- value: assigned,
3872
- changed: true
3873
- };
3874
- }));
3875
- if (result.changed) yield* failpoint.hit("subscription:register:after");
3876
- return result.value;
3877
- });
3878
- const get = Effect.fn("SqliteSubscriptionStore.get")(function* (input) {
3879
- return yield* readRegistration(yield* requireKey(input, "get-key"), "get subscription");
3880
- });
3881
- const list = Effect.fn("SqliteSubscriptionStore.list")(function* (ownerId, after, limit) {
3882
- const rows = yield* query(sql`
3883
- SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3884
- AND owner_id=${ownerId} AND ordinal>${after} ORDER BY ordinal LIMIT ${limit}
3885
- `, "list subscriptions");
3886
- const decoded = yield* decodeRows(Schema.Struct({
3887
- owner_id: Schema.String,
3888
- subscription_id: Schema.String,
3889
- ordinal: Schema.Natural
3890
- }), rows, "list subscriptions");
3891
- return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.listRecord")(function* (row) {
3892
- const record = yield* readRegistration({
3893
- partition,
3894
- ownerId: row.owner_id,
3895
- subscriptionId: row.subscription_id
3896
- }, "list subscription");
3897
- if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("list subscription projection");
3898
- return record;
3899
- }));
3900
- });
3901
- const cancel = Effect.fn("SqliteSubscriptionStore.cancel")(function* (input) {
3902
- const key = yield* requireKey(input, "cancel-key");
3903
- const result = yield* transact(Effect.gen(function* () {
3904
- const current = yield* readRegistration(key, "cancel subscription");
3905
- if (current === null) return yield* error("not-found", "subscription");
3906
- if (current.state === "cancelled") return {
3907
- value: current,
3908
- changed: false
3909
- };
3910
- const updated = {
3911
- ...current,
3912
- state: "cancelled",
3913
- recovery: null
3914
- };
3915
- yield* failpoint.hit("subscription:cancel:before");
3916
- yield* writeRegistration(updated);
3917
- return {
3918
- value: updated,
3919
- changed: true
3920
- };
3921
- }));
3922
- if (result.changed) yield* failpoint.hit("subscription:cancel:after");
3923
- return result.value;
3924
- });
3925
- const accept = Effect.fn("SqliteSubscriptionStore.accept")(function* (input, inputLimits) {
3926
- const event = yield* validate(AcceptedEvent, input, "accept-event");
3927
- const limits = yield* validate(SubscriptionLimits, inputLimits, "accept-limits");
3928
- yield* requirePartition(event.partition, "accept-partition");
3929
- const result = yield* transact(Effect.gen(function* () {
3930
- const existing = yield* readEvent(event.eventId, "accept event");
3931
- if (existing !== null) {
3932
- if (!sameAcceptedEventIdentity(existing, event)) return yield* error("conflict", "event-identity");
3933
- return {
3934
- value: existing,
3935
- changed: false
3936
- };
3937
- }
3938
- if (bytes(event.payload) > limits.maxPayloadBytes) return yield* error("capacity", "payload-bytes");
3939
- if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_events WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count events")) >= limits.maxEvents) return yield* error("capacity", "events");
3940
- const accepted = {
3941
- ...event,
3942
- cutoff: yield* nextSequence(),
3943
- cursor: 0,
3944
- routingComplete: false,
3945
- routingFailure: null
3946
- };
3947
- const json = yield* encode(AcceptedEvent, accepted, "encode accepted event");
3948
- yield* failpoint.hit("subscription:accept:before");
3949
- yield* query(sql`
3950
- INSERT INTO effect_agent_subscription_events (tenant_id, source_address, event_id, source_name, source_version,
3951
- matching_key, payload_digest, cutoff, cursor, routing_complete, next_attempt_at_millis, record_json)
3952
- VALUES (${partition.tenantId}, ${partition.address}, ${accepted.eventId}, ${accepted.source.name}, ${accepted.source.version},
3953
- ${accepted.matchingKey}, ${accepted.payloadDigest}, ${accepted.cutoff}, ${accepted.cursor}, 0, ${accepted.nextAttemptAtMillis}, ${json})
3954
- `, "insert event");
3955
- return {
3956
- value: accepted,
3957
- changed: true
3958
- };
3959
- }));
3960
- if (result.changed) yield* failpoint.hit("subscription:accept:after");
3961
- return result.value;
3962
- });
3963
- const event = Effect.fn("SqliteSubscriptionStore.event")((eventId) => readEvent(eventId, "get event"));
3964
- const pendingEvents = Effect.fn("SqliteSubscriptionStore.pendingEvents")(function* (nowMillis, after, limit) {
3965
- const rows = yield* query(sql`
3966
- SELECT event_id FROM effect_agent_subscription_events WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3967
- AND routing_complete=0 AND next_attempt_at_millis<=${nowMillis} AND event_id>${after} ORDER BY event_id LIMIT ${limit}
3968
- `, "pending events");
3969
- return yield* decodeRows(Schema.Struct({ event_id: Schema.String }), rows, "pending event keys").pipe(Effect.map((items) => items.map((item) => item.event_id)));
3970
- });
3971
- const candidates = Effect.fn("SqliteSubscriptionStore.candidates")(function* (input, limit) {
3972
- const supplied = yield* validate(AcceptedEvent, input, "candidates-event");
3973
- yield* requirePartition(supplied.partition, "candidates-partition");
3974
- const stored = yield* readEvent(supplied.eventId, "candidates event");
3975
- if (stored === null) return yield* error("not-found", "event");
3976
- if (!sameAcceptedEventIdentity(stored, supplied)) return yield* error("conflict", "event");
3977
- const rows = yield* query(sql`
3978
- SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3979
- AND source_name=${stored.source.name} AND source_version=${stored.source.version} AND matching_key=${stored.matchingKey}
3980
- AND ordinal>${stored.cursor} AND ordinal<=${stored.cutoff} ORDER BY ordinal LIMIT ${limit}
3981
- `, "subscription candidates");
3982
- const decoded = yield* decodeRows(Schema.Struct({
3983
- owner_id: Schema.String,
3984
- subscription_id: Schema.String,
3985
- ordinal: Schema.Natural
3986
- }), rows, "subscription candidates");
3987
- return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.candidateRecord")(function* (row) {
3988
- const record = yield* readRegistration({
3989
- partition,
3990
- ownerId: row.owner_id,
3991
- subscriptionId: row.subscription_id
3992
- }, "subscription candidate");
3993
- if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("subscription candidate projection");
3994
- return record;
3995
- }));
3996
- });
3997
- const insertDelivery = Effect.fn("SqliteSubscriptionStore.insertDelivery")(function* (delivery) {
3998
- const json = yield* encode(SubscriptionDelivery, delivery, "encode selected delivery");
3999
- yield* query(sql`
4000
- INSERT INTO effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, event_id,
4001
- delivery_key, state, next_attempt_at_millis, record_json)
4002
- VALUES (${partition.tenantId}, ${partition.address}, ${delivery.key.subscription.ownerId}, ${delivery.key.subscription.subscriptionId},
4003
- ${delivery.key.eventId}, ${subscriptionDeliveryKeyString(delivery.key)}, ${delivery.state}, ${delivery.retry.nextAttemptAtMillis}, ${json})
4004
- `, "insert delivery");
4005
- });
4006
- const select = Effect.fn("SqliteSubscriptionStore.select")(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
4007
- const supplied = yield* validate(AcceptedEvent, inputEvent, "select-event");
4008
- const deliveries = yield* validate(Schema.Array(SubscriptionDelivery), inputDeliveries, "select-deliveries");
4009
- const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
4010
- yield* requirePartition(supplied.partition, "select-partition");
4011
- for (const candidate of deliveries) yield* requirePartition(candidate.key.subscription.partition, "select-delivery-partition");
4012
- if (yield* transact(Effect.gen(function* () {
4013
- const accepted = yield* readEvent(supplied.eventId, "select event");
4014
- if (accepted === null) return yield* error("not-found", "event");
4015
- if (!sameAcceptedEventIdentity(accepted, supplied) || accepted.cursor !== supplied.cursor) return yield* error("conflict", "event-cursor");
4016
- if (accepted.routingComplete) return false;
4017
- if (!Number.isSafeInteger(cursor) || cursor < accepted.cursor || cursor > accepted.cutoff) return yield* error("validation", "cursor");
4018
- yield* failpoint.hit("subscription:select:before");
4019
- const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
4020
- const additions = [];
4021
- for (const delivery of deliveries) {
4022
- const record = yield* readRegistration(delivery.key.subscription, "select registration");
4023
- if (record === null) return yield* error("not-found", "subscription");
4024
- if (!subscriptionDeliveryCanSelect(delivery, record, accepted) || delivery.key.eventId !== accepted.eventId || delivery.source.name !== accepted.source.name || delivery.source.version !== accepted.source.version || record.ordinal <= accepted.cursor || record.ordinal > cursor) return yield* error("conflict", "selection");
4025
- const existing = yield* readDelivery(delivery.key, "select existing delivery");
4026
- if (existing !== null) {
4027
- if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
4028
- continue;
4029
- }
4030
- if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, false)) continue;
4031
- additions.push({
4032
- delivery,
4033
- record
4034
- });
4035
- }
4036
- if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count deliveries")) + additions.length > limits.maxDeliveries) return yield* error("capacity", "deliveries");
4037
- for (const ownerId of new Set(additions.map(({ record }) => record.key.ownerId))) if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${ownerId}`, "count owner deliveries")) + additions.filter(({ record }) => record.key.ownerId === ownerId).length > limits.maxDeliveriesPerOwner) return yield* error("capacity", "owner-deliveries");
4038
- for (const addition of additions) {
4039
- yield* insertDelivery(addition.delivery);
4040
- if (addition.record.configuration.mode === "once") yield* writeRegistration({
4041
- ...addition.record,
4042
- state: "consumed",
4043
- recovery: null
4044
- });
4045
- }
4046
- yield* writeEvent({
4047
- ...accepted,
4048
- cursor,
4049
- routingComplete: complete,
4050
- routingFailure: null
4051
- });
4052
- return true;
4053
- }))) yield* failpoint.hit("subscription:select:after");
4054
- });
4055
- const catchUp = Effect.fn("SqliteSubscriptionStore.catchUp")(function* (inputEvent, inputDelivery, nowMillis, inputLimits) {
4056
- const supplied = yield* validate(AcceptedEvent, inputEvent, "catch-up-event");
4057
- const delivery = yield* validate(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
4058
- const limits = yield* validate(SubscriptionLimits, inputLimits, "catch-up-limits");
4059
- yield* requirePartition(supplied.partition, "catch-up-partition");
4060
- yield* requirePartition(delivery.key.subscription.partition, "catch-up-delivery-partition");
4061
- if (yield* transact(Effect.gen(function* () {
4062
- const accepted = yield* readEvent(supplied.eventId, "catch-up event");
4063
- const record = yield* readRegistration(delivery.key.subscription, "catch-up subscription");
4064
- if (accepted === null || record === null) return yield* error("not-found", accepted === null ? "event" : "subscription");
4065
- if (!sameAcceptedEventIdentity(accepted, supplied) || !subscriptionDeliveryCanSelect(delivery, record, accepted) || delivery.key.eventId !== accepted.eventId || delivery.source.name !== accepted.source.name || delivery.source.version !== accepted.source.version || record.configuration.mode !== "once") return yield* error("conflict", "catch-up-identity");
4066
- const existing = yield* readDelivery(delivery.key, "catch-up existing delivery");
4067
- if (existing !== null) {
4068
- if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
4069
- return false;
4070
- }
4071
- yield* failpoint.hit("subscription:catch-up:before");
4072
- const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
4073
- if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, true)) return yield* error("conflict", "catch-up-eligibility");
4074
- if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count deliveries")) >= limits.maxDeliveries) return yield* error("capacity", "deliveries");
4075
- if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${record.key.ownerId}`, "count owner deliveries")) >= limits.maxDeliveriesPerOwner) return yield* error("capacity", "owner-deliveries");
4076
- yield* insertDelivery(delivery);
4077
- yield* writeRegistration({
4078
- ...record,
4079
- state: "consumed",
4080
- recovery: null
4081
- });
4082
- return true;
4083
- }))) yield* failpoint.hit("subscription:catch-up:after");
4084
- });
4085
- const deferEvent = Effect.fn("SqliteSubscriptionStore.deferEvent")(function* (eventId, nextAttemptAtMillis, code) {
4086
- const routingFailure = code === void 0 ? "routing-failed" : yield* validate(SubscriptionName, code, "routing-failure");
4087
- yield* transact(Effect.gen(function* () {
4088
- const accepted = yield* readEvent(eventId, "defer event");
4089
- if (accepted === null) return yield* error("not-found", "event");
4090
- yield* failpoint.hit("subscription:defer-event:before");
4091
- yield* writeEvent({
4092
- ...accepted,
4093
- nextAttemptAtMillis,
4094
- routingFailure
4095
- });
4096
- }));
4097
- yield* failpoint.hit("subscription:defer-event:after");
4098
- });
4099
- const delivery = Effect.fn("SqliteSubscriptionStore.delivery")(function* (input) {
4100
- const key = yield* validate(SubscriptionDeliveryKey, input, "delivery-key");
4101
- yield* requirePartition(key.subscription.partition, "delivery-partition");
4102
- return yield* readDelivery(key, "get delivery");
4103
- });
4104
- const pendingDeliveries = Effect.fn("SqliteSubscriptionStore.pendingDeliveries")(function* (nowMillis, after, limit) {
4105
- const rows = yield* query(sql`
4106
- SELECT owner_id, subscription_id, event_id FROM effect_agent_subscription_deliveries
4107
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
4108
- AND next_attempt_at_millis<=${nowMillis} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
4109
- `, "pending deliveries");
4110
- const rowSchema = Schema.Struct({
4111
- owner_id: Schema.String,
4112
- subscription_id: Schema.String,
4113
- event_id: Schema.String
4114
- });
4115
- return yield* decodeRows(rowSchema, rows, "pending delivery keys").pipe(Effect.map((items) => items.map((item) => ({
4116
- subscription: {
4117
- partition,
4118
- ownerId: item.owner_id,
4119
- subscriptionId: item.subscription_id
4120
- },
4121
- eventId: item.event_id
4122
- }))));
4123
- });
4124
- const listDeliveries = Effect.fn("SqliteSubscriptionStore.listDeliveries")(function* (input, after, limit) {
4125
- const key = yield* requireKey(input, "list-deliveries-key");
4126
- const rows = yield* query(sql`
4127
- SELECT record_json FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
4128
- AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
4129
- `, "list deliveries");
4130
- const decoded = yield* decodeRows(JsonRow, rows, "list deliveries");
4131
- return yield* Effect.forEach(decoded, (row) => decode(SubscriptionDelivery, row.record_json, "list delivery"));
4132
- });
4133
- const changeDelivery = Effect.fn("SqliteSubscriptionStore.changeDelivery")(function* (inputKey, inputDeliveryId, inputChange) {
4134
- const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
4135
- const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
4136
- const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
4137
- yield* requirePartition(key.subscription.partition, "change-delivery-partition");
4138
- const result = yield* transact(Effect.gen(function* () {
4139
- const existing = yield* readDelivery(key, "change delivery");
4140
- const record = yield* readRegistration(key.subscription, "change delivery subscription");
4141
- if (existing === null || record === null) return yield* error("not-found", existing === null ? "delivery" : "subscription");
4142
- yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
4143
- const effectiveChange = change._tag === "Prepare" ? {
4144
- ...change,
4145
- nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis)
4146
- } : change;
4147
- const transition = applySubscriptionDeliveryChange(existing, record, deliveryId, effectiveChange);
4148
- if (Result.isFailure(transition)) return yield* transition.failure;
4149
- if (transition.success === existing) return {
4150
- value: existing,
4151
- changed: false
4152
- };
4153
- yield* writeDelivery(transition.success);
4154
- return {
4155
- value: transition.success,
4156
- changed: true
4157
- };
4158
- }));
4159
- if (result.changed) yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
4160
- return result.value;
4161
- });
4162
- const recovering = Effect.fn("SqliteSubscriptionStore.recovering")(function* (nowMillis, after, limit) {
4163
- const rows = yield* query(sql`
4164
- SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions
4165
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active'
4166
- AND recovery_at_millis IS NOT NULL AND recovery_at_millis<=${nowMillis} AND ordinal>${after}
4167
- ORDER BY ordinal LIMIT ${limit}
4168
- `, "recovering subscriptions");
4169
- const rowSchema = Schema.Struct({
4170
- owner_id: Schema.String,
4171
- subscription_id: Schema.String,
4172
- ordinal: Schema.Natural
4173
- });
4174
- return yield* decodeRows(rowSchema, rows, "recovering subscription keys").pipe(Effect.map((items) => items.map((item) => ({
4175
- key: {
4176
- partition,
4177
- ownerId: item.owner_id,
4178
- subscriptionId: item.subscription_id
4179
- },
4180
- ordinal: item.ordinal
4181
- }))));
4182
- });
4183
- const deferRecovery = Effect.fn("SqliteSubscriptionStore.deferRecovery")(function* (input, recovery) {
4184
- const key = yield* requireKey(input, "defer-recovery-key");
4185
- yield* transact(Effect.gen(function* () {
4186
- const record = yield* readRegistration(key, "defer recovery");
4187
- if (record === null) return yield* error("not-found", "subscription");
4188
- yield* failpoint.hit("subscription:defer-recovery:before");
4189
- yield* writeRegistration({
4190
- ...record,
4191
- recovery: record.state === "active" ? recovery : null
4192
- });
4193
- }));
4194
- yield* failpoint.hit("subscription:defer-recovery:after");
4195
- });
4196
- const readScanCursors = Effect.gen(function* () {
4197
- const rows = yield* query(sql`
4198
- SELECT event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
4199
- FROM effect_agent_subscription_sequences
4200
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
4201
- `, "read subscription scan cursors");
4202
- const decoded = yield* decodeRows(ScanRow, rows, "read subscription scan cursors");
4203
- if (decoded.length !== 1) return yield* corrupt("subscription scan cursors");
4204
- return {
4205
- events: decoded[0].event_scan_cursor,
4206
- deliveries: decoded[0].delivery_scan_cursor,
4207
- recovery: decoded[0].recovery_scan_cursor
4208
- };
4209
- });
4210
- const advanceScanCursors = Effect.fn("SqliteSubscriptionStore.advanceScanCursors")(function* (input) {
4211
- const cursors = yield* validate(SubscriptionScanCursors, input, "scan-cursors");
4212
- yield* transact(Effect.gen(function* () {
4213
- yield* failpoint.hit("subscription:advance-scan-cursors:before");
4214
- yield* query(sql`
4215
- UPDATE effect_agent_subscription_sequences
4216
- SET event_scan_cursor=${cursors.events}, delivery_scan_cursor=${cursors.deliveries}, recovery_scan_cursor=${cursors.recovery}
4217
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
4218
- `, "advance subscription scan cursors");
4219
- }));
4220
- yield* failpoint.hit("subscription:advance-scan-cursors:after");
4221
- });
4222
- const indexedDeadline = query(sql`
4223
- SELECT MIN(deadline) AS deadline FROM (
4224
- SELECT next_attempt_at_millis AS deadline FROM effect_agent_subscription_events
4225
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND routing_complete=0
4226
- UNION ALL SELECT next_attempt_at_millis FROM effect_agent_subscription_deliveries
4227
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
4228
- UNION ALL SELECT recovery_at_millis FROM effect_agent_subscriptions
4229
- WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active' AND recovery_at_millis IS NOT NULL
4230
- )
4231
- `, "next subscription deadline").pipe(Effect.flatMap((rows) => decodeRows(Schema.Struct({ deadline: Schema.NullOr(Schema.Number) }), rows, "next subscription deadline")), Effect.flatMap((rows) => rows.length === 1 ? Effect.succeed(rows[0].deadline) : Effect.fail(corrupt("next subscription deadline"))));
4232
- const nextDeadline = Effect.gen(function* () {
4233
- const cursors = yield* readScanCursors;
4234
- if (cursors.events !== "" || cursors.deliveries !== "" || cursors.recovery !== 0) return 0;
4235
- return yield* indexedDeadline;
4236
- });
4237
- return SubscriptionStore.of({
4238
- partition,
4239
- register,
4240
- get,
4241
- list,
4242
- cancel,
4243
- accept,
4244
- event,
4245
- pendingEvents,
4246
- candidates,
4247
- select,
4248
- catchUp,
4249
- deferEvent,
4250
- delivery,
4251
- pendingDeliveries,
4252
- listDeliveries,
4253
- changeDelivery,
4254
- recovering,
4255
- deferRecovery,
4256
- readScanCursors,
4257
- advanceScanCursors,
4258
- nextDeadline
4259
- });
4260
- });
4261
- const subscriptionStoreLayer = (partition) => Layer.effect(SubscriptionStore, makeSubscriptionStore(partition));
4262
- //#endregion
4263
- export { CurrentSqliteStorageVersion, SqliteAppendConflict, SqliteCheckpointConflict, SqliteFenceRejected, SqliteLedgerError, SqliteStorageCompatibilityError, SqliteStorageConfig, SqliteStorageConfigValue, SqliteStorageCorruptionError, SqliteStorageError, SqliteStorageFailpoint, SqliteStorageFailpointError, SqliteStorageFailpointLocation, SqliteWriteContention, activityProcessorStoreLayer, activityProcessorStoreLayerWithFailpoints, layer, ledgerLayer, memoryReaderLayer, memoryStoreLayer, memoryStoreLayerWithFailpoints, observationOffsetAt, scheduleStoreLayer, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer, subscriptionStoreLayer, threadStoreLayer };
4264
-
4265
- //# sourceMappingURL=index.mjs.map
1
+ import { t as SqliteActivityStore_exports } from "./SqliteActivityStore.mjs";
2
+ import { t as SqliteStorageConfig_exports } from "./SqliteStorageConfig.mjs";
3
+ import { t as SqliteStorageError_exports } from "./SqliteStorageError.mjs";
4
+ import { t as SqliteStorageFailpoint_exports } from "./SqliteStorageFailpoint.mjs";
5
+ import { t as SqliteScheduleStore_exports } from "./SqliteScheduleStore.mjs";
6
+ import { t as SqliteStorageVersion_exports } from "./SqliteStorageVersion.mjs";
7
+ import { t as SqliteThreadStore_exports } from "./SqliteThreadStore.mjs";
8
+ import { t as SqliteSubmissionLedger_exports } from "./SqliteSubmissionLedger.mjs";
9
+ import { t as SqliteSubscriptionStore_exports } from "./SqliteSubscriptionStore.mjs";
10
+ export { SqliteActivityStore_exports as SqliteActivityStore, 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 };