@effect-agent/storage-cloudflare 0.1.0-beta.8 → 0.1.0-beta.80

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 (73) hide show
  1. package/dist/DoMemoryStore.d.mts +29 -0
  2. package/dist/DoMemoryStore.mjs +55 -0
  3. package/dist/DoMemoryStore.mjs.map +1 -0
  4. package/dist/DoMessageDeliveryStore.d.mts +17 -0
  5. package/dist/DoMessageDeliveryStore.mjs +32 -0
  6. package/dist/DoMessageDeliveryStore.mjs.map +1 -0
  7. package/dist/DoScheduleStore.d.mts +36 -0
  8. package/dist/DoScheduleStore.mjs +388 -0
  9. package/dist/DoScheduleStore.mjs.map +1 -0
  10. package/dist/DoStorageConfig.d.mts +45 -0
  11. package/dist/DoStorageConfig.mjs +52 -0
  12. package/dist/DoStorageConfig.mjs.map +1 -0
  13. package/dist/DoStorageError-Cmhvl4TQ.d.mts +98 -0
  14. package/dist/DoStorageError.d.mts +2 -0
  15. package/dist/DoStorageError.mjs +164 -0
  16. package/dist/DoStorageError.mjs.map +1 -0
  17. package/dist/DoStorageFailpoint.d.mts +17 -0
  18. package/dist/DoStorageFailpoint.mjs +14 -0
  19. package/dist/DoStorageFailpoint.mjs.map +1 -0
  20. package/dist/DoStorageFailpointTesting.d.mts +34 -0
  21. package/dist/DoStorageFailpointTesting.mjs +35 -0
  22. package/dist/DoStorageFailpointTesting.mjs.map +1 -0
  23. package/dist/DoStorageVersion-x5OOurvZ.d.mts +12 -0
  24. package/dist/DoStorageVersion.d.mts +2 -0
  25. package/dist/DoStorageVersion.mjs +8 -0
  26. package/dist/DoStorageVersion.mjs.map +1 -0
  27. package/dist/DoSubmissionLedger.d.mts +23 -0
  28. package/dist/DoSubmissionLedger.mjs +1849 -0
  29. package/dist/DoSubmissionLedger.mjs.map +1 -0
  30. package/dist/DoSubscriptionStore.d.mts +25 -0
  31. package/dist/DoSubscriptionStore.mjs +183 -0
  32. package/dist/DoSubscriptionStore.mjs.map +1 -0
  33. package/dist/DoThreadStore.d.mts +59 -0
  34. package/dist/DoThreadStore.mjs +465 -0
  35. package/dist/DoThreadStore.mjs.map +1 -0
  36. package/dist/MemoryProtocol.d.mts +38160 -0
  37. package/dist/MemoryProtocol.mjs +219 -0
  38. package/dist/MemoryProtocol.mjs.map +1 -0
  39. package/dist/PortProtocol.d.mts +3207 -0
  40. package/dist/PortProtocol.mjs +176 -0
  41. package/dist/PortProtocol.mjs.map +1 -0
  42. package/dist/PortRouting.d.mts +83 -0
  43. package/dist/PortRouting.mjs +410 -0
  44. package/dist/PortRouting.mjs.map +1 -0
  45. package/dist/do-journal-DHqCQeBU.mjs +991 -0
  46. package/dist/do-journal-DHqCQeBU.mjs.map +1 -0
  47. package/dist/index.d.mts +14 -1521
  48. package/dist/index.mjs +14 -3695
  49. package/dist/migrations-Ba_kOrSx.mjs +287 -0
  50. package/dist/migrations-Ba_kOrSx.mjs.map +1 -0
  51. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  52. package/package.json +1 -45
  53. package/src/DoMemoryStore.ts +59 -0
  54. package/src/DoMessageDeliveryStore.ts +53 -0
  55. package/src/DoScheduleStore.ts +622 -0
  56. package/src/{errors.ts → DoStorageError.ts} +10 -3
  57. package/src/DoStorageFailpoint.ts +20 -0
  58. package/src/{do-storage-failpoint.ts → DoStorageFailpointTesting.ts} +5 -18
  59. package/src/DoStorageVersion.ts +2 -0
  60. package/src/{do-ledger.ts → DoSubmissionLedger.ts} +862 -163
  61. package/src/DoSubscriptionStore.ts +329 -0
  62. package/src/DoThreadStore.ts +1014 -0
  63. package/src/MemoryProtocol.ts +355 -0
  64. package/src/{port-protocol.ts → PortProtocol.ts} +44 -37
  65. package/src/{routing.ts → PortRouting.ts} +292 -264
  66. package/src/index.ts +13 -32
  67. package/src/{do-journal.ts → internal/do-journal.ts} +716 -232
  68. package/src/internal/message-delivery-schema.ts +24 -0
  69. package/src/{migrations.ts → internal/migrations.ts} +51 -35
  70. package/src/internal/recovery-checkpoint-schema.ts +17 -0
  71. package/dist/index.mjs.map +0 -1
  72. package/src/do-conversation-store.ts +0 -850
  73. /package/src/{do-storage-config.ts → DoStorageConfig.ts} +0 -0
package/dist/index.mjs CHANGED
@@ -1,3695 +1,14 @@
1
- import { AbortCommand, AbortIntent, AdmissionAdmitted, AdmissionConflict, AdmissionIndeterminate, AdmissionNotAdmitted, AdmissionRequest, AdmissionResolution, AdmissionResult, AppendConflict, AppendResult, ApprovalConflict, ApprovalDecision, ApprovalDecisionCommand, ApprovalDecisionIntent, AttachChildToReservationRequest, BeginChildBudgetReleaseRequest, CanonicalBatch, CanonicalRecord, CanonicalRecordEnvelope, CanonicalSequence, CheckpointRejected, ChildAttachmentSnapshot, ChildBudgetReservationRequest, ChildBudgetReservationSnapshot, ChildReservationConflict, ChildReservationStatus, ChildSettledNotification, ChildSettledOutcome, Claim, ClaimJoiningRequest, ClaimRequest, ConversationCheckpoint, ConversationExport, ConversationExportRequest, ConversationMaterialization, ConversationNotMaterialized, ConversationObservation, ConversationRead, ConversationStore, ConversationStoreError, ConversationTail, ConversationTailRequest, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, Digest, EMPTY_TAIL_DIGEST, FenceRejected, FencedAppendRequest, InputAppliedMarker, JoinSnapshot, JoinedToHost, JoiningClaim, LedgerCapabilities, LedgerError, LoadCheckpointRequest, MarkInputAppliedRequest, MarkJoinedRequest, MarkReadyRequest, MarkUnknownRequest, ObservationOffset, OwnershipLost, OwnershipRenewal, OwnershipSnapshot, ParentLinkage, PersistedJson, ProducerEpoch, QueueSequence, RecordEnvelope, RecoverySnapshot, RecoverySnapshotRequest, ReleaseChildBudgetRequest, ReleaseOwnershipRequest, RenewOwnershipRequest, ReservedChildBudget, ReservedSettlement, RevertJoiningRequest, SaveCheckpointRequest, Settlement, SettlementConflict, SettlementFinalization, SettlementOutcome, SettlementReservation, SettlementReservationSnapshot, SubmissionLedger, SubmissionLookup, SubmissionLookupById, SubmissionLookupByKey, SubmissionSnapshot, SubmissionState, SuspendRequest, SuspensionReason, SuspensionSnapshot, UnknownResolution, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, digestCanonicalBatch, submissionAbortRecordId } from "@effect-agent/session";
2
- import { Clock, Context, Crypto, DateTime, Duration, Effect, Layer, Option, Ref, Schema, Stream } from "effect";
3
- import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-do";
4
- import * as SqlClient from "effect/unstable/sql/SqlClient";
5
- import { BrowserCrypto } from "@effect/platform-browser";
6
- import { SqlError } from "effect/unstable/sql/SqlError";
7
- //#region src/errors.ts
8
- /** The Durable Object's SQLite storage uses a private-development format this adapter cannot read. */
9
- var DoStorageCompatibilityError = class extends Schema.TaggedError()("DoStorageCompatibilityError", {
10
- actualVersion: Schema.Int,
11
- message: Schema.String,
12
- supportedVersion: Schema.Int
13
- }) {};
14
- /** Stored bytes failed the current Schema and cannot be used as recovery truth. */
15
- var DoStorageCorruptionError = class extends Schema.TaggedError()("DoStorageCorruptionError", {
16
- message: Schema.String,
17
- rowKey: Schema.String,
18
- table: Schema.String
19
- }) {};
20
- /** Durable Object SQLite infrastructure failed while opening or operating the store. */
21
- var DoStorageError = class extends Schema.TaggedError()("DoStorageError", {
22
- cause: Schema.optionalKey(Schema.Defect()),
23
- message: Schema.String,
24
- operation: Schema.String
25
- }) {};
26
- /**
27
- * Durable Object SQLite infrastructure failed while operating the Submission Ledger. Surfaces
28
- * at the SubmissionLedger port as the typed `LedgerError` with this error preserved as its
29
- * cause, so the adapter-level tag is never erased.
30
- */
31
- var DoLedgerError = class extends Schema.TaggedError()("DoLedgerError", {
32
- cause: Schema.optionalKey(Schema.Defect()),
33
- message: Schema.String,
34
- operation: Schema.String
35
- }) {};
36
- /**
37
- * A value to be stored exceeds the configured Durable Object per-value bound
38
- * (`DoStorageConfigValue.maxStoredValueBytes`, kept under the platform's 2 MB SQLite value
39
- * limit). The refusal happens typed BEFORE any durable mutation; no partial state is written.
40
- * Payloads of this size are the designed overflow case for a future R2-backed AttachmentStore
41
- * (deployment spec §3.1, deferred until a real attachment requirement exists).
42
- */
43
- var DoValueBoundExceeded = class extends Schema.TaggedError()("DoValueBoundExceeded", {
44
- actualBytes: Schema.Int,
45
- maxBytes: Schema.Int,
46
- operation: Schema.String
47
- }) {
48
- get message() {
49
- return `A stored value of ${this.actualBytes} bytes exceeds the Durable Object per-value bound of ${this.maxBytes} bytes during ${this.operation}. Nothing was written. Values of this size are the designed R2 AttachmentStore overflow path (deferred, deployment spec §3.1).`;
50
- }
51
- };
52
- /**
53
- * A canonical batch retry conflicts with existing append state. Tail conflicts carry the
54
- * actual committed tail as a diagnostic resume hint.
55
- */
56
- var DoAppendConflict = class extends Schema.TaggedError()("DoAppendConflict", {
57
- message: Schema.String,
58
- reason: Schema.Literals([
59
- "batch-digest",
60
- "record-identity",
61
- "tail"
62
- ]),
63
- actualTailSequence: Schema.optionalKey(CanonicalSequence),
64
- actualTailDigest: Schema.optionalKey(Schema.String)
65
- }) {};
66
- /**
67
- * A producer epoch does not match the Conversation's current writer registration. Appends
68
- * require the exact registered epoch, so both older and newer unregistered epochs are fenced;
69
- * a newer epoch takes over by materializing first.
70
- */
71
- var DoFenceRejected = class extends Schema.TaggedError()("DoFenceRejected", {
72
- actualEpoch: ProducerEpoch,
73
- message: Schema.String,
74
- producerEpoch: ProducerEpoch
75
- }) {};
76
- /** A checkpoint conflicts with a previously stored checkpoint at the same offset. */
77
- var DoCheckpointConflict = class extends Schema.TaggedError()("DoCheckpointConflict", { message: Schema.String }) {};
78
- /**
79
- * Deterministic fault-injection locations at Durable Object storage operation boundaries.
80
- *
81
- * The string list is copied VERBATIM from `SqliteStorageFailpointLocation`
82
- * (`packages/storage-sqlite/src/errors.ts`) so every crash-matrix row keeps the same name on
83
- * both platforms — the DN process-kill evidence and the DC eviction evidence address identical
84
- * locations. There is intentionally no Cloudflare-only location.
85
- */
86
- const DoStorageFailpointLocation = Schema.Literals([
87
- "materialize:before",
88
- "materialize:after",
89
- "append:before",
90
- "append:after-batch-insert",
91
- "append:after-record-insert",
92
- "append:after-tail-update",
93
- "append:after",
94
- "export:after-conversation-read",
95
- "save-checkpoint:before",
96
- "save-checkpoint:after",
97
- "ledger:admit:before",
98
- "ledger:admit:after",
99
- "ledger:mark-ready:before",
100
- "ledger:mark-ready:after",
101
- "ledger:claim:before",
102
- "ledger:claim:after",
103
- "ledger:mark-input-applied:before",
104
- "ledger:mark-input-applied:after",
105
- "ledger:renew:before",
106
- "ledger:renew:after",
107
- "ledger:reserve-settlement:before",
108
- "ledger:reserve-settlement:after",
109
- "ledger:finalize-settlement:before",
110
- "ledger:finalize-settlement:after",
111
- "ledger:request-abort:before",
112
- "ledger:request-abort:after",
113
- "ledger:release:before",
114
- "ledger:release:after",
115
- "ledger:claim-joining:before",
116
- "ledger:claim-joining:after",
117
- "ledger:mark-joined:before",
118
- "ledger:mark-joined:after",
119
- "ledger:revert-joining:before",
120
- "ledger:revert-joining:after",
121
- "ledger:suspend:before",
122
- "ledger:suspend:after",
123
- "ledger:approval-decision:before",
124
- "ledger:approval-decision:after",
125
- "ledger:mark-unknown:before",
126
- "ledger:mark-unknown:after",
127
- "ledger:unknown-resolution:before",
128
- "ledger:unknown-resolution:after",
129
- "ledger:child-reservation:before",
130
- "ledger:child-reservation:after",
131
- "ledger:child-attach:before",
132
- "ledger:child-attach:after",
133
- "ledger:child-release-pending:before",
134
- "ledger:child-release-pending:after",
135
- "ledger:child-release:before",
136
- "ledger:child-release:after",
137
- "ledger:child-settled:before",
138
- "ledger:child-settled:after"
139
- ]);
140
- /** Deterministic test-only fault or pause injected at a Durable Object storage boundary. */
141
- var DoStorageFailpointError = class extends Schema.TaggedError()("DoStorageFailpointError", { location: DoStorageFailpointLocation }) {
142
- get message() {
143
- return `Injected Durable Object storage failure at ${this.location}.`;
144
- }
145
- };
146
- //#endregion
147
- //#region src/migrations.ts
148
- /**
149
- * The exact-or-fresh storage version recorded in `effect_agent_meta`. Cloudflare is a fresh
150
- * platform, so there is exactly ONE migration carrying the complete current schema — no
151
- * v1→v4 history to replay (deployment spec §9: no rolling data-version promise during
152
- * private development).
153
- */
154
- const CurrentDoStorageVersion = 1;
155
- /**
156
- * The Conversation Durable Object schema. Table names and columns mirror the Node/SQLite v4
157
- * schema byte-for-byte (`packages/storage-sqlite/src/migrations.ts`, migrations 1–4 collapsed
158
- * into their final shape) so the shared conformance suites and crash-matrix rows address
159
- * identical durable state. Two DC-specific additions:
160
- *
161
- * 1. `effect_agent_meta` replaces `PRAGMA user_version` as the exact-or-fresh version gate —
162
- * a meta table is portable regardless of which PRAGMAs Durable Object SQL storage allows.
163
- * 2. `effect_agent_child_settlements` is the durable cross-store notification marker the
164
- * SubmissionLedger port contract mandates for cross-store adapters (`suspend`'s covering
165
- * check and `recordChildSettled`'s wake both consult it): parent and child Conversations
166
- * live in different Durable Objects, so a child settlement reported before the parent's
167
- * suspend commits must be observable from the PARENT's own storage.
168
- */
169
- const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversation_object": Effect.gen(function* () {
170
- const sql = yield* SqlClient.SqlClient;
171
- yield* sql`
172
- CREATE TABLE effect_agent_conversations (
173
- conversation_id TEXT PRIMARY KEY NOT NULL,
174
- created_at TEXT NOT NULL,
175
- tail_sequence INTEGER NOT NULL,
176
- tail_digest TEXT NOT NULL,
177
- producer_epoch INTEGER NOT NULL
178
- )
179
- `.withoutTransform;
180
- yield* sql`
181
- CREATE TABLE effect_agent_canonical_batches (
182
- conversation_id TEXT NOT NULL,
183
- batch_id TEXT NOT NULL,
184
- first_sequence INTEGER NOT NULL,
185
- last_sequence INTEGER NOT NULL,
186
- batch_digest TEXT NOT NULL,
187
- tail_digest TEXT NOT NULL,
188
- batch_json TEXT NOT NULL,
189
- PRIMARY KEY (conversation_id, batch_id),
190
- FOREIGN KEY (conversation_id)
191
- REFERENCES effect_agent_conversations(conversation_id)
192
- ON DELETE RESTRICT
193
- )
194
- `.withoutTransform;
195
- yield* sql`
196
- CREATE TABLE effect_agent_canonical_records (
197
- conversation_id TEXT NOT NULL,
198
- sequence INTEGER NOT NULL,
199
- record_id TEXT NOT NULL,
200
- batch_id TEXT NOT NULL,
201
- record_json TEXT NOT NULL,
202
- PRIMARY KEY (conversation_id, sequence),
203
- UNIQUE (conversation_id, record_id),
204
- FOREIGN KEY (conversation_id, batch_id)
205
- REFERENCES effect_agent_canonical_batches(conversation_id, batch_id)
206
- ON DELETE RESTRICT
207
- )
208
- `.withoutTransform;
209
- yield* sql`
210
- CREATE INDEX effect_agent_canonical_records_batch
211
- ON effect_agent_canonical_records (conversation_id, batch_id, sequence)
212
- `.withoutTransform;
213
- yield* sql`
214
- CREATE TABLE effect_agent_checkpoints (
215
- conversation_id TEXT NOT NULL,
216
- through_sequence INTEGER NOT NULL,
217
- tail_digest TEXT NOT NULL,
218
- checkpoint_json TEXT NOT NULL,
219
- PRIMARY KEY (conversation_id, through_sequence),
220
- FOREIGN KEY (conversation_id)
221
- REFERENCES effect_agent_conversations(conversation_id)
222
- ON DELETE RESTRICT
223
- )
224
- `.withoutTransform;
225
- yield* sql`
226
- CREATE TABLE effect_agent_submissions (
227
- submission_id TEXT PRIMARY KEY NOT NULL,
228
- conversation_id TEXT NOT NULL,
229
- queue_sequence INTEGER NOT NULL,
230
- principal TEXT NOT NULL,
231
- idempotency_key TEXT NOT NULL,
232
- agent_id TEXT NOT NULL,
233
- agent_digests_json TEXT NOT NULL,
234
- deployment_id TEXT NOT NULL,
235
- input_json TEXT NOT NULL,
236
- input_digest TEXT NOT NULL,
237
- receipt_id TEXT NOT NULL,
238
- state TEXT NOT NULL,
239
- settled_outcome TEXT,
240
- created_at TEXT NOT NULL,
241
- ready_at TEXT,
242
- input_applied_record_id TEXT,
243
- input_applied_sequence INTEGER,
244
- joined_host_submission_id TEXT,
245
- suspended_reason_json TEXT,
246
- suspended_at TEXT,
247
- unknown_reason TEXT,
248
- unknown_tool_call_ids_json TEXT,
249
- parent_submission_id TEXT,
250
- parent_tool_call_id TEXT,
251
- UNIQUE (conversation_id, principal, idempotency_key),
252
- UNIQUE (conversation_id, queue_sequence)
253
- )
254
- `.withoutTransform;
255
- yield* sql`
256
- CREATE INDEX effect_agent_submissions_joined_host
257
- ON effect_agent_submissions (joined_host_submission_id)
258
- `.withoutTransform;
259
- yield* sql`
260
- CREATE INDEX effect_agent_submissions_parent
261
- ON effect_agent_submissions (parent_submission_id)
262
- `.withoutTransform;
263
- yield* sql`
264
- CREATE TABLE effect_agent_submission_ownership (
265
- submission_id TEXT PRIMARY KEY NOT NULL,
266
- attempt_id TEXT NOT NULL,
267
- ownership_token TEXT NOT NULL,
268
- producer_epoch INTEGER NOT NULL,
269
- owner_producer_id TEXT NOT NULL,
270
- lease_expires_at TEXT NOT NULL,
271
- FOREIGN KEY (submission_id)
272
- REFERENCES effect_agent_submissions(submission_id)
273
- ON DELETE RESTRICT
274
- )
275
- `.withoutTransform;
276
- yield* sql`
277
- CREATE TABLE effect_agent_attempts (
278
- attempt_id TEXT PRIMARY KEY NOT NULL,
279
- submission_id TEXT NOT NULL,
280
- conversation_id TEXT NOT NULL,
281
- owner_producer_id TEXT NOT NULL,
282
- producer_epoch INTEGER NOT NULL,
283
- claimed_at TEXT NOT NULL,
284
- FOREIGN KEY (submission_id)
285
- REFERENCES effect_agent_submissions(submission_id)
286
- ON DELETE RESTRICT
287
- )
288
- `.withoutTransform;
289
- yield* sql`
290
- CREATE TABLE effect_agent_settlement_reservations (
291
- submission_id TEXT PRIMARY KEY NOT NULL,
292
- settlement_id TEXT NOT NULL,
293
- outcome TEXT NOT NULL,
294
- record_id TEXT NOT NULL,
295
- record_json TEXT NOT NULL,
296
- record_digest TEXT NOT NULL,
297
- reserved_at TEXT NOT NULL,
298
- finalized_at TEXT,
299
- FOREIGN KEY (submission_id)
300
- REFERENCES effect_agent_submissions(submission_id)
301
- ON DELETE RESTRICT
302
- )
303
- `.withoutTransform;
304
- yield* sql`
305
- CREATE TABLE effect_agent_abort_intents (
306
- submission_id TEXT PRIMARY KEY NOT NULL,
307
- author TEXT NOT NULL,
308
- reason TEXT NOT NULL,
309
- requested_at TEXT NOT NULL,
310
- canonical_record_id TEXT,
311
- FOREIGN KEY (submission_id)
312
- REFERENCES effect_agent_submissions(submission_id)
313
- ON DELETE RESTRICT
314
- )
315
- `.withoutTransform;
316
- yield* sql`
317
- CREATE TABLE effect_agent_approval_decisions (
318
- submission_id TEXT NOT NULL,
319
- tool_call_id TEXT NOT NULL,
320
- decision TEXT NOT NULL,
321
- resolver TEXT NOT NULL,
322
- reason TEXT NOT NULL,
323
- decided_at TEXT NOT NULL,
324
- PRIMARY KEY (submission_id, tool_call_id),
325
- FOREIGN KEY (submission_id)
326
- REFERENCES effect_agent_submissions(submission_id)
327
- ON DELETE RESTRICT
328
- )
329
- `.withoutTransform;
330
- yield* sql`
331
- CREATE TABLE effect_agent_unknown_resolutions (
332
- submission_id TEXT NOT NULL,
333
- tool_call_id TEXT NOT NULL,
334
- author TEXT NOT NULL,
335
- reason TEXT NOT NULL,
336
- resolution_json TEXT NOT NULL,
337
- resolved_at TEXT NOT NULL,
338
- PRIMARY KEY (submission_id, tool_call_id),
339
- FOREIGN KEY (submission_id)
340
- REFERENCES effect_agent_submissions(submission_id)
341
- ON DELETE RESTRICT
342
- )
343
- `.withoutTransform;
344
- yield* sql`
345
- CREATE TABLE effect_agent_child_reservations (
346
- reservation_id TEXT PRIMARY KEY NOT NULL,
347
- parent_submission_id TEXT NOT NULL,
348
- parent_tool_call_id TEXT NOT NULL,
349
- child_submission_id TEXT,
350
- status TEXT NOT NULL,
351
- allocation_json TEXT NOT NULL,
352
- allocation_digest TEXT NOT NULL,
353
- accounting_json TEXT,
354
- reserved_at TEXT NOT NULL,
355
- release_began_at TEXT,
356
- released_at TEXT,
357
- UNIQUE (parent_submission_id, parent_tool_call_id),
358
- FOREIGN KEY (parent_submission_id)
359
- REFERENCES effect_agent_submissions(submission_id)
360
- ON DELETE RESTRICT
361
- )
362
- `.withoutTransform;
363
- yield* sql`
364
- CREATE TABLE effect_agent_child_settlements (
365
- parent_submission_id TEXT NOT NULL,
366
- child_submission_id TEXT NOT NULL,
367
- child_outcome TEXT,
368
- recorded_at TEXT NOT NULL,
369
- PRIMARY KEY (parent_submission_id, child_submission_id)
370
- )
371
- `.withoutTransform;
372
- yield* sql`
373
- CREATE TABLE effect_agent_meta (
374
- key TEXT PRIMARY KEY NOT NULL,
375
- value TEXT NOT NULL
376
- )
377
- `.withoutTransform;
378
- yield* sql`
379
- INSERT INTO effect_agent_meta (key, value)
380
- VALUES ('storage_version', ${String(1)})
381
- `.withoutTransform;
382
- }) });
383
- //#endregion
384
- //#region src/do-journal.ts
385
- /**
386
- * Static schema ceiling for stored text columns. Writes are bounded in BYTES by the
387
- * configured `maxStoredValueBytes` (always ≤ 2,000,000); UTF-8 byte length is never smaller
388
- * than UTF-16 string length, so any value that passed the byte bound also passes this
389
- * decode-side character ceiling.
390
- */
391
- const BoundedStoredText$1 = Schema.String.check(Schema.isMaxLength(2e6));
392
- const BoundedIdentifier$1 = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
393
- const MAX_RECORDS_PER_CONVERSATION = 65536;
394
- const MAX_IDENTIFIER_LENGTH$1 = 1024;
395
- /** Durable Object SQL storage allows at most 100 bound parameters per statement. */
396
- const MAX_BOUND_PARAMETERS = 100;
397
- const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
398
- const chunked = (values, size) => {
399
- const chunks = [];
400
- for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
401
- return chunks;
402
- };
403
- var DoMetaRow = class extends Schema.Class("DoMetaRow")({ value: Schema.NonEmptyString.check(Schema.isMaxLength(128)) }) {};
404
- var DoNameRow = class extends Schema.Class("DoNameRow")({ name: BoundedIdentifier$1 }) {};
405
- var ConversationRow = class extends Schema.Class("ConversationRow")({
406
- conversation_id: BoundedIdentifier$1,
407
- created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
408
- producer_epoch: ProducerEpoch,
409
- tail_digest: BoundedStoredText$1,
410
- tail_sequence: CanonicalSequence
411
- }) {};
412
- var BatchRow = class extends Schema.Class("BatchRow")({
413
- batch_digest: BoundedStoredText$1,
414
- batch_id: BoundedIdentifier$1,
415
- batch_json: BoundedStoredText$1,
416
- conversation_id: BoundedIdentifier$1,
417
- first_sequence: CanonicalSequence,
418
- last_sequence: CanonicalSequence,
419
- tail_digest: BoundedStoredText$1
420
- }) {};
421
- var RecordRow = class extends Schema.Class("RecordRow")({
422
- batch_id: BoundedIdentifier$1,
423
- conversation_id: BoundedIdentifier$1,
424
- record_id: BoundedIdentifier$1,
425
- record_json: BoundedStoredText$1,
426
- sequence: CanonicalSequence
427
- }) {};
428
- var CheckpointRow = class extends Schema.Class("CheckpointRow")({
429
- checkpoint_json: BoundedStoredText$1,
430
- conversation_id: BoundedIdentifier$1,
431
- tail_digest: BoundedStoredText$1,
432
- through_sequence: CanonicalSequence
433
- }) {};
434
- var RawRecord = class extends Schema.Class("@effect-agent/storage-cloudflare/RawRecord")({
435
- recordId: BoundedIdentifier$1,
436
- recordJson: BoundedStoredText$1
437
- }) {};
438
- var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawAppendRequest")({
439
- batchDigest: BoundedStoredText$1,
440
- batchId: BoundedIdentifier$1,
441
- batchJson: BoundedStoredText$1,
442
- conversationId: BoundedIdentifier$1,
443
- expectedTailDigest: BoundedStoredText$1,
444
- expectedTailSequence: CanonicalSequence,
445
- producerEpoch: ProducerEpoch,
446
- records: Schema.NonEmptyArray(RawRecord).check(Schema.isMaxLength(256)),
447
- tailDigest: BoundedStoredText$1
448
- }) {};
449
- var RawAppendResult = class extends Schema.Class("@effect-agent/storage-cloudflare/RawAppendResult")({
450
- firstSequence: CanonicalSequence,
451
- lastSequence: CanonicalSequence,
452
- replayed: Schema.Boolean,
453
- tailDigest: BoundedStoredText$1
454
- }) {};
455
- var RawReadRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawReadRequest")({
456
- conversationId: BoundedIdentifier$1,
457
- fromSequenceExclusive: CanonicalSequence,
458
- limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
459
- }) {};
460
- var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-cloudflare/RawCheckpoint")({
461
- checkpointJson: BoundedStoredText$1,
462
- conversationId: BoundedIdentifier$1,
463
- tailDigest: BoundedStoredText$1,
464
- throughSequence: CanonicalSequence
465
- }) {};
466
- var RawConversationExport = class extends Schema.Class("@effect-agent/storage-cloudflare/RawConversationExport")({
467
- batches: Schema.Array(BatchRow),
468
- checkpoints: Schema.Array(CheckpointRow),
469
- conversation: ConversationRow,
470
- records: Schema.Array(RecordRow)
471
- }) {};
472
- const noFailpoint$1 = () => Effect.void;
473
- const storageError = (operation) => (error) => DoStorageError.make({
474
- cause: error,
475
- operation,
476
- message: error.message
477
- });
478
- /** Decode raw Durable Object SQLite rows against a Schema, reporting failures as typed corruption. */
479
- const decodeRows = Effect.fn("DoJournal.decodeRows")((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
480
- table,
481
- rowKey,
482
- message: String(error)
483
- }))));
484
- /** Decode exactly one raw row against a Schema, reporting failures as typed corruption. */
485
- const decodeSingleRow = Effect.fn("DoJournal.decodeSingleRow")((schema, table, rowKey, rows) => decodeRows(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(DoStorageCorruptionError.make({
486
- table,
487
- rowKey,
488
- message: `Expected exactly one row but found ${decoded.length}.`
489
- })))));
490
- const REQUIRED_TABLES = [
491
- "effect_agent_abort_intents",
492
- "effect_agent_approval_decisions",
493
- "effect_agent_attempts",
494
- "effect_agent_canonical_batches",
495
- "effect_agent_canonical_records",
496
- "effect_agent_checkpoints",
497
- "effect_agent_child_reservations",
498
- "effect_agent_child_settlements",
499
- "effect_agent_conversations",
500
- "effect_agent_meta",
501
- "effect_agent_settlement_reservations",
502
- "effect_agent_submission_ownership",
503
- "effect_agent_submissions",
504
- "effect_agent_unknown_resolutions"
505
- ];
506
- /**
507
- * Exact-or-fresh storage gate (DEPLOY-008) over `effect_agent_meta` instead of
508
- * `PRAGMA user_version` (unverified on Durable Object SQL storage; a meta table is portable
509
- * regardless). No WAL check (Durable Object storage owns durability and confirms writes
510
- * through output gates) and no busy timeout (a Durable Object has exactly one writer): the
511
- * Node machinery those served has no DC analogue and is deliberately absent.
512
- */
513
- const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint$1, maxStoredValueBytes) {
514
- const metaTableRows = yield* sql`
515
- SELECT name
516
- FROM sqlite_master
517
- WHERE type = 'table'
518
- AND name = 'effect_agent_meta'
519
- `.pipe(Effect.mapError(storageError("read storage version table")));
520
- if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_meta", metaTableRows)).length === 0) {
521
- const existingRows = yield* sql`
522
- SELECT name
523
- FROM sqlite_master
524
- WHERE type = 'table'
525
- AND name LIKE 'effect_agent_%'
526
- ORDER BY name
527
- `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
528
- if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* DoStorageCompatibilityError.make({
529
- actualVersion: 0,
530
- supportedVersion: 1,
531
- message: "The Durable Object contains unversioned Effect Agent tables. Reset the development namespace explicitly; refusing to mutate ambiguous stored data."
532
- });
533
- yield* SqliteMigrator.run({ loader: doMigrations }).pipe(Effect.provideService(SqlClient.SqlClient, sql), Effect.mapError((error) => DoStorageError.make({
534
- cause: error,
535
- operation: "initialize current storage",
536
- message: error.message
537
- })));
538
- } else {
539
- const versionRows = yield* sql`
540
- SELECT value
541
- FROM effect_agent_meta
542
- WHERE key = 'storage_version'
543
- `.pipe(Effect.mapError(storageError("read storage version")));
544
- const version = yield* decodeSingleRow(Schema.Array(DoMetaRow), "effect_agent_meta", "storage_version", versionRows);
545
- if (version.value !== String(1)) {
546
- const actualVersion = Number.parseInt(version.value, 10);
547
- return yield* DoStorageCompatibilityError.make({
548
- actualVersion: Number.isSafeInteger(actualVersion) ? actualVersion : -1,
549
- supportedVersion: 1,
550
- message: `The Durable Object uses private-development storage version ${version.value}; this build supports exactly version 1. Replace the development namespace explicitly; automatic stored-data migrations are not provided during private development.`
551
- });
552
- }
553
- }
554
- const requiredRows = yield* sql`
555
- SELECT name
556
- FROM sqlite_master
557
- WHERE type = 'table'
558
- AND name IN ${sql.in([...REQUIRED_TABLES])}
559
- ORDER BY name
560
- `.pipe(Effect.mapError(storageError("verify storage tables")));
561
- if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "required_tables", requiredRows)).length !== REQUIRED_TABLES.length) return yield* DoStorageCompatibilityError.make({
562
- actualVersion: 1,
563
- supportedVersion: 1,
564
- message: "The Durable Object claims the current format but is missing required tables. Reset the corrupt private-development data."
565
- });
566
- return makeJournal(sql, failpoint, maxStoredValueBytes);
567
- });
568
- const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
569
- /** Typed pre-write refusal for any single value over the configured byte bound. */
570
- const checkValueBound = (operation, value) => {
571
- const actualBytes = storedTextBytes(value);
572
- return actualBytes > maxStoredValueBytes ? Effect.fail(DoValueBoundExceeded.make({
573
- actualBytes,
574
- maxBytes: maxStoredValueBytes,
575
- operation
576
- })) : Effect.void;
577
- };
578
- /**
579
- * Runs one journal write transaction on the Durable Object storage-backed
580
- * `withTransaction` (`ctx.storage.transaction()` under the hood). Within one Durable
581
- * Object there is exactly ONE writer, so the Node `BEGIN IMMEDIATE` + busy-retry +
582
- * `SqliteWriteContention` machinery has no analogue here and is deliberately absent.
583
- * Ownership-token and epoch checks still run INSIDE the transaction, so fencing atomicity
584
- * (DUR-006) is preserved identically.
585
- *
586
- * Journal write transactions are always top level: the Durable Object client rejects
587
- * nested transactions, so new journal operations must not wrap this helper inside another
588
- * transaction.
589
- */
590
- const withWriteTransaction = (operation) => (effect) => sql.withTransaction(effect).pipe(Effect.mapError((error) => error instanceof SqlError ? storageError(operation)(error) : error), Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }));
591
- const materialize = Effect.fn("DoJournal.materialize")(function* (conversationId, createdAt, emptyTailDigest, producerEpoch) {
592
- if (conversationId.length > MAX_IDENTIFIER_LENGTH$1) return yield* DoStorageError.make({
593
- operation: "materialize conversation",
594
- message: "Conversation identity exceeds the Durable Object storage bounds."
595
- });
596
- yield* checkValueBound("materialize conversation", emptyTailDigest);
597
- yield* withWriteTransaction("materialize transaction")(Effect.gen(function* () {
598
- const existingRows = yield* sql`
599
- SELECT
600
- conversation_id,
601
- created_at,
602
- tail_sequence,
603
- tail_digest,
604
- producer_epoch
605
- FROM effect_agent_conversations
606
- WHERE conversation_id = ${conversationId}
607
- `.pipe(Effect.mapError(storageError("read materialized conversation")));
608
- const existing = yield* decodeRows(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, existingRows);
609
- if (existing.length > 1) return yield* DoStorageCorruptionError.make({
610
- table: "effect_agent_conversations",
611
- rowKey: conversationId,
612
- message: "A conversation primary key returned more than one row."
613
- });
614
- if (existing.length === 0) {
615
- yield* sql`
616
- INSERT INTO effect_agent_conversations (
617
- conversation_id,
618
- created_at,
619
- tail_sequence,
620
- tail_digest,
621
- producer_epoch
622
- ) VALUES (
623
- ${conversationId},
624
- ${createdAt},
625
- 0,
626
- ${emptyTailDigest},
627
- ${producerEpoch}
628
- )
629
- `.pipe(Effect.mapError(storageError("materialize conversation")));
630
- return;
631
- }
632
- if (producerEpoch < existing[0].producer_epoch) return yield* DoFenceRejected.make({
633
- producerEpoch,
634
- actualEpoch: existing[0].producer_epoch,
635
- message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
636
- });
637
- if (producerEpoch > existing[0].producer_epoch) yield* sql`
638
- UPDATE effect_agent_conversations
639
- SET producer_epoch = ${producerEpoch}
640
- WHERE conversation_id = ${conversationId}
641
- `.pipe(Effect.mapError(storageError("advance materialization epoch")));
642
- }));
643
- });
644
- const getConversation = Effect.fn("DoJournal.getConversation")(function* (conversationId) {
645
- const rows = yield* sql`
646
- SELECT
647
- conversation_id,
648
- created_at,
649
- tail_sequence,
650
- tail_digest,
651
- producer_epoch
652
- FROM effect_agent_conversations
653
- WHERE conversation_id = ${conversationId}
654
- `.pipe(Effect.mapError(storageError("read conversation")));
655
- return yield* decodeRows(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, rows);
656
- });
657
- const append = Effect.fn("DoJournal.append")(function* (request) {
658
- if (request.conversationId.length > MAX_IDENTIFIER_LENGTH$1 || request.batchId.length > MAX_IDENTIFIER_LENGTH$1 || request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH$1)) return yield* DoStorageError.make({
659
- operation: "append canonical batch",
660
- message: "Canonical identifiers exceed the Durable Object storage bounds."
661
- });
662
- yield* checkValueBound("append canonical batch", request.batchJson);
663
- yield* checkValueBound("append canonical batch", request.batchDigest);
664
- yield* checkValueBound("append canonical batch", request.tailDigest);
665
- yield* Effect.forEach(request.records, (record) => checkValueBound("append canonical record", record.recordJson), { discard: true });
666
- return yield* withWriteTransaction("append transaction")(Effect.gen(function* () {
667
- const recordIds = request.records.map((record) => record.recordId);
668
- if (new Set(recordIds).size !== recordIds.length) return yield* DoAppendConflict.make({
669
- message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
670
- reason: "record-identity"
671
- });
672
- const conversationRows = yield* sql`
673
- SELECT
674
- conversation_id,
675
- created_at,
676
- tail_sequence,
677
- tail_digest,
678
- producer_epoch
679
- FROM effect_agent_conversations
680
- WHERE conversation_id = ${request.conversationId}
681
- `.pipe(Effect.mapError(storageError("read append tail")));
682
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", request.conversationId, conversationRows);
683
- if (request.producerEpoch !== conversation.producer_epoch) return yield* DoFenceRejected.make({
684
- producerEpoch: request.producerEpoch,
685
- actualEpoch: conversation.producer_epoch,
686
- message: `Producer epoch ${request.producerEpoch} is not the current epoch ${conversation.producer_epoch}.`
687
- });
688
- const batchRows = yield* sql`
689
- SELECT
690
- conversation_id,
691
- batch_id,
692
- first_sequence,
693
- last_sequence,
694
- batch_digest,
695
- tail_digest,
696
- batch_json
697
- FROM effect_agent_canonical_batches
698
- WHERE conversation_id = ${request.conversationId}
699
- AND batch_id = ${request.batchId}
700
- `.pipe(Effect.mapError(storageError("read idempotent batch")));
701
- const batches = yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.conversationId}/${request.batchId}`, batchRows);
702
- if (batches.length > 1) return yield* DoStorageCorruptionError.make({
703
- table: "effect_agent_canonical_batches",
704
- rowKey: `${request.conversationId}/${request.batchId}`,
705
- message: "A canonical batch primary key returned more than one row."
706
- });
707
- if (batches.length === 1) {
708
- const existing = batches[0];
709
- if (existing.batch_digest !== request.batchDigest) return yield* DoAppendConflict.make({
710
- message: `Batch ${request.batchId} already exists with different canonical content.`,
711
- reason: "batch-digest"
712
- });
713
- return RawAppendResult.make({
714
- firstSequence: existing.first_sequence,
715
- lastSequence: existing.last_sequence,
716
- replayed: true,
717
- tailDigest: existing.tail_digest
718
- });
719
- }
720
- if (request.expectedTailSequence !== conversation.tail_sequence || request.expectedTailDigest !== conversation.tail_digest) return yield* DoAppendConflict.make({
721
- message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${conversation.tail_sequence}/${conversation.tail_digest}.`,
722
- reason: "tail",
723
- actualTailSequence: conversation.tail_sequence,
724
- actualTailDigest: conversation.tail_digest
725
- });
726
- if (conversation.tail_sequence + request.records.length > MAX_RECORDS_PER_CONVERSATION) return yield* DoStorageError.make({
727
- operation: "append canonical batch",
728
- message: `Conversation record limit ${MAX_RECORDS_PER_CONVERSATION} would be exceeded.`
729
- });
730
- const existingRecords = [];
731
- for (const chunk of chunked(recordIds, MAX_BOUND_PARAMETERS - 10)) {
732
- const existingRecordRows = yield* sql`
733
- SELECT
734
- conversation_id,
735
- sequence,
736
- record_id,
737
- batch_id,
738
- record_json
739
- FROM effect_agent_canonical_records
740
- WHERE conversation_id = ${request.conversationId}
741
- AND record_id IN ${sql.in([...chunk])}
742
- ORDER BY sequence
743
- `.pipe(Effect.mapError(storageError("check canonical record identities")));
744
- existingRecords.push(...yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}/record_ids`, existingRecordRows));
745
- }
746
- if (existingRecords.length > 0) return yield* DoAppendConflict.make({
747
- message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
748
- reason: "record-identity"
749
- });
750
- const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(conversation.tail_sequence + 1).pipe(Effect.mapError((error) => DoStorageError.make({
751
- cause: error,
752
- operation: "append canonical batch",
753
- message: error.message
754
- })));
755
- const lastSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(firstSequence + request.records.length - 1).pipe(Effect.mapError((error) => DoStorageError.make({
756
- cause: error,
757
- operation: "append canonical batch",
758
- message: error.message
759
- })));
760
- yield* sql`
761
- INSERT INTO effect_agent_canonical_batches (
762
- conversation_id,
763
- batch_id,
764
- first_sequence,
765
- last_sequence,
766
- batch_digest,
767
- tail_digest,
768
- batch_json
769
- ) VALUES (
770
- ${request.conversationId},
771
- ${request.batchId},
772
- ${firstSequence},
773
- ${lastSequence},
774
- ${request.batchDigest},
775
- ${request.tailDigest},
776
- ${request.batchJson}
777
- )
778
- `.pipe(Effect.mapError(storageError("insert canonical batch")));
779
- yield* failpoint("append:after-batch-insert");
780
- yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
781
- yield* sql`
782
- INSERT INTO effect_agent_canonical_records (
783
- conversation_id,
784
- sequence,
785
- record_id,
786
- batch_id,
787
- record_json
788
- ) VALUES (
789
- ${request.conversationId},
790
- ${firstSequence + index},
791
- ${record.recordId},
792
- ${request.batchId},
793
- ${record.recordJson}
794
- )
795
- `.pipe(Effect.mapError(storageError("insert canonical record")));
796
- yield* failpoint("append:after-record-insert");
797
- }), { discard: true });
798
- yield* sql`
799
- UPDATE effect_agent_conversations
800
- SET
801
- tail_sequence = ${lastSequence},
802
- tail_digest = ${request.tailDigest},
803
- producer_epoch = ${request.producerEpoch}
804
- WHERE conversation_id = ${request.conversationId}
805
- `.pipe(Effect.mapError(storageError("advance conversation tail")));
806
- yield* failpoint("append:after-tail-update");
807
- return RawAppendResult.make({
808
- firstSequence,
809
- lastSequence,
810
- replayed: false,
811
- tailDigest: request.tailDigest
812
- });
813
- }));
814
- });
815
- const read = Effect.fn("DoJournal.read")(function* (request) {
816
- const rows = yield* sql`
817
- SELECT
818
- conversation_id,
819
- sequence,
820
- record_id,
821
- batch_id,
822
- record_json
823
- FROM effect_agent_canonical_records
824
- WHERE conversation_id = ${request.conversationId}
825
- AND sequence > ${request.fromSequenceExclusive}
826
- ORDER BY sequence
827
- LIMIT ${request.limit}
828
- `.pipe(Effect.mapError(storageError("read canonical records")));
829
- return yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}>${request.fromSequenceExclusive}`, rows);
830
- });
831
- const exportConversation = Effect.fn("DoJournal.exportConversation")(function* (conversationId) {
832
- return yield* sql.withTransaction(Effect.gen(function* () {
833
- const conversationRows = yield* sql`
834
- SELECT
835
- conversation_id,
836
- created_at,
837
- tail_sequence,
838
- tail_digest,
839
- producer_epoch
840
- FROM effect_agent_conversations
841
- WHERE conversation_id = ${conversationId}
842
- `.pipe(Effect.mapError(storageError("export conversation")));
843
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, conversationRows);
844
- yield* failpoint("export:after-conversation-read");
845
- const batchRows = yield* sql`
846
- SELECT
847
- conversation_id,
848
- batch_id,
849
- first_sequence,
850
- last_sequence,
851
- batch_digest,
852
- tail_digest,
853
- batch_json
854
- FROM effect_agent_canonical_batches
855
- WHERE conversation_id = ${conversationId}
856
- ORDER BY first_sequence
857
- `.pipe(Effect.mapError(storageError("export canonical batches")));
858
- const recordRows = yield* sql`
859
- SELECT
860
- conversation_id,
861
- sequence,
862
- record_id,
863
- batch_id,
864
- record_json
865
- FROM effect_agent_canonical_records
866
- WHERE conversation_id = ${conversationId}
867
- ORDER BY sequence
868
- `.pipe(Effect.mapError(storageError("export canonical records")));
869
- const checkpointRows = yield* sql`
870
- SELECT
871
- conversation_id,
872
- through_sequence,
873
- tail_digest,
874
- checkpoint_json
875
- FROM effect_agent_checkpoints
876
- WHERE conversation_id = ${conversationId}
877
- ORDER BY through_sequence
878
- `.pipe(Effect.mapError(storageError("export checkpoints")));
879
- return RawConversationExport.make({
880
- conversation,
881
- batches: yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", conversationId, batchRows),
882
- records: yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", conversationId, recordRows),
883
- checkpoints: yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", conversationId, checkpointRows)
884
- });
885
- })).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(storageError("export transaction")(error))));
886
- });
887
- const saveCheckpoint = Effect.fn("DoJournal.saveCheckpoint")(function* (checkpoint) {
888
- if (checkpoint.conversationId.length > MAX_IDENTIFIER_LENGTH$1) return yield* DoStorageError.make({
889
- operation: "save checkpoint",
890
- message: "Checkpoint identity exceeds the Durable Object storage bounds."
891
- });
892
- yield* checkValueBound("save checkpoint", checkpoint.checkpointJson);
893
- yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
894
- const conversationRows = yield* sql`
895
- SELECT
896
- conversation_id,
897
- created_at,
898
- tail_sequence,
899
- tail_digest,
900
- producer_epoch
901
- FROM effect_agent_conversations
902
- WHERE conversation_id = ${checkpoint.conversationId}
903
- `.pipe(Effect.mapError(storageError("read checkpoint tail")));
904
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", checkpoint.conversationId, conversationRows);
905
- if (checkpoint.throughSequence > conversation.tail_sequence) return yield* DoCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${conversation.tail_sequence}.` });
906
- const checkpointRows = yield* sql`
907
- SELECT
908
- conversation_id,
909
- through_sequence,
910
- tail_digest,
911
- checkpoint_json
912
- FROM effect_agent_checkpoints
913
- WHERE conversation_id = ${checkpoint.conversationId}
914
- AND through_sequence = ${checkpoint.throughSequence}
915
- `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
916
- const existing = yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.conversationId}/${checkpoint.throughSequence}`, checkpointRows);
917
- if (existing.length > 1) return yield* DoStorageCorruptionError.make({
918
- table: "effect_agent_checkpoints",
919
- rowKey: `${checkpoint.conversationId}/${checkpoint.throughSequence}`,
920
- message: "A checkpoint primary key returned more than one row."
921
- });
922
- if (existing.length === 1) {
923
- if (existing[0].tail_digest !== checkpoint.tailDigest || existing[0].checkpoint_json !== checkpoint.checkpointJson) return yield* DoCheckpointConflict.make({ message: "A different checkpoint already exists at this canonical sequence." });
924
- return;
925
- }
926
- yield* sql`
927
- INSERT INTO effect_agent_checkpoints (
928
- conversation_id,
929
- through_sequence,
930
- tail_digest,
931
- checkpoint_json
932
- ) VALUES (
933
- ${checkpoint.conversationId},
934
- ${checkpoint.throughSequence},
935
- ${checkpoint.tailDigest},
936
- ${checkpoint.checkpointJson}
937
- )
938
- `.pipe(Effect.mapError(storageError("insert checkpoint")));
939
- }));
940
- });
941
- const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (conversationId, atOrBeforeSequence) {
942
- const rows = yield* sql`
943
- SELECT
944
- conversation_id,
945
- through_sequence,
946
- tail_digest,
947
- checkpoint_json
948
- FROM effect_agent_checkpoints
949
- WHERE conversation_id = ${conversationId}
950
- AND through_sequence <= ${atOrBeforeSequence}
951
- ORDER BY through_sequence DESC
952
- LIMIT 1
953
- `.pipe(Effect.mapError(storageError("load checkpoint")));
954
- return yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${conversationId}<=${atOrBeforeSequence}`, rows);
955
- });
956
- return {
957
- append,
958
- checkValueBound,
959
- exportConversation,
960
- getConversation,
961
- getTailDigestAt: Effect.fn("DoJournal.getTailDigestAt")(function* (conversationId, sequence) {
962
- if (sequence === 0) {
963
- const conversations = yield* getConversation(conversationId);
964
- return conversations.length === 0 ? [] : [conversations[0].tail_sequence === 0 ? conversations[0].tail_digest : void 0].filter((value) => value !== void 0);
965
- }
966
- const rows = yield* sql`
967
- SELECT
968
- conversation_id,
969
- batch_id,
970
- first_sequence,
971
- last_sequence,
972
- batch_digest,
973
- tail_digest,
974
- batch_json
975
- FROM effect_agent_canonical_batches
976
- WHERE conversation_id = ${conversationId}
977
- AND last_sequence = ${sequence}
978
- `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
979
- return (yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${conversationId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
980
- }),
981
- loadCheckpoint,
982
- materialize,
983
- read,
984
- saveCheckpoint,
985
- scanStoredPayloads: Effect.fn("DoJournal.scanStoredPayloads")(function* () {
986
- return yield* sql.withTransaction(Effect.gen(function* () {
987
- const conversations = yield* sql`
988
- SELECT
989
- conversation_id,
990
- created_at,
991
- tail_sequence,
992
- tail_digest,
993
- producer_epoch
994
- FROM effect_agent_conversations
995
- ORDER BY conversation_id
996
- `.pipe(Effect.mapError(storageError("scan conversations")));
997
- const batches = yield* sql`
998
- SELECT
999
- conversation_id,
1000
- batch_id,
1001
- first_sequence,
1002
- last_sequence,
1003
- batch_digest,
1004
- tail_digest,
1005
- batch_json
1006
- FROM effect_agent_canonical_batches
1007
- ORDER BY conversation_id, first_sequence
1008
- `.pipe(Effect.mapError(storageError("scan canonical batches")));
1009
- const records = yield* sql`
1010
- SELECT
1011
- conversation_id,
1012
- sequence,
1013
- record_id,
1014
- batch_id,
1015
- record_json
1016
- FROM effect_agent_canonical_records
1017
- ORDER BY conversation_id, sequence
1018
- `.pipe(Effect.mapError(storageError("scan canonical records")));
1019
- const checkpoints = yield* sql`
1020
- SELECT
1021
- conversation_id,
1022
- through_sequence,
1023
- tail_digest,
1024
- checkpoint_json
1025
- FROM effect_agent_checkpoints
1026
- ORDER BY conversation_id, through_sequence
1027
- `.pipe(Effect.mapError(storageError("scan checkpoints")));
1028
- return {
1029
- conversations: yield* decodeRows(Schema.Array(ConversationRow), "effect_agent_conversations", "startup_scan", conversations),
1030
- batches: yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
1031
- records: yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
1032
- checkpoints: yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
1033
- };
1034
- })).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(storageError("startup scan transaction")(error))));
1035
- }),
1036
- withWriteTransaction
1037
- };
1038
- };
1039
- const initializeDoJournal = ensureCurrentStorage;
1040
- //#endregion
1041
- //#region src/do-storage-config.ts
1042
- const ObservationPollInterval = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
1043
- const OwnershipLeaseMillis = Schema.Int.check(Schema.isGreaterThan(0));
1044
- /**
1045
- * Default per-value byte bound, kept under the Durable Object platform's 2 MB SQLite value
1046
- * limit with a safety margin. This is the DC analogue of Node's 16 MB `BoundedStoredText`
1047
- * bound: both fail typed before mutating, only the threshold differs (a documented DN/DC
1048
- * behavioral difference; Travel Planner payloads sit orders of magnitude below both).
1049
- */
1050
- const DEFAULT_MAX_STORED_VALUE_BYTES = 19e5;
1051
- /** The hard schema ceiling for the configurable bound: never at or above the platform limit. */
1052
- const MaxStoredValueBytes = Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(2e6));
1053
- /**
1054
- * Validated construction configuration consumed by the Durable Object storage Layers. The
1055
- * storage identity itself belongs to the SqlClient Layer (built from `ctx.storage`);
1056
- * duplicating it here could silently diverge from the handle actually in use.
1057
- */
1058
- var DoStorageConfigValue = class extends Schema.Class("@effect-agent/storage-cloudflare/DoStorageConfigValue")({
1059
- observationPollInterval: ObservationPollInterval,
1060
- /**
1061
- * Submission ownership lease duration in milliseconds (D5). Inside one Durable Object the
1062
- * object itself is the serialized owner, so the lease's primary DC role is fencing work
1063
- * across DO incarnations (an evicted incarnation's claim becomes reclaimable); correctness
1064
- * never depends on it because every canonical append is fenced by producer epoch.
1065
- */
1066
- ownershipLeaseDuration: OwnershipLeaseMillis,
1067
- /**
1068
- * Maximum bytes for any single stored text value (canonical batch/record JSON, admission
1069
- * input payload, checkpoint JSON). Enforced typed BEFORE any write; must stay under the
1070
- * platform's 2 MB per-value limit.
1071
- */
1072
- maxStoredValueBytes: MaxStoredValueBytes,
1073
- /**
1074
- * Re-verify every stored payload and digest chain while opening the store. Per-operation
1075
- * Schema decoding and the digest chain already fail clearly on corrupt rows, so the full
1076
- * scan is an explicit opt-in integrity audit rather than a startup requirement.
1077
- */
1078
- verifyOnOpen: Schema.Boolean
1079
- }) {};
1080
- /** Explicit Durable Object storage configuration authority. */
1081
- var DoStorageConfig = class extends Context.Service()("@effect-agent/storage-cloudflare/DoStorageConfig") {};
1082
- //#endregion
1083
- //#region src/do-storage-failpoint.ts
1084
- const noFailpoint = () => Effect.void;
1085
- /** Test-only control for replacing the active Durable Object failpoint handler. */
1086
- var DoStorageFailpointTestControl = class extends Context.Service()("@effect-agent/storage-cloudflare/DoStorageFailpointTestControl") {};
1087
- /** Explicit fault-injection authority used at Durable Object storage operation boundaries. */
1088
- var DoStorageFailpoint = class DoStorageFailpoint extends Context.Service()("@effect-agent/storage-cloudflare/DoStorageFailpoint") {
1089
- /** Production default: no fault injection. */
1090
- static layer = Layer.succeed(this)({ hit: noFailpoint });
1091
- /** Reusable test Layer with a control service backed by the same handler Ref. */
1092
- static layerTest = Layer.effectContext(Effect.gen(function* () {
1093
- const handler = yield* Ref.make(noFailpoint);
1094
- return Context.make(DoStorageFailpoint, DoStorageFailpoint.of({ hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))) })).pipe(Context.add(DoStorageFailpointTestControl, DoStorageFailpointTestControl.of({
1095
- clear: Ref.set(handler, noFailpoint),
1096
- setHandler: (next) => Ref.set(handler, next)
1097
- })));
1098
- }));
1099
- };
1100
- /**
1101
- * The DC-specific eviction failpoint mode: instead of failing typed, an armed hit evicts the
1102
- * Durable Object through an injected `evict` thunk — in production-shaped harnesses that thunk
1103
- * is `() => ctx.abort()`, the platform's real failure mode. `ctx.abort()` never returns (it
1104
- * throws while destroying the in-memory instance and every in-flight implicit or explicit
1105
- * storage transaction rolls back), so an armed hit ends the current Attempt exactly like an
1106
- * unannounced platform eviction; DO storage — the only correctness-critical state — survives
1107
- * for the next incarnation, which the persisted alarm wakes without any incoming request.
1108
- *
1109
- * The handles stay injected: this package never imports `cloudflare:workers`, so the harness
1110
- * that owns a `DurableObjectState` supplies the thunk.
1111
- */
1112
- const evictionFailpointHandler = (options) => (location) => options.isArmed(location).pipe(Effect.flatMap((armed) => armed ? Effect.sync(() => {
1113
- options.evict(location);
1114
- throw new Error(`Durable Object eviction did not interrupt execution at ${location}.`);
1115
- }) : Effect.void));
1116
- //#endregion
1117
- //#region src/do-conversation-store.ts
1118
- const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
1119
- const DO_OFFSET_PREFIX = "effect-agent-do@1:";
1120
- const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
1121
- const isDigest = Schema.is(Digest);
1122
- const storeError = (operation, error) => ConversationStoreError.make({
1123
- cause: error,
1124
- operation,
1125
- message: error.message
1126
- });
1127
- const schemaStoreError = (operation, error) => ConversationStoreError.make({
1128
- cause: error,
1129
- operation,
1130
- message: error.message
1131
- });
1132
- const makeOffset = Effect.fn("DoConversationStore.makeOffset")(function* (conversationId, sequence) {
1133
- return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.flatMap((validatedSequence) => Schema.decodeUnknownEffect(ObservationOffset)(`${DO_OFFSET_PREFIX}${encodeURIComponent(conversationId)}:${validatedSequence}`)), Effect.mapError((error) => schemaStoreError("encode observation offset", error)));
1134
- });
1135
- const parseOffset = Effect.fn("DoConversationStore.parseOffset")(function* (conversationId, offset) {
1136
- if (offset === void 0) return ZERO_CANONICAL_SEQUENCE;
1137
- const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
1138
- const conversationPrefix = `${DO_OFFSET_PREFIX}${encodeURIComponent(conversationId)}:`;
1139
- if (!text.startsWith(conversationPrefix)) return yield* ConversationStoreError.make({
1140
- operation: "decode observation offset",
1141
- message: "The observation offset belongs to a different adapter, storage version, or Conversation."
1142
- });
1143
- const sequenceText = text.slice(conversationPrefix.length);
1144
- if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield* ConversationStoreError.make({
1145
- operation: "decode observation offset",
1146
- message: "The observation offset is malformed."
1147
- });
1148
- return yield* Schema.decodeUnknownEffect(CanonicalSequence)(Number(sequenceText)).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
1149
- });
1150
- const mapFence = (conversationId, error) => FenceRejected.make({
1151
- conversationId,
1152
- actualEpoch: error.actualEpoch,
1153
- attemptedEpoch: error.producerEpoch
1154
- });
1155
- const encodeCanonicalRecord = Effect.fn("DoConversationStore.encodeCanonicalRecord")(function* (record) {
1156
- return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(record).pipe(Effect.mapError((error) => schemaStoreError("encode canonical record", error)));
1157
- });
1158
- const encodeCanonicalBatch = Effect.fn("DoConversationStore.encodeCanonicalBatch")(function* (batch) {
1159
- return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalBatch))(batch).pipe(Effect.mapError((error) => schemaStoreError("encode canonical batch", error)));
1160
- });
1161
- const encodeCheckpoint = Effect.fn("DoConversationStore.encodeCheckpoint")(function* (checkpoint) {
1162
- return yield* Schema.encodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpoint).pipe(Effect.mapError((error) => schemaStoreError("encode checkpoint", error)));
1163
- });
1164
- const decodeEnvelope = Effect.fn("DoConversationStore.decodeEnvelope")(function* (row) {
1165
- const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) => ConversationStoreError.make({
1166
- operation: "decode canonical record",
1167
- message: error.message
1168
- })));
1169
- const conversationId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.conversationId)(row.conversation_id).pipe(Effect.mapError((error) => schemaStoreError("decode conversation identity", error)));
1170
- const offset = yield* makeOffset(conversationId, row.sequence);
1171
- const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(row.batch_id).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
1172
- return CanonicalRecordEnvelope.make({
1173
- conversationId,
1174
- batchId,
1175
- sequence: row.sequence,
1176
- offset,
1177
- record
1178
- });
1179
- });
1180
- const decodeCheckpoint = Effect.fn("DoConversationStore.decodeCheckpoint")(function* (checkpointJson) {
1181
- return yield* Schema.decodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpointJson).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint", error)));
1182
- });
1183
- const requireConversation = Effect.fn("DoConversationStore.requireConversation")(function* (journal, conversationId) {
1184
- const rows = yield* journal.getConversation(conversationId).pipe(Effect.mapError((error) => storeError("read conversation", error)));
1185
- if (rows.length === 0) return yield* ConversationNotMaterialized.make({ conversationId });
1186
- return rows[0];
1187
- });
1188
- const tailDigestAt = Effect.fn("DoConversationStore.tailDigestAt")(function* (journal, conversationId, sequence) {
1189
- if (sequence === 0) return EMPTY_TAIL_DIGEST;
1190
- const digests = yield* journal.getTailDigestAt(conversationId, sequence).pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
1191
- if (digests.length !== 1) return yield* CheckpointRejected.make({
1192
- conversationId,
1193
- reason: "digest-mismatch"
1194
- });
1195
- return yield* Schema.decodeUnknownEffect(Digest)(digests[0]).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)));
1196
- });
1197
- const groupByKey = (rows, key) => {
1198
- const grouped = /* @__PURE__ */ new Map();
1199
- for (const row of rows) {
1200
- const existing = grouped.get(key(row));
1201
- if (existing === void 0) grouped.set(key(row), [row]);
1202
- else existing.push(row);
1203
- }
1204
- return grouped;
1205
- };
1206
- /**
1207
- * Opt-in full integrity audit (`verifyOnOpen`). Every stored payload is decoded, re-encoded,
1208
- * and re-digested against the canonical chain. Routine opens skip this scan: per-operation
1209
- * Schema decoding plus the digest chain already fail clearly on corrupt rows.
1210
- */
1211
- const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloads")(function* (journal, crypto) {
1212
- const stored = yield* journal.scanStoredPayloads();
1213
- const batches = yield* Effect.forEach(stored.batches, (batch) => Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(Effect.map((decoded) => ({
1214
- decoded,
1215
- row: batch
1216
- })), Effect.mapError((error) => DoStorageCorruptionError.make({
1217
- table: "effect_agent_canonical_batches",
1218
- rowKey: `${batch.conversation_id}/${batch.batch_id}`,
1219
- message: error.message
1220
- }))));
1221
- const records = yield* Effect.forEach(stored.records, (record) => Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(Effect.map((decoded) => ({
1222
- decoded,
1223
- row: record
1224
- })), Effect.mapError((error) => DoStorageCorruptionError.make({
1225
- table: "effect_agent_canonical_records",
1226
- rowKey: `${record.conversation_id}/${record.sequence}`,
1227
- message: error.message
1228
- }))));
1229
- const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpoint.checkpoint_json).pipe(Effect.map((decoded) => ({
1230
- decoded,
1231
- row: checkpoint
1232
- })), Effect.mapError((error) => DoStorageCorruptionError.make({
1233
- table: "effect_agent_checkpoints",
1234
- rowKey: `${checkpoint.conversation_id}/${checkpoint.through_sequence}`,
1235
- message: error.message
1236
- }))));
1237
- const batchesByConversation = groupByKey(batches, ({ row }) => row.conversation_id);
1238
- const recordsByConversation = groupByKey(records, ({ row }) => row.conversation_id);
1239
- const checkpointsByConversation = groupByKey(checkpoints, ({ row }) => row.conversation_id);
1240
- const materializedIds = new Set(stored.conversations.map((conversation) => conversation.conversation_id));
1241
- for (const conversation of stored.conversations) {
1242
- const conversationBatches = batchesByConversation.get(conversation.conversation_id) ?? [];
1243
- const conversationRecords = recordsByConversation.get(conversation.conversation_id) ?? [];
1244
- const conversationCheckpoints = checkpointsByConversation.get(conversation.conversation_id) ?? [];
1245
- const recordsByBatch = groupByKey(conversationRecords, ({ row }) => row.batch_id);
1246
- let previousDigest = EMPTY_TAIL_DIGEST;
1247
- let expectedSequence = 1;
1248
- const tailDigests = /* @__PURE__ */ new Map([[0, EMPTY_TAIL_DIGEST]]);
1249
- for (const { decoded: canonicalBatch, row: batchRow } of conversationBatches) {
1250
- const key = `${batchRow.conversation_id}/${batchRow.batch_id}`;
1251
- if (canonicalBatch.batchId !== batchRow.batch_id || batchRow.first_sequence !== expectedSequence || batchRow.last_sequence !== batchRow.first_sequence + canonicalBatch.records.length - 1) return yield* DoStorageCorruptionError.make({
1252
- table: "effect_agent_canonical_batches",
1253
- rowKey: key,
1254
- message: "Canonical batch identity, sequence, or record count is inconsistent."
1255
- });
1256
- const digest = yield* digestCanonicalBatch(previousDigest, canonicalBatch).pipe(Effect.provideService(Crypto.Crypto, crypto), Effect.mapError((error) => DoStorageCorruptionError.make({
1257
- table: "effect_agent_canonical_batches",
1258
- rowKey: key,
1259
- message: error.message
1260
- })));
1261
- if (batchRow.batch_digest !== digest || batchRow.tail_digest !== digest) return yield* DoStorageCorruptionError.make({
1262
- table: "effect_agent_canonical_batches",
1263
- rowKey: key,
1264
- message: "Canonical batch digest does not match its decoded content and prior tail."
1265
- });
1266
- const batchRecords = recordsByBatch.get(batchRow.batch_id) ?? [];
1267
- if (batchRecords.length !== canonicalBatch.records.length) return yield* DoStorageCorruptionError.make({
1268
- table: "effect_agent_canonical_records",
1269
- rowKey: key,
1270
- message: "Canonical batch and record-table counts differ."
1271
- });
1272
- for (let index = 0; index < canonicalBatch.records.length; index++) {
1273
- const expectedRecord = canonicalBatch.records[index];
1274
- const storedRecord = batchRecords[index];
1275
- const expectedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(expectedRecord).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
1276
- table: "effect_agent_canonical_batches",
1277
- rowKey: key,
1278
- message: error.message
1279
- })));
1280
- const storedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(storedRecord.decoded).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
1281
- table: "effect_agent_canonical_records",
1282
- rowKey: `${key}/${storedRecord.row.sequence}`,
1283
- message: error.message
1284
- })));
1285
- if (storedRecord.row.sequence !== batchRow.first_sequence + index || storedRecord.row.record_id !== expectedRecord.recordId || expectedJson !== storedJson) return yield* DoStorageCorruptionError.make({
1286
- table: "effect_agent_canonical_records",
1287
- rowKey: `${key}/${storedRecord.row.sequence}`,
1288
- message: "Canonical record identity, sequence, or payload differs from its batch."
1289
- });
1290
- }
1291
- previousDigest = digest;
1292
- expectedSequence = batchRow.last_sequence + 1;
1293
- tailDigests.set(batchRow.last_sequence, digest);
1294
- }
1295
- if (conversationRecords.length !== conversation.tail_sequence || conversation.tail_sequence !== expectedSequence - 1 || conversation.tail_digest !== previousDigest) return yield* DoStorageCorruptionError.make({
1296
- table: "effect_agent_conversations",
1297
- rowKey: conversation.conversation_id,
1298
- message: "Conversation tail does not match its canonical batch chain."
1299
- });
1300
- for (const checkpoint of conversationCheckpoints) if (checkpoint.decoded.conversationId !== conversation.conversation_id || checkpoint.decoded.throughSequence !== checkpoint.row.through_sequence || checkpoint.decoded.tailDigest !== checkpoint.row.tail_digest || tailDigests.get(checkpoint.row.through_sequence) !== checkpoint.row.tail_digest) return yield* DoStorageCorruptionError.make({
1301
- table: "effect_agent_checkpoints",
1302
- rowKey: `${conversation.conversation_id}/${checkpoint.row.through_sequence}`,
1303
- message: "Checkpoint identity or digest is not bound to a canonical batch tail."
1304
- });
1305
- }
1306
- if (batches.some(({ row }) => !materializedIds.has(row.conversation_id)) || records.some(({ row }) => !materializedIds.has(row.conversation_id)) || checkpoints.some(({ row }) => !materializedIds.has(row.conversation_id))) return yield* DoStorageCorruptionError.make({
1307
- table: "effect_agent_conversations",
1308
- rowKey: "startup_scan",
1309
- message: "Canonical rows exist without a materialized Conversation."
1310
- });
1311
- });
1312
- const makeServices$1 = Effect.fn("DoConversationStore.makeServices")(function* () {
1313
- const config = yield* DoStorageConfig;
1314
- const failpoint = yield* DoStorageFailpoint;
1315
- const sql = yield* SqlClient.SqlClient;
1316
- const crypto = yield* Crypto.Crypto;
1317
- const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
1318
- if (config.verifyOnOpen) yield* decodeStartupPayloads(journal, crypto);
1319
- const provideCrypto = (effect) => Effect.provideService(effect, Crypto.Crypto, crypto);
1320
- const hitFailpoint = Effect.fn("DoConversationStore.hitFailpoint")((location) => failpoint.hit(location).pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))));
1321
- const materialize = Effect.fn("DoConversationStore.materialize")(function* (request) {
1322
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationMaterialization))(request).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
1323
- const now = yield* Clock.currentTimeMillis;
1324
- yield* hitFailpoint("materialize:before");
1325
- yield* journal.materialize(validated.conversationId, new Date(now).toISOString(), EMPTY_TAIL_DIGEST, validated.producerEpoch).pipe(Effect.mapError((error) => error._tag === "DoFenceRejected" ? mapFence(validated.conversationId, error) : storeError("materialize conversation", error)));
1326
- yield* hitFailpoint("materialize:after");
1327
- });
1328
- const append = Effect.fn("DoConversationStore.append")(function* (request) {
1329
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
1330
- yield* requireConversation(journal, validated.conversationId);
1331
- const tailDigest = yield* provideCrypto(digestCanonicalBatch(validated.expectedTailDigest, validated.batch)).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
1332
- const batchJson = yield* encodeCanonicalBatch(validated.batch);
1333
- const rawRecords = yield* Effect.forEach(validated.batch.records, (record) => encodeCanonicalRecord(record).pipe(Effect.map((recordJson) => ({
1334
- recordId: record.recordId,
1335
- recordJson
1336
- }))));
1337
- const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
1338
- conversationId: validated.conversationId,
1339
- batchId: validated.batch.batchId,
1340
- batchDigest: tailDigest,
1341
- batchJson,
1342
- expectedTailSequence: validated.expectedTailSequence,
1343
- expectedTailDigest: validated.expectedTailDigest,
1344
- producerEpoch: validated.producerEpoch,
1345
- records: rawRecords,
1346
- tailDigest
1347
- }).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
1348
- yield* hitFailpoint("append:before");
1349
- const result = yield* journal.append(rawRequest).pipe(Effect.mapError((error) => {
1350
- if (error instanceof DoFenceRejected) return mapFence(validated.conversationId, error);
1351
- if (error instanceof DoAppendConflict) return error.actualTailSequence !== void 0 && isDigest(error.actualTailDigest) ? AppendConflict.make({
1352
- conversationId: validated.conversationId,
1353
- batchId: validated.batch.batchId,
1354
- reason: error.reason,
1355
- actualTailSequence: error.actualTailSequence,
1356
- actualTailDigest: error.actualTailDigest
1357
- }) : AppendConflict.make({
1358
- conversationId: validated.conversationId,
1359
- batchId: validated.batch.batchId,
1360
- reason: error.reason
1361
- });
1362
- return storeError("append canonical batch", error);
1363
- }), Effect.flatMap((result) => Schema.decodeUnknownEffect(AppendResult)(result).pipe(Effect.mapError((error) => schemaStoreError("decode append result", error)))));
1364
- yield* hitFailpoint("append:after");
1365
- return result;
1366
- });
1367
- const loadRecords = Effect.fn("DoConversationStore.loadRecords")(function* (request) {
1368
- const rows = yield* journal.read(request).pipe(Effect.mapError((error) => storeError("read canonical records", error)));
1369
- return yield* Effect.forEach(rows, decodeEnvelope);
1370
- });
1371
- const readEffect = Effect.fn("DoConversationStore.read")(function* (request) {
1372
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationRead))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation read", error)));
1373
- yield* requireConversation(journal, validated.conversationId);
1374
- const records = yield* loadRecords(RawReadRequest.make({
1375
- conversationId: validated.conversationId,
1376
- fromSequenceExclusive: validated.afterSequence ?? ZERO_CANONICAL_SEQUENCE,
1377
- limit: validated.limit
1378
- }));
1379
- return Stream.fromIterable(records);
1380
- });
1381
- const read = (request) => Stream.unwrap(readEffect(request));
1382
- const observeEffect = Effect.fn("DoConversationStore.observe")(function* (request) {
1383
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationObservation))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation observation", error)));
1384
- yield* requireConversation(journal, validated.conversationId);
1385
- const initialSequence = yield* parseOffset(validated.conversationId, validated.afterOffset);
1386
- const cursor = yield* Ref.make(initialSequence);
1387
- const poll = Effect.fn("DoConversationStore.observePoll")(function* () {
1388
- const fromSequenceExclusive = yield* Ref.get(cursor);
1389
- const records = yield* loadRecords(RawReadRequest.make({
1390
- conversationId: validated.conversationId,
1391
- fromSequenceExclusive,
1392
- limit: 1024
1393
- }));
1394
- if (records.length === 0) {
1395
- yield* Effect.sleep(config.observationPollInterval);
1396
- return [];
1397
- }
1398
- yield* Ref.set(cursor, records[records.length - 1].sequence);
1399
- return records;
1400
- });
1401
- return Stream.fromIterableEffectRepeat(poll());
1402
- });
1403
- const observe = (request) => Stream.unwrap(observeEffect(request));
1404
- const exportConversation = Effect.fn("DoConversationStore.export")(function* (request) {
1405
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationExportRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation export", error)));
1406
- yield* requireConversation(journal, validated.conversationId);
1407
- const exported = yield* journal.exportConversation(validated.conversationId).pipe(Effect.mapError((error) => storeError("export conversation", error)));
1408
- const records = yield* Effect.forEach(exported.records, decodeEnvelope);
1409
- if (records.length > 65536) return yield* ConversationStoreError.make({
1410
- operation: "decode conversation export",
1411
- message: "The conversation exceeds the current export record limit."
1412
- });
1413
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.conversation.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
1414
- return ConversationExport.make({
1415
- format: "effect-agent/conversation@1",
1416
- conversationId: validated.conversationId,
1417
- tailSequence: exported.conversation.tail_sequence,
1418
- tailDigest,
1419
- records
1420
- });
1421
- });
1422
- const inspectTail = Effect.fn("DoConversationStore.inspectTail")(function* (request) {
1423
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationTailRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
1424
- const conversation = yield* requireConversation(journal, validated.conversationId);
1425
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(conversation.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode tail digest", error)));
1426
- return ConversationTail.make({
1427
- conversationId: validated.conversationId,
1428
- tailSequence: conversation.tail_sequence,
1429
- tailDigest,
1430
- producerEpoch: conversation.producer_epoch
1431
- });
1432
- });
1433
- const saveCheckpoint = Effect.fn("DoConversationStore.saveCheckpoint")(function* (request) {
1434
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
1435
- const conversation = yield* requireConversation(journal, validated.checkpoint.conversationId);
1436
- if (validated.checkpoint.throughSequence > conversation.tail_sequence) return yield* CheckpointRejected.make({
1437
- conversationId: validated.checkpoint.conversationId,
1438
- reason: "ahead-of-tail"
1439
- });
1440
- if ((yield* tailDigestAt(journal, validated.checkpoint.conversationId, validated.checkpoint.throughSequence)) !== validated.checkpoint.tailDigest) return yield* CheckpointRejected.make({
1441
- conversationId: validated.checkpoint.conversationId,
1442
- reason: "digest-mismatch"
1443
- });
1444
- const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
1445
- const raw = RawCheckpoint.make({
1446
- conversationId: validated.checkpoint.conversationId,
1447
- throughSequence: validated.checkpoint.throughSequence,
1448
- tailDigest: validated.checkpoint.tailDigest,
1449
- checkpointJson
1450
- });
1451
- yield* hitFailpoint("save-checkpoint:before");
1452
- yield* journal.saveCheckpoint(raw).pipe(Effect.mapError((error) => error instanceof DoCheckpointConflict ? CheckpointRejected.make({
1453
- conversationId: validated.checkpoint.conversationId,
1454
- reason: "digest-mismatch"
1455
- }) : storeError("save checkpoint", error)));
1456
- yield* hitFailpoint("save-checkpoint:after");
1457
- });
1458
- const loadCheckpoint = Effect.fn("DoConversationStore.loadCheckpoint")(function* (request) {
1459
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
1460
- const conversation = yield* requireConversation(journal, validated.conversationId);
1461
- const rows = yield* journal.loadCheckpoint(validated.conversationId, validated.atOrBeforeSequence ?? conversation.tail_sequence).pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
1462
- if (rows.length === 0) return Option.none();
1463
- if (rows.length !== 1) return yield* ConversationStoreError.make({
1464
- operation: "load checkpoint",
1465
- message: `Expected at most one checkpoint row but found ${rows.length}.`
1466
- });
1467
- const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
1468
- if ((yield* tailDigestAt(journal, checkpoint.conversationId, checkpoint.throughSequence)) !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
1469
- conversationId: checkpoint.conversationId,
1470
- reason: "digest-mismatch"
1471
- });
1472
- return Option.some(checkpoint);
1473
- });
1474
- const conversationStore = ConversationStore.of({
1475
- append,
1476
- export: exportConversation,
1477
- inspectTail,
1478
- loadCheckpoint,
1479
- materialize,
1480
- observe,
1481
- read,
1482
- saveCheckpoint
1483
- });
1484
- return Context.make(ConversationStore, conversationStore);
1485
- });
1486
- /**
1487
- * Durable Object Conversation Store implementation with configuration, failpoint, SQL, and
1488
- * Crypto authority kept visible in its input channel.
1489
- */
1490
- const conversationStoreLayer = Layer.effectContext(makeServices$1());
1491
- /**
1492
- * Validated Durable Object storage configuration Layer with the documented defaults applied.
1493
- * Shared by the ConversationStore and SubmissionLedger convenience layers so their defaults
1494
- * cannot drift.
1495
- */
1496
- const storageConfigLayer = (options) => Layer.effect(DoStorageConfig)(Schema.decodeUnknownEffect(DoStorageConfigValue)({
1497
- observationPollInterval: options.observationPollInterval ?? 25,
1498
- ownershipLeaseDuration: options.ownershipLeaseDuration ?? Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION),
1499
- maxStoredValueBytes: options.maxStoredValueBytes ?? 19e5,
1500
- verifyOnOpen: options.verifyOnOpen ?? false
1501
- }).pipe(Effect.mapError((error) => DoStorageError.make({
1502
- cause: error,
1503
- operation: "configure Durable Object storage",
1504
- message: error.message
1505
- }))));
1506
- /** The failpoint Layer selected by convenience options: explicit handler or the no-op default. */
1507
- const storageFailpointLayer = (options) => options.failpoint === void 0 ? DoStorageFailpoint.layer : Layer.succeed(DoStorageFailpoint)({ hit: options.failpoint });
1508
- /**
1509
- * A composition-root convenience Layer for canonical Conversations inside one Durable Object,
1510
- * built over `ctx.storage`. Durable accepted work is served by the separate SubmissionLedger
1511
- * port; point both at the SAME `ctx.storage` so claims fence the same producer epochs
1512
- * (ADR-0011 D7's "same file" rule, transposed to one object's private database).
1513
- */
1514
- const layer = (options) => {
1515
- const sqlLayer = SqliteClient.layer({ storage: options.storage });
1516
- return conversationStoreLayer.pipe(Layer.provide(Layer.mergeAll(storageConfigLayer(options), storageFailpointLayer(options), sqlLayer, BrowserCrypto.layer)));
1517
- };
1518
- /** Create an adapter-owned resumable observation offset for a known canonical sequence. */
1519
- const observationOffsetAt = makeOffset;
1520
- //#endregion
1521
- //#region src/do-ledger.ts
1522
- /**
1523
- * Static decode-side ceiling; writes are bounded in bytes by the configured
1524
- * `maxStoredValueBytes` (see do-journal.ts).
1525
- */
1526
- const BoundedStoredText = Schema.String.check(Schema.isMaxLength(2e6));
1527
- const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
1528
- const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
1529
- const SCAN_PAGE_SIZE = 256;
1530
- const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
1531
- const MAX_IDENTIFIER_LENGTH = 1024;
1532
- var SubmissionRow = class extends Schema.Class("SubmissionRow")({
1533
- submission_id: BoundedIdentifier,
1534
- conversation_id: BoundedIdentifier,
1535
- queue_sequence: QueueSequence,
1536
- principal: BoundedIdentifier,
1537
- idempotency_key: BoundedIdentifier,
1538
- agent_id: BoundedIdentifier,
1539
- agent_digests_json: BoundedStoredText,
1540
- deployment_id: BoundedIdentifier,
1541
- input_json: BoundedStoredText,
1542
- input_digest: Digest,
1543
- receipt_id: BoundedIdentifier,
1544
- state: SubmissionState,
1545
- settled_outcome: Schema.NullOr(SettlementOutcome),
1546
- created_at: BoundedTimestamp,
1547
- ready_at: Schema.NullOr(BoundedTimestamp),
1548
- input_applied_record_id: Schema.NullOr(BoundedIdentifier),
1549
- input_applied_sequence: Schema.NullOr(CanonicalSequence),
1550
- joined_host_submission_id: Schema.NullOr(BoundedIdentifier),
1551
- suspended_reason_json: Schema.NullOr(BoundedStoredText),
1552
- suspended_at: Schema.NullOr(BoundedTimestamp),
1553
- unknown_reason: Schema.NullOr(BoundedStoredText),
1554
- unknown_tool_call_ids_json: Schema.NullOr(BoundedStoredText),
1555
- parent_submission_id: Schema.NullOr(BoundedIdentifier),
1556
- parent_tool_call_id: Schema.NullOr(BoundedIdentifier)
1557
- }) {};
1558
- var ChildReservationRow = class extends Schema.Class("ChildReservationRow")({
1559
- reservation_id: BoundedIdentifier,
1560
- parent_submission_id: BoundedIdentifier,
1561
- parent_tool_call_id: BoundedIdentifier,
1562
- child_submission_id: Schema.NullOr(BoundedIdentifier),
1563
- status: ChildReservationStatus,
1564
- allocation_json: BoundedStoredText,
1565
- allocation_digest: Digest,
1566
- accounting_json: Schema.NullOr(BoundedStoredText),
1567
- reserved_at: BoundedTimestamp,
1568
- release_began_at: Schema.NullOr(BoundedTimestamp),
1569
- released_at: Schema.NullOr(BoundedTimestamp)
1570
- }) {};
1571
- var ChildSettlementMarkerRow = class extends Schema.Class("ChildSettlementMarkerRow")({
1572
- parent_submission_id: BoundedIdentifier,
1573
- child_submission_id: BoundedIdentifier,
1574
- child_outcome: Schema.NullOr(SettlementOutcome),
1575
- recorded_at: BoundedTimestamp
1576
- }) {};
1577
- var ApprovalDecisionRow = class extends Schema.Class("ApprovalDecisionRow")({
1578
- submission_id: BoundedIdentifier,
1579
- tool_call_id: BoundedIdentifier,
1580
- decision: ApprovalDecision,
1581
- resolver: BoundedIdentifier,
1582
- reason: BoundedStoredText,
1583
- decided_at: BoundedTimestamp
1584
- }) {};
1585
- var UnknownResolutionRow = class extends Schema.Class("UnknownResolutionRow")({
1586
- submission_id: BoundedIdentifier,
1587
- tool_call_id: BoundedIdentifier,
1588
- author: BoundedIdentifier,
1589
- reason: BoundedStoredText,
1590
- resolution_json: BoundedStoredText,
1591
- resolved_at: BoundedTimestamp
1592
- }) {};
1593
- var OwnershipRow = class extends Schema.Class("OwnershipRow")({
1594
- submission_id: BoundedIdentifier,
1595
- attempt_id: BoundedIdentifier,
1596
- ownership_token: BoundedIdentifier,
1597
- producer_epoch: ProducerEpoch,
1598
- owner_producer_id: BoundedIdentifier,
1599
- lease_expires_at: BoundedTimestamp
1600
- }) {};
1601
- var ReservationRow = class extends Schema.Class("ReservationRow")({
1602
- submission_id: BoundedIdentifier,
1603
- settlement_id: BoundedIdentifier,
1604
- outcome: SettlementOutcome,
1605
- record_id: BoundedIdentifier,
1606
- record_json: BoundedStoredText,
1607
- record_digest: Digest,
1608
- reserved_at: BoundedTimestamp,
1609
- finalized_at: Schema.NullOr(BoundedTimestamp)
1610
- }) {};
1611
- var AbortIntentRow = class extends Schema.Class("AbortIntentRow")({
1612
- submission_id: BoundedIdentifier,
1613
- author: BoundedIdentifier,
1614
- reason: BoundedStoredText,
1615
- requested_at: BoundedTimestamp,
1616
- canonical_record_id: Schema.NullOr(BoundedIdentifier)
1617
- }) {};
1618
- var MaxQueueSequenceRow = class extends Schema.Class("MaxQueueSequenceRow")({ max_queue_sequence: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) }) {};
1619
- var CanonicalRecordIdRow = class extends Schema.Class("CanonicalRecordIdRow")({ record_id: BoundedIdentifier }) {};
1620
- const SUBMISSION_COLUMNS = `
1621
- submission_id,
1622
- conversation_id,
1623
- queue_sequence,
1624
- principal,
1625
- idempotency_key,
1626
- agent_id,
1627
- agent_digests_json,
1628
- deployment_id,
1629
- input_json,
1630
- input_digest,
1631
- receipt_id,
1632
- state,
1633
- settled_outcome,
1634
- created_at,
1635
- ready_at,
1636
- input_applied_record_id,
1637
- input_applied_sequence,
1638
- joined_host_submission_id,
1639
- suspended_reason_json,
1640
- suspended_at,
1641
- unknown_reason,
1642
- unknown_tool_call_ids_json,
1643
- parent_submission_id,
1644
- parent_tool_call_id
1645
- `;
1646
- const CHILD_RESERVATION_COLUMNS = `
1647
- reservation_id,
1648
- parent_submission_id,
1649
- parent_tool_call_id,
1650
- child_submission_id,
1651
- status,
1652
- allocation_json,
1653
- allocation_digest,
1654
- accounting_json,
1655
- reserved_at,
1656
- release_began_at,
1657
- released_at
1658
- `;
1659
- /** The branded ToolCallId schema, reached through the session port so no core import is needed. */
1660
- const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
1661
- const ToolCallIdList = Schema.Array(ToolCallIdSchema);
1662
- const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
1663
- const encodeDefinitionDigestsText = Schema.encodeEffect(Schema.fromJsonString(DefinitionDigests));
1664
- const encodeRecordEnvelopeText = Schema.encodeEffect(Schema.fromJsonString(RecordEnvelope));
1665
- const decodeRecordEnvelopeText = Schema.decodeEffect(Schema.fromJsonString(RecordEnvelope));
1666
- const encodeSuspensionReasonText = Schema.encodeEffect(Schema.fromJsonString(SuspensionReason));
1667
- const encodeUnknownResolutionText = Schema.encodeEffect(Schema.fromJsonString(UnknownResolution));
1668
- const encodeToolCallIdsText = Schema.encodeEffect(Schema.fromJsonString(ToolCallIdList));
1669
- const decodeToolCallIdsText = Schema.decodeEffect(Schema.fromJsonString(ToolCallIdList));
1670
- const parseStoredJsonText = Schema.decodeEffect(Schema.fromJsonString(Schema.Json));
1671
- const decodeAdmissionResult = Schema.decodeUnknownEffect(AdmissionResult);
1672
- const decodeClaim = Schema.decodeUnknownEffect(Claim);
1673
- const decodeOwnershipRenewal = Schema.decodeUnknownEffect(OwnershipRenewal);
1674
- const decodeSettlement = Schema.decodeUnknownEffect(Settlement);
1675
- const decodeAbortIntent = Schema.decodeUnknownEffect(AbortIntent);
1676
- const decodeOwnershipSnapshot = Schema.decodeUnknownEffect(OwnershipSnapshot);
1677
- const decodeInputAppliedMarker = Schema.decodeUnknownEffect(InputAppliedMarker);
1678
- const decodeSubmissionSnapshotUnknown = Schema.decodeUnknownEffect(SubmissionSnapshot);
1679
- const decodeSubmissionId = Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId);
1680
- const decodeQueueSequence = Schema.decodeUnknownEffect(QueueSequence);
1681
- const decodeUtcInstant = Schema.decodeUnknownEffect(Schema.DateTimeUtcFromString);
1682
- const decodeJoiningClaim = Schema.decodeUnknownEffect(JoiningClaim);
1683
- const decodeJoinSnapshot = Schema.decodeUnknownEffect(JoinSnapshot);
1684
- const decodeSuspensionSnapshot = Schema.decodeUnknownEffect(SuspensionSnapshot);
1685
- const decodeApprovalDecisionIntent = Schema.decodeUnknownEffect(ApprovalDecisionIntent);
1686
- const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResolutionIntent);
1687
- const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
1688
- const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(ChildBudgetReservationSnapshot);
1689
- const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
1690
- /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
1691
- const internalFailure = (operation) => (error) => LedgerError.make({
1692
- operation,
1693
- message: error.message,
1694
- cause: error
1695
- });
1696
- /**
1697
- * Classify raw SQL failures. Within one Durable Object there is exactly one writer, so the
1698
- * Node adapter's retryable `SqliteWriteContention` classification has no analogue: every raw
1699
- * failure is a `DoLedgerError` preserved as the LedgerError's cause.
1700
- */
1701
- const sqlFailure = (operation) => (error) => internalFailure(operation)(DoLedgerError.make({
1702
- cause: error,
1703
- operation,
1704
- message: error.message
1705
- }));
1706
- const corruptionFailure = (operation, table, rowKey, message) => internalFailure(operation)(DoStorageCorruptionError.make({
1707
- table,
1708
- rowKey,
1709
- message
1710
- }));
1711
- const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1712
- const config = yield* DoStorageConfig;
1713
- const failpoint = yield* DoStorageFailpoint;
1714
- const sql = yield* SqlClient.SqlClient;
1715
- const crypto = yield* Crypto.Crypto;
1716
- const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
1717
- const hitFailpoint = (location, operation) => failpoint.hit(location).pipe(Effect.mapError((error) => internalFailure(operation)(error)));
1718
- /**
1719
- * Run one ledger mutation under the journal's Durable Object storage-backed transaction so
1720
- * ownership-token and epoch checks are atomic with their writes (DUR-006). Transaction
1721
- * failures surface as LedgerError carrying the typed `DoStorageError` as cause.
1722
- */
1723
- const inWriteTransaction = (operation, effect) => journal.withWriteTransaction(operation)(effect).pipe(Effect.mapError((error) => error instanceof DoStorageError ? internalFailure(operation)(error) : error));
1724
- const mintUuid = (operation) => crypto.randomUUIDv7.pipe(Effect.mapError((error) => internalFailure(operation)(error)));
1725
- const currentInstant = Effect.map(Clock.currentTimeMillis, (millis) => ({
1726
- millis,
1727
- iso: new Date(millis).toISOString()
1728
- }));
1729
- const timestampMillis = (operation, rowKey) => (timestamp) => decodeUtcInstant(timestamp).pipe(Effect.map(DateTime.toEpochMillis), Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submission_ownership", rowKey, error.message)));
1730
- const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1731
- const readSubmission = Effect.fn("DoSubmissionLedger.readSubmission")(function* (operation, submissionId) {
1732
- const rows = yield* sql`
1733
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1734
- FROM effect_agent_submissions
1735
- WHERE submission_id = ${submissionId}
1736
- `.pipe(Effect.mapError(sqlFailure(operation)));
1737
- const decoded = yield* decodeSubmissionRows(operation, submissionId, rows);
1738
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", submissionId, "A submission primary key returned more than one row.");
1739
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1740
- });
1741
- const requireSubmission = Effect.fn("DoSubmissionLedger.requireSubmission")(function* (operation, submissionId) {
1742
- const submission = yield* readSubmission(operation, submissionId);
1743
- if (Option.isNone(submission)) return yield* LedgerError.make({
1744
- operation,
1745
- message: `Unknown submission ${submissionId}.`
1746
- });
1747
- return submission.value;
1748
- });
1749
- const readOwnership = Effect.fn("DoSubmissionLedger.readOwnership")(function* (operation, submissionId) {
1750
- const rows = yield* sql`
1751
- SELECT
1752
- submission_id,
1753
- attempt_id,
1754
- ownership_token,
1755
- producer_epoch,
1756
- owner_producer_id,
1757
- lease_expires_at
1758
- FROM effect_agent_submission_ownership
1759
- WHERE submission_id = ${submissionId}
1760
- `.pipe(Effect.mapError(sqlFailure(operation)));
1761
- const decoded = yield* decodeRows(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1762
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submission_ownership", submissionId, "An ownership primary key returned more than one row.");
1763
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1764
- });
1765
- const conversationEpoch = Effect.fn("DoSubmissionLedger.conversationEpoch")(function* (operation, conversationId) {
1766
- const conversations = yield* journal.getConversation(conversationId).pipe(Effect.mapError(internalFailure(operation)));
1767
- return conversations.length === 0 ? EPOCH_ZERO : conversations[0].producer_epoch;
1768
- });
1769
- /**
1770
- * Verify inside the surrounding write transaction that the presented token still owns the
1771
- * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
1772
- * Conversation's current producer epoch (DUR-006).
1773
- */
1774
- const requireOwnership = Effect.fn("DoSubmissionLedger.requireOwnership")(function* (operation, submission, ownershipToken) {
1775
- const ownership = yield* readOwnership(operation, submission.submission_id);
1776
- if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
1777
- const actualEpoch = yield* conversationEpoch(operation, submission.conversation_id);
1778
- const submissionId = yield* Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId)(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
1779
- return yield* OwnershipLost.make({
1780
- submissionId,
1781
- actualEpoch
1782
- });
1783
- }
1784
- return ownership.value;
1785
- });
1786
- const decodeSubmissionSnapshot = Effect.fn("DoSubmissionLedger.decodeSubmissionSnapshot")(function* (operation, row) {
1787
- const agentDigests = yield* parseStoredJsonText(row.agent_digests_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
1788
- const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
1789
- 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.");
1790
- return yield* decodeSubmissionSnapshotUnknown({
1791
- submissionId: row.submission_id,
1792
- conversationId: row.conversation_id,
1793
- queueSequence: row.queue_sequence,
1794
- principal: row.principal,
1795
- idempotencyKey: row.idempotency_key,
1796
- agentId: row.agent_id,
1797
- agentDigests,
1798
- deploymentId: row.deployment_id,
1799
- inputPayload,
1800
- inputDigest: row.input_digest,
1801
- receiptId: row.receipt_id,
1802
- state: row.state,
1803
- createdAt: row.created_at,
1804
- ...row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome },
1805
- ...row.ready_at === null ? {} : { readyAt: row.ready_at },
1806
- ...row.parent_submission_id === null || row.parent_tool_call_id === null ? {} : { parentLinkage: {
1807
- parentSubmissionId: row.parent_submission_id,
1808
- parentToolCallId: row.parent_tool_call_id
1809
- } }
1810
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
1811
- });
1812
- const readReservation = Effect.fn("DoSubmissionLedger.readReservation")(function* (operation, submissionId) {
1813
- const rows = yield* sql`
1814
- SELECT
1815
- submission_id,
1816
- settlement_id,
1817
- outcome,
1818
- record_id,
1819
- record_json,
1820
- record_digest,
1821
- reserved_at,
1822
- finalized_at
1823
- FROM effect_agent_settlement_reservations
1824
- WHERE submission_id = ${submissionId}
1825
- `.pipe(Effect.mapError(sqlFailure(operation)));
1826
- const decoded = yield* decodeRows(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1827
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", submissionId, "A settlement reservation primary key returned more than one row.");
1828
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1829
- });
1830
- const readAbortIntent = Effect.fn("DoSubmissionLedger.readAbortIntent")(function* (operation, submissionId) {
1831
- const rows = yield* sql`
1832
- SELECT
1833
- submission_id,
1834
- author,
1835
- reason,
1836
- requested_at,
1837
- canonical_record_id
1838
- FROM effect_agent_abort_intents
1839
- WHERE submission_id = ${submissionId}
1840
- `.pipe(Effect.mapError(sqlFailure(operation)));
1841
- const decoded = yield* decodeRows(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1842
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_abort_intents", submissionId, "An abort intent primary key returned more than one row.");
1843
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1844
- });
1845
- /**
1846
- * Read the durable cross-store child-settlement markers recorded against one parent
1847
- * Submission (the DC realization of the port's "cross-store adapters record a durable
1848
- * notification marker" contract).
1849
- */
1850
- const readChildSettlementMarkers = Effect.fn("DoSubmissionLedger.readChildSettlementMarkers")(function* (operation, parentSubmissionId) {
1851
- const rows = yield* sql`
1852
- SELECT
1853
- parent_submission_id,
1854
- child_submission_id,
1855
- child_outcome,
1856
- recorded_at
1857
- FROM effect_agent_child_settlements
1858
- WHERE parent_submission_id = ${parentSubmissionId}
1859
- ORDER BY child_submission_id ASC
1860
- `.pipe(Effect.mapError(sqlFailure(operation)));
1861
- return yield* decodeRows(Schema.Array(ChildSettlementMarkerRow), "effect_agent_child_settlements", parentSubmissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1862
- });
1863
- /**
1864
- * Whether one listed child is provably settled from THIS store: either its own row lives
1865
- * here and is settled (single-store evidence, identical to the Node adapter), or a durable
1866
- * cross-store notification marker was recorded for it (the child's row lives in another
1867
- * Durable Object and its owner reported the settlement through `recordChildSettled`).
1868
- */
1869
- const childProvablySettled = Effect.fn("DoSubmissionLedger.childProvablySettled")(function* (operation, markerChildren, childSubmissionId) {
1870
- if (markerChildren.has(childSubmissionId)) return true;
1871
- const childRow = yield* readSubmission(operation, childSubmissionId);
1872
- return Option.isSome(childRow) && childRow.value.state === "settled";
1873
- });
1874
- const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1875
- const readChildReservation = Effect.fn("DoSubmissionLedger.readChildReservation")(function* (operation, reservationId) {
1876
- const rows = yield* sql`
1877
- SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
1878
- FROM effect_agent_child_reservations
1879
- WHERE reservation_id = ${reservationId}
1880
- `.pipe(Effect.mapError(sqlFailure(operation)));
1881
- const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
1882
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_child_reservations", reservationId, "A child reservation primary key returned more than one row.");
1883
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1884
- });
1885
- const readChildReservationForCall = Effect.fn("DoSubmissionLedger.readChildReservationForCall")(function* (operation, parentSubmissionId, parentToolCallId) {
1886
- const rows = yield* sql`
1887
- SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
1888
- FROM effect_agent_child_reservations
1889
- WHERE parent_submission_id = ${parentSubmissionId}
1890
- AND parent_tool_call_id = ${parentToolCallId}
1891
- `.pipe(Effect.mapError(sqlFailure(operation)));
1892
- const decoded = yield* decodeChildReservationRows(operation, `${parentSubmissionId}/${parentToolCallId}`, rows);
1893
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_child_reservations", `${parentSubmissionId}/${parentToolCallId}`, "A parent Tool Call returned more than one child reservation.");
1894
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1895
- });
1896
- const childReservationSnapshotFromRow = Effect.fn("DoSubmissionLedger.childReservationSnapshotFromRow")(function* (operation, row) {
1897
- const rowFailure = (error) => corruptionFailure(operation, "effect_agent_child_reservations", row.reservation_id, error.message);
1898
- const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(Effect.mapError(rowFailure));
1899
- const accounting = row.accounting_json === null ? void 0 : yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
1900
- return yield* decodeChildReservationSnapshotUnknown({
1901
- reservationId: row.reservation_id,
1902
- parentSubmissionId: row.parent_submission_id,
1903
- parentToolCallId: row.parent_tool_call_id,
1904
- status: row.status,
1905
- allocation,
1906
- allocationDigest: row.allocation_digest,
1907
- reservedAt: row.reserved_at,
1908
- ...row.child_submission_id === null ? {} : { childSubmissionId: row.child_submission_id },
1909
- ...accounting === void 0 ? {} : { accounting },
1910
- ...row.release_began_at === null ? {} : { releaseBeganAt: row.release_began_at },
1911
- ...row.released_at === null ? {} : { releasedAt: row.released_at }
1912
- }).pipe(Effect.mapError(rowFailure));
1913
- });
1914
- const readApprovalDecisions = Effect.fn("DoSubmissionLedger.readApprovalDecisions")(function* (operation, submissionId) {
1915
- const rows = yield* sql`
1916
- SELECT
1917
- submission_id,
1918
- tool_call_id,
1919
- decision,
1920
- resolver,
1921
- reason,
1922
- decided_at
1923
- FROM effect_agent_approval_decisions
1924
- WHERE submission_id = ${submissionId}
1925
- ORDER BY tool_call_id ASC
1926
- `.pipe(Effect.mapError(sqlFailure(operation)));
1927
- return yield* decodeRows(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1928
- });
1929
- const approvalIntentFromRow = Effect.fn("DoSubmissionLedger.approvalIntentFromRow")(function* (operation, row) {
1930
- return yield* decodeApprovalDecisionIntent({
1931
- submissionId: row.submission_id,
1932
- toolCallId: row.tool_call_id,
1933
- decision: row.decision,
1934
- resolver: row.resolver,
1935
- reason: row.reason,
1936
- decidedAt: row.decided_at
1937
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_approval_decisions", `${row.submission_id}/${row.tool_call_id}`, error.message)));
1938
- });
1939
- const readUnknownResolutions = Effect.fn("DoSubmissionLedger.readUnknownResolutions")(function* (operation, submissionId) {
1940
- const rows = yield* sql`
1941
- SELECT
1942
- submission_id,
1943
- tool_call_id,
1944
- author,
1945
- reason,
1946
- resolution_json,
1947
- resolved_at
1948
- FROM effect_agent_unknown_resolutions
1949
- WHERE submission_id = ${submissionId}
1950
- ORDER BY tool_call_id ASC
1951
- `.pipe(Effect.mapError(sqlFailure(operation)));
1952
- return yield* decodeRows(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1953
- });
1954
- const unknownResolutionIntentFromRow = Effect.fn("DoSubmissionLedger.unknownResolutionIntentFromRow")(function* (operation, row) {
1955
- 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)));
1956
- return yield* decodeUnknownResolutionIntent({
1957
- submissionId: row.submission_id,
1958
- toolCallId: row.tool_call_id,
1959
- author: row.author,
1960
- reason: row.reason,
1961
- resolution,
1962
- resolvedAt: row.resolved_at
1963
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_unknown_resolutions", `${row.submission_id}/${row.tool_call_id}`, error.message)));
1964
- });
1965
- /** The Submission's marked-unknown open Tool Call identities, empty when never marked. */
1966
- const storedUnknownToolCallIds = Effect.fn("DoSubmissionLedger.storedUnknownToolCallIds")(function* (operation, submission) {
1967
- if (submission.unknown_tool_call_ids_json === null) return [];
1968
- return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", submission.submission_id, error.message)));
1969
- });
1970
- /**
1971
- * Canonical history is the abort authority (DUR-015): the intent's canonicalRecordId is
1972
- * derived from the shared canonical-records table using the deterministic abort record
1973
- * identity, never from a cached ledger marker.
1974
- */
1975
- const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (operation, conversationId, submissionId) {
1976
- const recordId = submissionAbortRecordId(submissionId);
1977
- const rows = yield* sql`
1978
- SELECT record_id
1979
- FROM effect_agent_canonical_records
1980
- WHERE conversation_id = ${conversationId}
1981
- AND record_id = ${recordId}
1982
- `.pipe(Effect.mapError(sqlFailure(operation)));
1983
- return (yield* decodeRows(Schema.Array(CanonicalRecordIdRow), "effect_agent_canonical_records", `${conversationId}/${recordId}`, rows).pipe(Effect.mapError(internalFailure(operation)))).length === 0 ? void 0 : recordId;
1984
- });
1985
- const abortIntentFromRow = Effect.fn("DoSubmissionLedger.abortIntentFromRow")(function* (operation, submission, submissionId, row) {
1986
- const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.conversation_id, submissionId);
1987
- return yield* decodeAbortIntent({
1988
- submissionId: row.submission_id,
1989
- author: row.author,
1990
- reason: row.reason,
1991
- requestedAt: row.requested_at,
1992
- ...canonicalRecordId === void 0 ? {} : { canonicalRecordId }
1993
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_abort_intents", row.submission_id, error.message)));
1994
- });
1995
- const capabilities = Effect.succeed(LedgerCapabilities.make({ durability: "durable-cloudflare" }));
1996
- const admit = Effect.fn("DoSubmissionLedger.admit")(function* (request) {
1997
- const operation = "ledger admit";
1998
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AdmissionRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
1999
- const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(Effect.mapError(internalFailure(operation)));
2000
- yield* journal.checkValueBound(operation, inputJson).pipe(Effect.mapError(internalFailure(operation)));
2001
- const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(Effect.mapError(internalFailure(operation)));
2002
- const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.conversationId}`;
2003
- if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) return yield* LedgerError.make({
2004
- operation,
2005
- message: `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Conversation identity.`
2006
- });
2007
- const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
2008
- yield* hitFailpoint("ledger:admit:before", operation);
2009
- const result = yield* inWriteTransaction(operation, Effect.gen(function* () {
2010
- const keyRowKey = `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`;
2011
- const existingRows = yield* sql`
2012
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2013
- FROM effect_agent_submissions
2014
- WHERE conversation_id = ${validated.conversationId}
2015
- AND principal = ${validated.principal}
2016
- AND idempotency_key = ${validated.idempotencyKey}
2017
- `.pipe(Effect.mapError(sqlFailure(operation)));
2018
- const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
2019
- if (existing.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", keyRowKey, "An admission idempotency key returned more than one row.");
2020
- if (existing.length === 1) {
2021
- 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;
2022
- if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) return yield* AdmissionConflict.make({
2023
- conversationId: validated.conversationId,
2024
- principal: validated.principal,
2025
- idempotencyKey: validated.idempotencyKey,
2026
- existingInputDigest: existing[0].input_digest,
2027
- attemptedInputDigest: validated.inputDigest
2028
- });
2029
- return yield* decodeAdmissionResult({
2030
- submissionId: existing[0].submission_id,
2031
- receiptId: existing[0].receipt_id,
2032
- queueSequence: existing[0].queue_sequence,
2033
- state: existing[0].state,
2034
- replayed: true
2035
- }).pipe(Effect.mapError(internalFailure(operation)));
2036
- }
2037
- const maxRows = yield* sql`
2038
- SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
2039
- FROM effect_agent_submissions
2040
- WHERE conversation_id = ${validated.conversationId}
2041
- `.pipe(Effect.mapError(sqlFailure(operation)));
2042
- const decodedMax = yield* decodeRows(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.conversationId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
2043
- const queueSequence = yield* decodeQueueSequence((decodedMax[0]?.max_queue_sequence ?? 0) + 1).pipe(Effect.mapError(internalFailure(operation)));
2044
- const now = yield* currentInstant;
2045
- yield* sql`
2046
- INSERT INTO effect_agent_submissions (
2047
- submission_id,
2048
- conversation_id,
2049
- queue_sequence,
2050
- principal,
2051
- idempotency_key,
2052
- agent_id,
2053
- agent_digests_json,
2054
- deployment_id,
2055
- input_json,
2056
- input_digest,
2057
- receipt_id,
2058
- state,
2059
- created_at,
2060
- parent_submission_id,
2061
- parent_tool_call_id
2062
- ) VALUES (
2063
- ${mintedSubmissionId},
2064
- ${validated.conversationId},
2065
- ${queueSequence},
2066
- ${validated.principal},
2067
- ${validated.idempotencyKey},
2068
- ${validated.agentId},
2069
- ${agentDigestsJson},
2070
- ${validated.deploymentId},
2071
- ${inputJson},
2072
- ${validated.inputDigest},
2073
- ${mintedReceiptId},
2074
- 'admitted',
2075
- ${now.iso},
2076
- ${validated.parentLinkage?.parentSubmissionId ?? null},
2077
- ${validated.parentLinkage?.parentToolCallId ?? null}
2078
- )
2079
- `.pipe(Effect.mapError(sqlFailure(operation)));
2080
- return yield* decodeAdmissionResult({
2081
- submissionId: mintedSubmissionId,
2082
- receiptId: mintedReceiptId,
2083
- queueSequence,
2084
- state: "admitted",
2085
- replayed: false
2086
- }).pipe(Effect.mapError(internalFailure(operation)));
2087
- }));
2088
- yield* hitFailpoint("ledger:admit:after", operation);
2089
- return result;
2090
- });
2091
- const markReady = Effect.fn("DoSubmissionLedger.markReady")(function* (request) {
2092
- const operation = "ledger mark ready";
2093
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkReadyRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2094
- yield* hitFailpoint("ledger:mark-ready:before", operation);
2095
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2096
- if ((yield* requireSubmission(operation, validated.submissionId)).state !== "admitted") return;
2097
- const now = yield* currentInstant;
2098
- yield* sql`
2099
- UPDATE effect_agent_submissions
2100
- SET state = 'ready', ready_at = ${now.iso}
2101
- WHERE submission_id = ${validated.submissionId}
2102
- `.pipe(Effect.mapError(sqlFailure(operation)));
2103
- }));
2104
- yield* hitFailpoint("ledger:mark-ready:after", operation);
2105
- });
2106
- const lookup = Effect.fn("DoSubmissionLedger.lookup")(function* (request) {
2107
- const operation = "ledger lookup";
2108
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookup))(request).pipe(Effect.mapError(internalFailure(operation)));
2109
- if (validated._tag === "SubmissionLookupById") {
2110
- const row = yield* readSubmission(operation, validated.submissionId);
2111
- if (Option.isNone(row)) return Option.none();
2112
- return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
2113
- }
2114
- const rows = yield* sql`
2115
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2116
- FROM effect_agent_submissions
2117
- WHERE conversation_id = ${validated.conversationId}
2118
- AND principal = ${validated.principal}
2119
- AND idempotency_key = ${validated.idempotencyKey}
2120
- `.pipe(Effect.mapError(sqlFailure(operation)));
2121
- const decoded = yield* decodeSubmissionRows(operation, `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2122
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, "An admission idempotency key returned more than one row.");
2123
- if (decoded.length === 0) return Option.none();
2124
- return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
2125
- });
2126
- const resolveAdmission = Effect.fn("DoSubmissionLedger.resolveAdmission")(function* (request) {
2127
- const operation = "ledger resolve admission";
2128
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookupByKey))(request).pipe(Effect.mapError(internalFailure(operation)));
2129
- const rows = yield* sql`
2130
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2131
- FROM effect_agent_submissions
2132
- WHERE conversation_id = ${validated.conversationId}
2133
- AND principal = ${validated.principal}
2134
- AND idempotency_key = ${validated.idempotencyKey}
2135
- `.pipe(Effect.mapError(sqlFailure(operation)));
2136
- const decoded = yield* decodeSubmissionRows(operation, `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2137
- if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, "An admission idempotency key returned more than one row.");
2138
- if (decoded.length === 0) return AdmissionNotAdmitted.make();
2139
- return AdmissionAdmitted.make({ submission: yield* decodeSubmissionSnapshot(operation, decoded[0]) });
2140
- });
2141
- const claim = Effect.fn("DoSubmissionLedger.claim")(function* (request) {
2142
- const operation = "ledger claim";
2143
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2144
- const attemptId = `attempt-${yield* mintUuid(operation)}`;
2145
- const ownershipToken = `owner-${yield* mintUuid(operation)}`;
2146
- yield* hitFailpoint("ledger:claim:before", operation);
2147
- const claimed = yield* inWriteTransaction(operation, Effect.gen(function* () {
2148
- const headRows = yield* sql`
2149
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2150
- FROM effect_agent_submissions
2151
- WHERE conversation_id = ${validated.conversationId}
2152
- AND state <> 'settled'
2153
- ORDER BY queue_sequence ASC
2154
- LIMIT 1
2155
- `.pipe(Effect.mapError(sqlFailure(operation)));
2156
- const heads = yield* decodeSubmissionRows(operation, validated.conversationId, headRows);
2157
- if (heads.length === 0) return Option.none();
2158
- const head = heads[0];
2159
- if (head.state === "joining" || head.state === "joined" || head.state === "suspended" || head.state === "unknown") return Option.none();
2160
- const now = yield* currentInstant;
2161
- const ownership = yield* readOwnership(operation, head.submission_id);
2162
- if (Option.isSome(ownership)) {
2163
- if ((yield* timestampMillis(operation, head.submission_id)(ownership.value.lease_expires_at)) > now.millis) return Option.none();
2164
- }
2165
- const conversations = yield* journal.getConversation(head.conversation_id).pipe(Effect.mapError(internalFailure(operation)));
2166
- let producerEpoch;
2167
- if (conversations.length === 0) {
2168
- producerEpoch = 1;
2169
- yield* sql`
2170
- INSERT INTO effect_agent_conversations (
2171
- conversation_id,
2172
- created_at,
2173
- tail_sequence,
2174
- tail_digest,
2175
- producer_epoch
2176
- ) VALUES (
2177
- ${head.conversation_id},
2178
- ${now.iso},
2179
- 0,
2180
- ${EMPTY_TAIL_DIGEST},
2181
- ${producerEpoch}
2182
- )
2183
- `.pipe(Effect.mapError(sqlFailure(operation)));
2184
- } else {
2185
- producerEpoch = conversations[0].producer_epoch + 1;
2186
- yield* sql`
2187
- UPDATE effect_agent_conversations
2188
- SET producer_epoch = ${producerEpoch}
2189
- WHERE conversation_id = ${head.conversation_id}
2190
- `.pipe(Effect.mapError(sqlFailure(operation)));
2191
- }
2192
- const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
2193
- yield* sql`
2194
- INSERT INTO effect_agent_submission_ownership (
2195
- submission_id,
2196
- attempt_id,
2197
- ownership_token,
2198
- producer_epoch,
2199
- owner_producer_id,
2200
- lease_expires_at
2201
- ) VALUES (
2202
- ${head.submission_id},
2203
- ${attemptId},
2204
- ${ownershipToken},
2205
- ${producerEpoch},
2206
- ${validated.producerId},
2207
- ${leaseExpiresAt}
2208
- )
2209
- ON CONFLICT (submission_id) DO UPDATE SET
2210
- attempt_id = excluded.attempt_id,
2211
- ownership_token = excluded.ownership_token,
2212
- producer_epoch = excluded.producer_epoch,
2213
- owner_producer_id = excluded.owner_producer_id,
2214
- lease_expires_at = excluded.lease_expires_at
2215
- `.pipe(Effect.mapError(sqlFailure(operation)));
2216
- yield* sql`
2217
- INSERT INTO effect_agent_attempts (
2218
- attempt_id,
2219
- submission_id,
2220
- conversation_id,
2221
- owner_producer_id,
2222
- producer_epoch,
2223
- claimed_at
2224
- ) VALUES (
2225
- ${attemptId},
2226
- ${head.submission_id},
2227
- ${head.conversation_id},
2228
- ${validated.producerId},
2229
- ${producerEpoch},
2230
- ${now.iso}
2231
- )
2232
- `.pipe(Effect.mapError(sqlFailure(operation)));
2233
- if (head.state === "ready") yield* sql`
2234
- UPDATE effect_agent_submissions
2235
- SET state = 'running'
2236
- WHERE submission_id = ${head.submission_id}
2237
- `.pipe(Effect.mapError(sqlFailure(operation)));
2238
- const inputPayload = yield* parseStoredJsonText(head.input_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", head.submission_id, error.message)));
2239
- return Option.some(yield* decodeClaim({
2240
- submissionId: head.submission_id,
2241
- attemptId,
2242
- ownershipToken,
2243
- producerEpoch,
2244
- leaseExpiresAt,
2245
- inputPayload
2246
- }).pipe(Effect.mapError(internalFailure(operation))));
2247
- }));
2248
- yield* hitFailpoint("ledger:claim:after", operation);
2249
- return claimed;
2250
- });
2251
- const renewOwnership = Effect.fn("DoSubmissionLedger.renewOwnership")(function* (request) {
2252
- const operation = "ledger renew ownership";
2253
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RenewOwnershipRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2254
- yield* hitFailpoint("ledger:renew:before", operation);
2255
- const renewal = yield* inWriteTransaction(operation, Effect.gen(function* () {
2256
- const submission = yield* requireSubmission(operation, validated.submissionId);
2257
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2258
- const now = yield* currentInstant;
2259
- const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
2260
- yield* sql`
2261
- UPDATE effect_agent_submission_ownership
2262
- SET lease_expires_at = ${leaseExpiresAt}
2263
- WHERE submission_id = ${validated.submissionId}
2264
- `.pipe(Effect.mapError(sqlFailure(operation)));
2265
- return yield* decodeOwnershipRenewal({
2266
- ownershipToken: validated.ownershipToken,
2267
- leaseExpiresAt
2268
- }).pipe(Effect.mapError(internalFailure(operation)));
2269
- }));
2270
- yield* hitFailpoint("ledger:renew:after", operation);
2271
- return renewal;
2272
- });
2273
- const releaseOwnership = Effect.fn("DoSubmissionLedger.releaseOwnership")(function* (request) {
2274
- const operation = "ledger release ownership";
2275
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseOwnershipRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2276
- yield* hitFailpoint("ledger:release:before", operation);
2277
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2278
- const submission = yield* requireSubmission(operation, validated.submissionId);
2279
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2280
- yield* sql`
2281
- DELETE FROM effect_agent_submission_ownership
2282
- WHERE submission_id = ${validated.submissionId}
2283
- `.pipe(Effect.mapError(sqlFailure(operation)));
2284
- if (submission.state === "running") yield* sql`
2285
- UPDATE effect_agent_submissions
2286
- SET state = 'ready'
2287
- WHERE submission_id = ${validated.submissionId}
2288
- `.pipe(Effect.mapError(sqlFailure(operation)));
2289
- }));
2290
- yield* hitFailpoint("ledger:release:after", operation);
2291
- });
2292
- const markInputApplied = Effect.fn("DoSubmissionLedger.markInputApplied")(function* (request) {
2293
- const operation = "ledger mark input applied";
2294
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkInputAppliedRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2295
- yield* hitFailpoint("ledger:mark-input-applied:before", operation);
2296
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2297
- const submission = yield* requireSubmission(operation, validated.submissionId);
2298
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2299
- if (submission.input_applied_record_id !== null) {
2300
- if (submission.input_applied_record_id === validated.recordId && submission.input_applied_sequence === validated.sequence) return;
2301
- return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A different canonical input marker is already recorded for this Submission.");
2302
- }
2303
- yield* sql`
2304
- UPDATE effect_agent_submissions
2305
- SET
2306
- input_applied_record_id = ${validated.recordId},
2307
- input_applied_sequence = ${validated.sequence},
2308
- state = CASE
2309
- WHEN state IN ('admitted', 'ready', 'running') THEN 'input-applied'
2310
- ELSE state
2311
- END
2312
- WHERE submission_id = ${validated.submissionId}
2313
- `.pipe(Effect.mapError(sqlFailure(operation)));
2314
- }));
2315
- yield* hitFailpoint("ledger:mark-input-applied:after", operation);
2316
- });
2317
- const reserveSettlement = Effect.fn("DoSubmissionLedger.reserveSettlement")(function* (request) {
2318
- const operation = "ledger reserve settlement";
2319
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementReservation))(request).pipe(Effect.mapError(internalFailure(operation)));
2320
- const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(Effect.mapError(internalFailure(operation)));
2321
- yield* journal.checkValueBound(operation, recordJson).pipe(Effect.mapError(internalFailure(operation)));
2322
- yield* hitFailpoint("ledger:reserve-settlement:before", operation);
2323
- const reserved = yield* inWriteTransaction(operation, Effect.gen(function* () {
2324
- const existing = yield* readReservation(operation, validated.submissionId);
2325
- if (Option.isSome(existing)) {
2326
- 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({
2327
- submissionId: validated.submissionId,
2328
- existingOutcome: existing.value.outcome
2329
- });
2330
- const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, error.message)));
2331
- return ReservedSettlement.make({
2332
- submissionId: validated.submissionId,
2333
- settlementId: validated.settlementId,
2334
- outcome: validated.outcome,
2335
- record,
2336
- recordDigest: validated.recordDigest,
2337
- replayed: true
2338
- });
2339
- }
2340
- const submission = yield* requireSubmission(operation, validated.submissionId);
2341
- if (submission.state === "settled") {
2342
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2343
- return yield* SettlementConflict.make({
2344
- submissionId: validated.submissionId,
2345
- existingOutcome: submission.settled_outcome
2346
- });
2347
- }
2348
- if (!(submission.state === "joined" && submission.joined_host_submission_id !== null)) {
2349
- let queuedAbortSettlement = false;
2350
- if (validated.outcome === "aborted" && (submission.state === "ready" || submission.state === "terminalizing")) {
2351
- const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
2352
- if (Option.isSome(abortIntent)) {
2353
- const ownership = yield* readOwnership(operation, validated.submissionId);
2354
- queuedAbortSettlement = Option.isNone(ownership);
2355
- }
2356
- }
2357
- if (!queuedAbortSettlement) yield* requireOwnership(operation, submission, validated.ownershipToken);
2358
- }
2359
- const now = yield* currentInstant;
2360
- yield* sql`
2361
- INSERT INTO effect_agent_settlement_reservations (
2362
- submission_id,
2363
- settlement_id,
2364
- outcome,
2365
- record_id,
2366
- record_json,
2367
- record_digest,
2368
- reserved_at
2369
- ) VALUES (
2370
- ${validated.submissionId},
2371
- ${validated.settlementId},
2372
- ${validated.outcome},
2373
- ${validated.record.recordId},
2374
- ${recordJson},
2375
- ${validated.recordDigest},
2376
- ${now.iso}
2377
- )
2378
- `.pipe(Effect.mapError(sqlFailure(operation)));
2379
- yield* sql`
2380
- UPDATE effect_agent_submissions
2381
- SET state = 'terminalizing'
2382
- WHERE submission_id = ${validated.submissionId}
2383
- `.pipe(Effect.mapError(sqlFailure(operation)));
2384
- return ReservedSettlement.make({
2385
- submissionId: validated.submissionId,
2386
- settlementId: validated.settlementId,
2387
- outcome: validated.outcome,
2388
- record: validated.record,
2389
- recordDigest: validated.recordDigest,
2390
- replayed: false
2391
- });
2392
- }));
2393
- yield* hitFailpoint("ledger:reserve-settlement:after", operation);
2394
- return reserved;
2395
- });
2396
- const finalizeSettlement = Effect.fn("DoSubmissionLedger.finalizeSettlement")(function* (request) {
2397
- const operation = "ledger finalize settlement";
2398
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementFinalization))(request).pipe(Effect.mapError(internalFailure(operation)));
2399
- yield* hitFailpoint("ledger:finalize-settlement:before", operation);
2400
- const settlement = yield* inWriteTransaction(operation, Effect.gen(function* () {
2401
- const reservation = yield* readReservation(operation, validated.submissionId);
2402
- if (Option.isNone(reservation)) return yield* LedgerError.make({
2403
- operation,
2404
- message: `No settlement reservation exists for submission ${validated.submissionId}.`
2405
- });
2406
- if (reservation.value.settlement_id !== validated.settlementId) return yield* SettlementConflict.make({
2407
- submissionId: validated.submissionId,
2408
- existingOutcome: reservation.value.outcome
2409
- });
2410
- const submission = yield* requireSubmission(operation, validated.submissionId);
2411
- if (submission.state === "settled") {
2412
- 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.");
2413
- return yield* decodeSettlement({
2414
- submissionId: validated.submissionId,
2415
- settlementId: validated.settlementId,
2416
- receiptId: submission.receipt_id,
2417
- outcome: reservation.value.outcome,
2418
- settledAt: reservation.value.finalized_at
2419
- }).pipe(Effect.mapError(internalFailure(operation)));
2420
- }
2421
- const now = yield* currentInstant;
2422
- yield* sql`
2423
- UPDATE effect_agent_submissions
2424
- SET state = 'settled', settled_outcome = ${reservation.value.outcome}
2425
- WHERE submission_id = ${validated.submissionId}
2426
- `.pipe(Effect.mapError(sqlFailure(operation)));
2427
- yield* sql`
2428
- UPDATE effect_agent_settlement_reservations
2429
- SET finalized_at = ${now.iso}
2430
- WHERE submission_id = ${validated.submissionId}
2431
- `.pipe(Effect.mapError(sqlFailure(operation)));
2432
- yield* sql`
2433
- DELETE FROM effect_agent_submission_ownership
2434
- WHERE submission_id = ${validated.submissionId}
2435
- `.pipe(Effect.mapError(sqlFailure(operation)));
2436
- return yield* decodeSettlement({
2437
- submissionId: validated.submissionId,
2438
- settlementId: validated.settlementId,
2439
- receiptId: submission.receipt_id,
2440
- outcome: reservation.value.outcome,
2441
- settledAt: now.iso
2442
- }).pipe(Effect.mapError(internalFailure(operation)));
2443
- }));
2444
- yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2445
- return settlement;
2446
- });
2447
- const requestAbort = Effect.fn("DoSubmissionLedger.requestAbort")(function* (request) {
2448
- const operation = "ledger request abort";
2449
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AbortCommand))(request).pipe(Effect.mapError(internalFailure(operation)));
2450
- yield* hitFailpoint("ledger:request-abort:before", operation);
2451
- const intent = yield* inWriteTransaction(operation, Effect.gen(function* () {
2452
- const submission = yield* requireSubmission(operation, validated.submissionId);
2453
- if (submission.state === "settled") {
2454
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2455
- return yield* SettlementConflict.make({
2456
- submissionId: validated.submissionId,
2457
- existingOutcome: submission.settled_outcome
2458
- });
2459
- }
2460
- if (submission.state === "joined") {
2461
- if (submission.joined_host_submission_id === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A joined Submission carries no host linkage.");
2462
- const hostSubmissionId = yield* decodeSubmissionId(submission.joined_host_submission_id).pipe(Effect.mapError(internalFailure(operation)));
2463
- return yield* JoinedToHost.make({
2464
- submissionId: validated.submissionId,
2465
- hostSubmissionId
2466
- });
2467
- }
2468
- const existing = yield* readAbortIntent(operation, validated.submissionId);
2469
- if (Option.isSome(existing)) return yield* abortIntentFromRow(operation, submission, validated.submissionId, existing.value);
2470
- const now = yield* currentInstant;
2471
- yield* sql`
2472
- INSERT INTO effect_agent_abort_intents (
2473
- submission_id,
2474
- author,
2475
- reason,
2476
- requested_at
2477
- ) VALUES (
2478
- ${validated.submissionId},
2479
- ${validated.author},
2480
- ${validated.reason},
2481
- ${now.iso}
2482
- )
2483
- `.pipe(Effect.mapError(sqlFailure(operation)));
2484
- const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.conversation_id, validated.submissionId);
2485
- return yield* decodeAbortIntent({
2486
- submissionId: validated.submissionId,
2487
- author: validated.author,
2488
- reason: validated.reason,
2489
- requestedAt: now.iso,
2490
- ...canonicalRecordId === void 0 ? {} : { canonicalRecordId }
2491
- }).pipe(Effect.mapError(internalFailure(operation)));
2492
- }));
2493
- yield* hitFailpoint("ledger:request-abort:after", operation);
2494
- return intent;
2495
- });
2496
- const claimJoining = Effect.fn("DoSubmissionLedger.claimJoining")(function* (request) {
2497
- const operation = "ledger claim joining";
2498
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2499
- yield* hitFailpoint("ledger:claim-joining:before", operation);
2500
- const claims = yield* inWriteTransaction(operation, Effect.gen(function* () {
2501
- const host = yield* requireSubmission(operation, validated.hostSubmissionId);
2502
- if (host.conversation_id !== validated.conversationId) return yield* LedgerError.make({
2503
- operation,
2504
- message: `Host submission ${validated.hostSubmissionId} does not belong to conversation ${validated.conversationId}.`
2505
- });
2506
- yield* requireOwnership(operation, host, validated.ownershipToken);
2507
- const laterRows = yield* sql`
2508
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2509
- FROM effect_agent_submissions
2510
- WHERE conversation_id = ${validated.conversationId}
2511
- AND queue_sequence > ${host.queue_sequence}
2512
- ORDER BY queue_sequence ASC
2513
- `.pipe(Effect.mapError(sqlFailure(operation)));
2514
- const later = yield* decodeSubmissionRows(operation, validated.conversationId, laterRows);
2515
- const claimed = [];
2516
- for (const row of later) {
2517
- if (claimed.length >= validated.maxCount) break;
2518
- if ((row.state === "joining" || row.state === "joined") && row.joined_host_submission_id === validated.hostSubmissionId) continue;
2519
- if (row.state === "settled" && row.settled_outcome === "aborted") continue;
2520
- if (row.state !== "ready") break;
2521
- yield* sql`
2522
- UPDATE effect_agent_submissions
2523
- SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
2524
- WHERE submission_id = ${row.submission_id}
2525
- `.pipe(Effect.mapError(sqlFailure(operation)));
2526
- const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", row.submission_id, error.message)));
2527
- claimed.push(yield* decodeJoiningClaim({
2528
- submissionId: row.submission_id,
2529
- queueSequence: row.queue_sequence,
2530
- inputPayload
2531
- }).pipe(Effect.mapError(internalFailure(operation))));
2532
- }
2533
- return claimed;
2534
- }));
2535
- yield* hitFailpoint("ledger:claim-joining:after", operation);
2536
- return claims;
2537
- });
2538
- const markJoined = Effect.fn("DoSubmissionLedger.markJoined")(function* (request) {
2539
- const operation = "ledger mark joined";
2540
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkJoinedRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2541
- yield* hitFailpoint("ledger:mark-joined:before", operation);
2542
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2543
- const submission = yield* requireSubmission(operation, validated.submissionId);
2544
- if (submission.joined_host_submission_id === null) return yield* LedgerError.make({
2545
- operation,
2546
- message: `Submission ${validated.submissionId} was never claimed for joining.`
2547
- });
2548
- const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
2549
- yield* requireOwnership(operation, host, validated.ownershipToken);
2550
- if (submission.input_applied_record_id !== null) {
2551
- if (submission.input_applied_record_id === validated.recordId && submission.input_applied_sequence === validated.sequence) return;
2552
- return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A different join marker is already recorded for this Submission.");
2553
- }
2554
- if (submission.state !== "joining" && submission.state !== "joined") return yield* LedgerError.make({
2555
- operation,
2556
- message: `Cannot mark submission ${validated.submissionId} joined from state ${submission.state}.`
2557
- });
2558
- yield* sql`
2559
- UPDATE effect_agent_submissions
2560
- SET
2561
- input_applied_record_id = ${validated.recordId},
2562
- input_applied_sequence = ${validated.sequence},
2563
- state = 'joined'
2564
- WHERE submission_id = ${validated.submissionId}
2565
- `.pipe(Effect.mapError(sqlFailure(operation)));
2566
- }));
2567
- yield* hitFailpoint("ledger:mark-joined:after", operation);
2568
- });
2569
- const revertJoining = Effect.fn("DoSubmissionLedger.revertJoining")(function* (request) {
2570
- const operation = "ledger revert joining";
2571
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RevertJoiningRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2572
- yield* hitFailpoint("ledger:revert-joining:before", operation);
2573
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2574
- if ((yield* requireSubmission(operation, validated.submissionId)).state !== "joining") return;
2575
- yield* sql`
2576
- UPDATE effect_agent_submissions
2577
- SET state = 'ready', joined_host_submission_id = NULL
2578
- WHERE submission_id = ${validated.submissionId}
2579
- `.pipe(Effect.mapError(sqlFailure(operation)));
2580
- }));
2581
- yield* hitFailpoint("ledger:revert-joining:after", operation);
2582
- });
2583
- const suspend = Effect.fn("DoSubmissionLedger.suspend")(function* (request) {
2584
- const operation = "ledger suspend";
2585
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SuspendRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2586
- const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(Effect.mapError(internalFailure(operation)));
2587
- yield* hitFailpoint("ledger:suspend:before", operation);
2588
- const outcome = yield* inWriteTransaction(operation, Effect.gen(function* () {
2589
- const submission = yield* requireSubmission(operation, validated.submissionId);
2590
- if (submission.state === "settled") {
2591
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2592
- return yield* SettlementConflict.make({
2593
- submissionId: validated.submissionId,
2594
- existingOutcome: submission.settled_outcome
2595
- });
2596
- }
2597
- const reservation = yield* readReservation(operation, validated.submissionId);
2598
- if (Option.isSome(reservation)) return yield* SettlementConflict.make({
2599
- submissionId: validated.submissionId,
2600
- existingOutcome: reservation.value.outcome
2601
- });
2602
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2603
- if (validated.reason._tag === "ApprovalPending") {
2604
- const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
2605
- const decided = new Set(decisions.map((row) => row.tool_call_id));
2606
- if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return "resume-immediately";
2607
- } else {
2608
- const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
2609
- const markerChildren = new Set(markers.map((row) => row.child_submission_id));
2610
- let allSettled = true;
2611
- for (const child of validated.reason.children) if (!(yield* childProvablySettled(operation, markerChildren, child.childSubmissionId))) {
2612
- allSettled = false;
2613
- break;
2614
- }
2615
- if (allSettled) return "resume-immediately";
2616
- }
2617
- const now = yield* currentInstant;
2618
- yield* sql`
2619
- UPDATE effect_agent_submissions
2620
- SET
2621
- state = 'suspended',
2622
- suspended_reason_json = ${reasonJson},
2623
- suspended_at = ${now.iso}
2624
- WHERE submission_id = ${validated.submissionId}
2625
- `.pipe(Effect.mapError(sqlFailure(operation)));
2626
- yield* sql`
2627
- DELETE FROM effect_agent_submission_ownership
2628
- WHERE submission_id = ${validated.submissionId}
2629
- `.pipe(Effect.mapError(sqlFailure(operation)));
2630
- return "suspended";
2631
- }));
2632
- yield* hitFailpoint("ledger:suspend:after", operation);
2633
- return outcome;
2634
- });
2635
- /**
2636
- * Once every pending call of a recorded ApprovalPending suspension has a decision intent,
2637
- * the lane wakes: suspended → input-applied, suspension cleared (plan §2.6). A
2638
- * WaitingForChild suspension wakes only through recordChildSettled. Runs inside the caller's
2639
- * write transaction.
2640
- */
2641
- const wakeSuspendedIfCovered = Effect.fn("DoSubmissionLedger.wakeSuspendedIfCovered")(function* (operation, submission) {
2642
- if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
2643
- 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)));
2644
- if (reason._tag !== "ApprovalPending") return;
2645
- const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
2646
- const decided = new Set(decisions.map((row) => row.tool_call_id));
2647
- if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
2648
- yield* sql`
2649
- UPDATE effect_agent_submissions
2650
- SET
2651
- state = 'input-applied',
2652
- suspended_reason_json = NULL,
2653
- suspended_at = NULL
2654
- WHERE submission_id = ${submission.submission_id}
2655
- `.pipe(Effect.mapError(sqlFailure(operation)));
2656
- });
2657
- const recordApprovalDecision = Effect.fn("DoSubmissionLedger.recordApprovalDecision")(function* (command) {
2658
- const operation = "ledger record approval decision";
2659
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ApprovalDecisionCommand))(command).pipe(Effect.mapError(internalFailure(operation)));
2660
- yield* hitFailpoint("ledger:approval-decision:before", operation);
2661
- const intent = yield* inWriteTransaction(operation, Effect.gen(function* () {
2662
- const submission = yield* requireSubmission(operation, validated.submissionId);
2663
- if (submission.state === "settled") {
2664
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2665
- return yield* SettlementConflict.make({
2666
- submissionId: validated.submissionId,
2667
- existingOutcome: submission.settled_outcome
2668
- });
2669
- }
2670
- const existing = (yield* readApprovalDecisions(operation, validated.submissionId)).find((row) => row.tool_call_id === validated.toolCallId);
2671
- if (existing !== void 0) {
2672
- if (existing.decision !== validated.decision) return yield* ApprovalConflict.make({
2673
- submissionId: validated.submissionId,
2674
- toolCallId: validated.toolCallId,
2675
- existingDecision: existing.decision
2676
- });
2677
- return yield* approvalIntentFromRow(operation, existing);
2678
- }
2679
- const now = yield* currentInstant;
2680
- yield* sql`
2681
- INSERT INTO effect_agent_approval_decisions (
2682
- submission_id,
2683
- tool_call_id,
2684
- decision,
2685
- resolver,
2686
- reason,
2687
- decided_at
2688
- ) VALUES (
2689
- ${validated.submissionId},
2690
- ${validated.toolCallId},
2691
- ${validated.decision},
2692
- ${validated.resolver},
2693
- ${validated.reason},
2694
- ${now.iso}
2695
- )
2696
- `.pipe(Effect.mapError(sqlFailure(operation)));
2697
- yield* wakeSuspendedIfCovered(operation, submission);
2698
- return yield* decodeApprovalDecisionIntent({
2699
- submissionId: validated.submissionId,
2700
- toolCallId: validated.toolCallId,
2701
- decision: validated.decision,
2702
- resolver: validated.resolver,
2703
- reason: validated.reason,
2704
- decidedAt: now.iso
2705
- }).pipe(Effect.mapError(internalFailure(operation)));
2706
- }));
2707
- yield* hitFailpoint("ledger:approval-decision:after", operation);
2708
- return intent;
2709
- });
2710
- const markUnknown = Effect.fn("DoSubmissionLedger.markUnknown")(function* (request) {
2711
- const operation = "ledger mark unknown";
2712
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkUnknownRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2713
- yield* hitFailpoint("ledger:mark-unknown:before", operation);
2714
- yield* inWriteTransaction(operation, Effect.gen(function* () {
2715
- const submission = yield* requireSubmission(operation, validated.submissionId);
2716
- if (submission.state === "settled") {
2717
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2718
- return yield* SettlementConflict.make({
2719
- submissionId: validated.submissionId,
2720
- existingOutcome: submission.settled_outcome
2721
- });
2722
- }
2723
- const reservation = yield* readReservation(operation, validated.submissionId);
2724
- if (Option.isSome(reservation)) return yield* SettlementConflict.make({
2725
- submissionId: validated.submissionId,
2726
- existingOutcome: reservation.value.outcome
2727
- });
2728
- const existingIds = yield* storedUnknownToolCallIds(operation, submission);
2729
- const known = new Set(existingIds);
2730
- const merged = [...existingIds, ...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId))];
2731
- const idsJson = yield* encodeToolCallIdsText(merged).pipe(Effect.mapError(internalFailure(operation)));
2732
- yield* sql`
2733
- UPDATE effect_agent_submissions
2734
- SET
2735
- state = 'unknown',
2736
- unknown_reason = ${submission.unknown_reason ?? validated.reason},
2737
- unknown_tool_call_ids_json = ${idsJson}
2738
- WHERE submission_id = ${validated.submissionId}
2739
- `.pipe(Effect.mapError(sqlFailure(operation)));
2740
- }));
2741
- yield* hitFailpoint("ledger:mark-unknown:after", operation);
2742
- });
2743
- const recordUnknownResolution = Effect.fn("DoSubmissionLedger.recordUnknownResolution")(function* (command) {
2744
- const operation = "ledger record unknown resolution";
2745
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(UnknownResolutionCommand))(command).pipe(Effect.mapError(internalFailure(operation)));
2746
- const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(Effect.mapError(internalFailure(operation)));
2747
- yield* hitFailpoint("ledger:unknown-resolution:before", operation);
2748
- const intent = yield* inWriteTransaction(operation, Effect.gen(function* () {
2749
- const submission = yield* requireSubmission(operation, validated.submissionId);
2750
- if (submission.state === "settled") {
2751
- if (submission.settled_outcome === null) return yield* corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, "A settled Submission carries no terminal outcome.");
2752
- return yield* SettlementConflict.make({
2753
- submissionId: validated.submissionId,
2754
- existingOutcome: submission.settled_outcome
2755
- });
2756
- }
2757
- const existing = (yield* readUnknownResolutions(operation, validated.submissionId)).find((row) => row.tool_call_id === validated.toolCallId);
2758
- if (existing !== void 0 && existing.resolution_json !== resolutionJson) return yield* UnknownResolutionConflict.make({
2759
- submissionId: validated.submissionId,
2760
- toolCallId: validated.toolCallId
2761
- });
2762
- let resolved;
2763
- if (existing !== void 0) resolved = yield* unknownResolutionIntentFromRow(operation, existing);
2764
- else {
2765
- const now = yield* currentInstant;
2766
- yield* sql`
2767
- INSERT INTO effect_agent_unknown_resolutions (
2768
- submission_id,
2769
- tool_call_id,
2770
- author,
2771
- reason,
2772
- resolution_json,
2773
- resolved_at
2774
- ) VALUES (
2775
- ${validated.submissionId},
2776
- ${validated.toolCallId},
2777
- ${validated.author},
2778
- ${validated.reason},
2779
- ${resolutionJson},
2780
- ${now.iso}
2781
- )
2782
- `.pipe(Effect.mapError(sqlFailure(operation)));
2783
- const resolution = yield* parseStoredJsonText(resolutionJson).pipe(Effect.mapError(internalFailure(operation)));
2784
- resolved = yield* decodeUnknownResolutionIntent({
2785
- submissionId: validated.submissionId,
2786
- toolCallId: validated.toolCallId,
2787
- author: validated.author,
2788
- reason: validated.reason,
2789
- resolution,
2790
- resolvedAt: now.iso
2791
- }).pipe(Effect.mapError(internalFailure(operation)));
2792
- }
2793
- if (submission.state === "unknown" && submission.unknown_tool_call_ids_json !== null) {
2794
- const markedIds = yield* storedUnknownToolCallIds(operation, submission);
2795
- const covering = yield* readUnknownResolutions(operation, validated.submissionId);
2796
- const coveredIds = new Set(covering.map((row) => row.tool_call_id));
2797
- if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) yield* sql`
2798
- UPDATE effect_agent_submissions
2799
- SET
2800
- state = 'input-applied',
2801
- unknown_reason = NULL,
2802
- unknown_tool_call_ids_json = NULL
2803
- WHERE submission_id = ${validated.submissionId}
2804
- `.pipe(Effect.mapError(sqlFailure(operation)));
2805
- }
2806
- return resolved;
2807
- }));
2808
- yield* hitFailpoint("ledger:unknown-resolution:after", operation);
2809
- return intent;
2810
- });
2811
- const recordChildSettled = Effect.fn("DoSubmissionLedger.recordChildSettled")(function* (request) {
2812
- const operation = "ledger record child settled";
2813
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildSettledNotification))(request).pipe(Effect.mapError(internalFailure(operation)));
2814
- yield* hitFailpoint("ledger:child-settled:before", operation);
2815
- const outcome = yield* inWriteTransaction(operation, Effect.gen(function* () {
2816
- const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2817
- const child = yield* readSubmission(operation, validated.childSubmissionId);
2818
- if (Option.isSome(child) && child.value.state !== "settled") return yield* LedgerError.make({
2819
- operation,
2820
- message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`
2821
- });
2822
- const now = yield* currentInstant;
2823
- yield* sql`
2824
- INSERT INTO effect_agent_child_settlements (
2825
- parent_submission_id,
2826
- child_submission_id,
2827
- child_outcome,
2828
- recorded_at
2829
- ) VALUES (
2830
- ${validated.parentSubmissionId},
2831
- ${validated.childSubmissionId},
2832
- ${Option.isSome(child) ? child.value.settled_outcome : null},
2833
- ${now.iso}
2834
- )
2835
- ON CONFLICT (parent_submission_id, child_submission_id) DO NOTHING
2836
- `.pipe(Effect.mapError(sqlFailure(operation)));
2837
- if (parent.state !== "suspended" || parent.suspended_reason_json === null) return "not-waiting";
2838
- 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)));
2839
- if (reason._tag !== "WaitingForChild") return "not-waiting";
2840
- if (!reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)) return "not-waiting";
2841
- const markers = yield* readChildSettlementMarkers(operation, validated.parentSubmissionId);
2842
- const markerChildren = new Set(markers.map((row) => row.child_submission_id));
2843
- for (const entry of reason.children) if (!(yield* childProvablySettled(operation, markerChildren, entry.childSubmissionId))) return "still-waiting";
2844
- yield* sql`
2845
- UPDATE effect_agent_submissions
2846
- SET
2847
- state = 'input-applied',
2848
- suspended_reason_json = NULL,
2849
- suspended_at = NULL
2850
- WHERE submission_id = ${validated.parentSubmissionId}
2851
- `.pipe(Effect.mapError(sqlFailure(operation)));
2852
- return "woken";
2853
- }));
2854
- yield* hitFailpoint("ledger:child-settled:after", operation);
2855
- return outcome;
2856
- });
2857
- const reserveChildBudget = Effect.fn("DoSubmissionLedger.reserveChildBudget")(function* (request) {
2858
- const operation = "ledger reserve child budget";
2859
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildBudgetReservationRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2860
- const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(Effect.mapError(internalFailure(operation)));
2861
- yield* hitFailpoint("ledger:child-reservation:before", operation);
2862
- const reserved = yield* inWriteTransaction(operation, Effect.gen(function* () {
2863
- const existing = yield* readChildReservation(operation, validated.reservationId);
2864
- if (Option.isSome(existing)) {
2865
- if (!(existing.value.parent_submission_id === validated.parentSubmissionId && existing.value.parent_tool_call_id === validated.parentToolCallId && existing.value.allocation_digest === validated.allocationDigest && existing.value.allocation_json === allocationJson)) return yield* ChildReservationConflict.make({
2866
- reservationId: validated.reservationId,
2867
- status: existing.value.status,
2868
- message: "A reservation with this identity exists with a different parent Tool Call or allocation."
2869
- });
2870
- return ReservedChildBudget.make({
2871
- reservation: yield* childReservationSnapshotFromRow(operation, existing.value),
2872
- replayed: true
2873
- });
2874
- }
2875
- const collision = yield* readChildReservationForCall(operation, validated.parentSubmissionId, validated.parentToolCallId);
2876
- if (Option.isSome(collision)) return yield* ChildReservationConflict.make({
2877
- reservationId: validated.reservationId,
2878
- status: collision.value.status,
2879
- message: `Parent Tool Call ${validated.parentToolCallId} already owns reservation ${collision.value.reservation_id}.`
2880
- });
2881
- const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2882
- yield* requireOwnership(operation, parent, validated.ownershipToken);
2883
- const now = yield* currentInstant;
2884
- yield* sql`
2885
- INSERT INTO effect_agent_child_reservations (
2886
- reservation_id,
2887
- parent_submission_id,
2888
- parent_tool_call_id,
2889
- status,
2890
- allocation_json,
2891
- allocation_digest,
2892
- reserved_at
2893
- ) VALUES (
2894
- ${validated.reservationId},
2895
- ${validated.parentSubmissionId},
2896
- ${validated.parentToolCallId},
2897
- 'reserved',
2898
- ${allocationJson},
2899
- ${validated.allocationDigest},
2900
- ${now.iso}
2901
- )
2902
- `.pipe(Effect.mapError(sqlFailure(operation)));
2903
- const inserted = yield* readChildReservation(operation, validated.reservationId);
2904
- 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.");
2905
- return ReservedChildBudget.make({
2906
- reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
2907
- replayed: false
2908
- });
2909
- }));
2910
- yield* hitFailpoint("ledger:child-reservation:after", operation);
2911
- return reserved;
2912
- });
2913
- const attachChildToReservation = Effect.fn("DoSubmissionLedger.attachChildToReservation")(function* (request) {
2914
- const operation = "ledger attach child to reservation";
2915
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AttachChildToReservationRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2916
- yield* hitFailpoint("ledger:child-attach:before", operation);
2917
- const attached = yield* inWriteTransaction(operation, Effect.gen(function* () {
2918
- const existing = yield* readChildReservation(operation, validated.reservationId);
2919
- if (Option.isNone(existing)) return yield* LedgerError.make({
2920
- operation,
2921
- message: `Unknown child reservation ${validated.reservationId}.`
2922
- });
2923
- if (existing.value.child_submission_id !== null) {
2924
- if (existing.value.child_submission_id === validated.childSubmissionId) return yield* childReservationSnapshotFromRow(operation, existing.value);
2925
- return yield* ChildReservationConflict.make({
2926
- reservationId: validated.reservationId,
2927
- status: existing.value.status,
2928
- message: `Reservation ${validated.reservationId} already records child ${existing.value.child_submission_id}.`
2929
- });
2930
- }
2931
- const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
2932
- yield* requireOwnership(operation, parent, validated.ownershipToken);
2933
- if (existing.value.status !== "reserved") return yield* ChildReservationConflict.make({
2934
- reservationId: validated.reservationId,
2935
- status: existing.value.status,
2936
- message: `Cannot attach a child to a ${existing.value.status} reservation.`
2937
- });
2938
- yield* sql`
2939
- UPDATE effect_agent_child_reservations
2940
- SET child_submission_id = ${validated.childSubmissionId}
2941
- WHERE reservation_id = ${validated.reservationId}
2942
- `.pipe(Effect.mapError(sqlFailure(operation)));
2943
- const updated = yield* readChildReservation(operation, validated.reservationId);
2944
- 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.");
2945
- return yield* childReservationSnapshotFromRow(operation, updated.value);
2946
- }));
2947
- yield* hitFailpoint("ledger:child-attach:after", operation);
2948
- return attached;
2949
- });
2950
- const beginChildBudgetRelease = Effect.fn("DoSubmissionLedger.beginChildBudgetRelease")(function* (request) {
2951
- const operation = "ledger begin child budget release";
2952
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(BeginChildBudgetReleaseRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2953
- const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(Effect.mapError(internalFailure(operation)));
2954
- yield* hitFailpoint("ledger:child-release-pending:before", operation);
2955
- const frozen = yield* inWriteTransaction(operation, Effect.gen(function* () {
2956
- const existing = yield* readChildReservation(operation, validated.reservationId);
2957
- if (Option.isNone(existing)) return yield* LedgerError.make({
2958
- operation,
2959
- message: `Unknown child reservation ${validated.reservationId}.`
2960
- });
2961
- if (existing.value.status !== "reserved") {
2962
- if (existing.value.accounting_json === accountingJson) return yield* childReservationSnapshotFromRow(operation, existing.value);
2963
- return yield* ChildReservationConflict.make({
2964
- reservationId: validated.reservationId,
2965
- status: existing.value.status,
2966
- message: "A different accounting decision is already frozen for this reservation."
2967
- });
2968
- }
2969
- const now = yield* currentInstant;
2970
- yield* sql`
2971
- UPDATE effect_agent_child_reservations
2972
- SET
2973
- status = 'releasePending',
2974
- accounting_json = ${accountingJson},
2975
- release_began_at = ${now.iso}
2976
- WHERE reservation_id = ${validated.reservationId}
2977
- `.pipe(Effect.mapError(sqlFailure(operation)));
2978
- const updated = yield* readChildReservation(operation, validated.reservationId);
2979
- 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.");
2980
- return yield* childReservationSnapshotFromRow(operation, updated.value);
2981
- }));
2982
- yield* hitFailpoint("ledger:child-release-pending:after", operation);
2983
- return frozen;
2984
- });
2985
- const releaseChildBudget = Effect.fn("DoSubmissionLedger.releaseChildBudget")(function* (request) {
2986
- const operation = "ledger release child budget";
2987
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseChildBudgetRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
2988
- yield* hitFailpoint("ledger:child-release:before", operation);
2989
- const released = yield* inWriteTransaction(operation, Effect.gen(function* () {
2990
- const existing = yield* readChildReservation(operation, validated.reservationId);
2991
- if (Option.isNone(existing)) return yield* LedgerError.make({
2992
- operation,
2993
- message: `Unknown child reservation ${validated.reservationId}.`
2994
- });
2995
- if (existing.value.status === "released") return yield* childReservationSnapshotFromRow(operation, existing.value);
2996
- if (existing.value.status !== "releasePending") return yield* ChildReservationConflict.make({
2997
- reservationId: validated.reservationId,
2998
- status: existing.value.status,
2999
- message: "Cannot release a reservation whose accounting decision is not frozen."
3000
- });
3001
- const now = yield* currentInstant;
3002
- yield* sql`
3003
- UPDATE effect_agent_child_reservations
3004
- SET status = 'released', released_at = ${now.iso}
3005
- WHERE reservation_id = ${validated.reservationId}
3006
- `.pipe(Effect.mapError(sqlFailure(operation)));
3007
- const updated = yield* readChildReservation(operation, validated.reservationId);
3008
- 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.");
3009
- return yield* childReservationSnapshotFromRow(operation, updated.value);
3010
- }));
3011
- yield* hitFailpoint("ledger:child-release:after", operation);
3012
- return released;
3013
- });
3014
- const scanPage = Effect.fn("DoSubmissionLedger.scanPage")(function* (cursor) {
3015
- const operation = "ledger scan nonterminal";
3016
- const rows = yield* (cursor === void 0 ? sql`
3017
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3018
- FROM effect_agent_submissions
3019
- WHERE state <> 'settled'
3020
- ORDER BY conversation_id ASC, queue_sequence ASC
3021
- LIMIT ${SCAN_PAGE_SIZE}
3022
- ` : sql`
3023
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3024
- FROM effect_agent_submissions
3025
- WHERE state <> 'settled'
3026
- AND (
3027
- conversation_id > ${cursor.conversationId}
3028
- OR (
3029
- conversation_id = ${cursor.conversationId}
3030
- AND queue_sequence > ${cursor.queueSequence}
3031
- )
3032
- )
3033
- ORDER BY conversation_id ASC, queue_sequence ASC
3034
- LIMIT ${SCAN_PAGE_SIZE}
3035
- `).pipe(Effect.mapError(sqlFailure(operation)));
3036
- const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
3037
- const snapshots = yield* Effect.forEach(decoded, (row) => decodeSubmissionSnapshot(operation, row));
3038
- const last = decoded[decoded.length - 1];
3039
- return [snapshots, last === void 0 || decoded.length < SCAN_PAGE_SIZE ? Option.none() : Option.some({
3040
- conversationId: last.conversation_id,
3041
- queueSequence: last.queue_sequence
3042
- })];
3043
- });
3044
- const scanNonterminal = Stream.paginate(void 0, scanPage);
3045
- const loadRecoverySnapshot = Effect.fn("DoSubmissionLedger.loadRecoverySnapshot")(function* (request) {
3046
- const operation = "ledger load recovery snapshot";
3047
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RecoverySnapshotRequest))(request).pipe(Effect.mapError(internalFailure(operation)));
3048
- return yield* sql.withTransaction(Effect.gen(function* () {
3049
- const submissionRow = yield* requireSubmission(operation, validated.submissionId);
3050
- const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
3051
- let ownership;
3052
- const ownershipRow = yield* readOwnership(operation, validated.submissionId);
3053
- if (Option.isSome(ownershipRow)) ownership = yield* decodeOwnershipSnapshot({
3054
- attemptId: ownershipRow.value.attempt_id,
3055
- ownerProducerId: ownershipRow.value.owner_producer_id,
3056
- producerEpoch: ownershipRow.value.producer_epoch,
3057
- leaseExpiresAt: ownershipRow.value.lease_expires_at
3058
- }).pipe(Effect.mapError(internalFailure(operation)));
3059
- let inputApplied;
3060
- if (submissionRow.input_applied_record_id !== null && submissionRow.input_applied_sequence !== null) inputApplied = yield* decodeInputAppliedMarker({
3061
- recordId: submissionRow.input_applied_record_id,
3062
- sequence: submissionRow.input_applied_sequence
3063
- }).pipe(Effect.mapError(internalFailure(operation)));
3064
- let reservation;
3065
- const reservationRow = yield* readReservation(operation, validated.submissionId);
3066
- if (Option.isSome(reservationRow)) {
3067
- const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, error.message)));
3068
- const settlementId = yield* Schema.decodeUnknownEffect(SettlementReservationSnapshot.fields.settlementId)(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
3069
- reservation = SettlementReservationSnapshot.make({
3070
- settlementId,
3071
- outcome: reservationRow.value.outcome,
3072
- record,
3073
- recordDigest: reservationRow.value.record_digest,
3074
- finalized: reservationRow.value.finalized_at !== null
3075
- });
3076
- }
3077
- let abortIntent;
3078
- const abortRow = yield* readAbortIntent(operation, validated.submissionId);
3079
- if (Option.isSome(abortRow)) abortIntent = yield* abortIntentFromRow(operation, submissionRow, validated.submissionId, abortRow.value);
3080
- const joinRows = yield* sql`
3081
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3082
- FROM effect_agent_submissions
3083
- WHERE joined_host_submission_id = ${validated.submissionId}
3084
- ORDER BY queue_sequence ASC
3085
- `.pipe(Effect.mapError(sqlFailure(operation)));
3086
- const joinSubmissions = yield* decodeSubmissionRows(operation, validated.submissionId, joinRows);
3087
- const joins = yield* Effect.forEach(joinSubmissions, (row) => decodeJoinSnapshot({
3088
- submissionId: row.submission_id,
3089
- state: row.state,
3090
- hostSubmissionId: validated.submissionId
3091
- }).pipe(Effect.mapError(internalFailure(operation))));
3092
- let hostSubmissionId;
3093
- if (submissionRow.joined_host_submission_id !== null) hostSubmissionId = yield* decodeSubmissionId(submissionRow.joined_host_submission_id).pipe(Effect.mapError(internalFailure(operation)));
3094
- let suspension;
3095
- if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
3096
- const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, error.message)));
3097
- suspension = yield* decodeSuspensionSnapshot({
3098
- reason,
3099
- suspendedAt: submissionRow.suspended_at
3100
- }).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", validated.submissionId, error.message)));
3101
- }
3102
- const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3103
- const approvalDecisions = yield* Effect.forEach(decisionRows, (row) => approvalIntentFromRow(operation, row));
3104
- const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3105
- const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) => unknownResolutionIntentFromRow(operation, row));
3106
- const childReservationRows = yield* sql`
3107
- SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
3108
- FROM effect_agent_child_reservations
3109
- WHERE parent_submission_id = ${validated.submissionId}
3110
- ORDER BY parent_tool_call_id ASC
3111
- `.pipe(Effect.mapError(sqlFailure(operation)));
3112
- const decodedChildReservations = yield* decodeChildReservationRows(operation, validated.submissionId, childReservationRows);
3113
- const childReservations = yield* Effect.forEach(decodedChildReservations, (row) => childReservationSnapshotFromRow(operation, row));
3114
- const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
3115
- const markersByChild = new Map(markers.map((row) => [row.child_submission_id, row]));
3116
- const childAttachments = [];
3117
- for (const row of decodedChildReservations) {
3118
- if (row.child_submission_id === null) continue;
3119
- const child = yield* readSubmission(operation, row.child_submission_id);
3120
- if (Option.isSome(child)) {
3121
- childAttachments.push(yield* decodeChildAttachmentSnapshot({
3122
- toolCallId: row.parent_tool_call_id,
3123
- childSubmissionId: row.child_submission_id,
3124
- childState: child.value.state,
3125
- ...child.value.settled_outcome === null ? {} : { childOutcome: child.value.settled_outcome }
3126
- }).pipe(Effect.mapError(internalFailure(operation))));
3127
- continue;
3128
- }
3129
- const marker = markersByChild.get(row.child_submission_id);
3130
- if (marker === void 0) continue;
3131
- childAttachments.push(yield* decodeChildAttachmentSnapshot({
3132
- toolCallId: row.parent_tool_call_id,
3133
- childSubmissionId: row.child_submission_id,
3134
- childState: "settled",
3135
- ...marker.child_outcome === null ? {} : { childOutcome: marker.child_outcome }
3136
- }).pipe(Effect.mapError(internalFailure(operation))));
3137
- }
3138
- let parentLinkage;
3139
- if (submissionRow.parent_submission_id !== null && submissionRow.parent_tool_call_id !== null) parentLinkage = yield* decodeParentLinkage({
3140
- parentSubmissionId: submissionRow.parent_submission_id,
3141
- parentToolCallId: submissionRow.parent_tool_call_id
3142
- }).pipe(Effect.mapError(internalFailure(operation)));
3143
- return RecoverySnapshot.make({
3144
- submission,
3145
- joins,
3146
- approvalDecisions,
3147
- unknownResolutions,
3148
- childReservations,
3149
- childAttachments,
3150
- ...parentLinkage === void 0 ? {} : { parentLinkage },
3151
- ...hostSubmissionId === void 0 ? {} : { hostSubmissionId },
3152
- ...suspension === void 0 ? {} : { suspension },
3153
- ...ownership === void 0 ? {} : { ownership },
3154
- ...inputApplied === void 0 ? {} : { inputApplied },
3155
- ...reservation === void 0 ? {} : { reservation },
3156
- ...abortIntent === void 0 ? {} : { abortIntent }
3157
- });
3158
- })).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(sqlFailure(operation)(error))));
3159
- });
3160
- return Context.make(SubmissionLedger, SubmissionLedger.of({
3161
- capabilities,
3162
- admit,
3163
- markReady,
3164
- lookup,
3165
- resolveAdmission,
3166
- claim,
3167
- renewOwnership,
3168
- releaseOwnership,
3169
- markInputApplied,
3170
- reserveSettlement,
3171
- finalizeSettlement,
3172
- requestAbort,
3173
- claimJoining,
3174
- markJoined,
3175
- revertJoining,
3176
- suspend,
3177
- recordApprovalDecision,
3178
- markUnknown,
3179
- recordUnknownResolution,
3180
- recordChildSettled,
3181
- reserveChildBudget,
3182
- attachChildToReservation,
3183
- beginChildBudgetRelease,
3184
- releaseChildBudget,
3185
- scanNonterminal,
3186
- loadRecoverySnapshot
3187
- }));
3188
- });
3189
- /**
3190
- * Durable Object SubmissionLedger implementation sharing the journal's private SQLite
3191
- * database, storage-backed transaction discipline, and producer-epoch fencing substrate.
3192
- * Configuration, failpoint, SQL, and Crypto authority stay visible in the input channel.
3193
- */
3194
- const submissionLedgerLayer = Layer.effectContext(makeServices());
3195
- /**
3196
- * A composition-root convenience Layer for the durable Submission Ledger. Point it at the
3197
- * same `ctx.storage` as the ConversationStore so claims fence the same producer epochs.
3198
- */
3199
- const ledgerLayer = (options) => submissionLedgerLayer.pipe(Layer.provide(Layer.mergeAll(storageConfigLayer(options), storageFailpointLayer(options), SqliteClient.layer({ storage: options.storage }), BrowserCrypto.layer)));
3200
- //#endregion
3201
- //#region src/port-protocol.ts
3202
- /**
3203
- * The cross-Durable-Object port protocol (plan §1.3, D-P6-3): Schema request/response/error
3204
- * envelopes for the CLOSED route-capable subset of the session ports. One Conversation's
3205
- * Durable Object executes another Conversation's request against its OWN local facets; the
3206
- * envelopes here are the only values that cross the Object boundary, and they are
3207
- * transport-agnostic — native Durable Object JS RPC is the shipped carrier, fetch-with-JSON
3208
- * the documented fallback, and both move the same Schema-encoded JSON.
3209
- *
3210
- * The closed subset is exactly the set of operations the durable coordinator performs against
3211
- * a FOREIGN Conversation (parent/child establishment, status checks, abort propagation,
3212
- * child-settlement notification, and the child-conversation store operations used by
3213
- * establishment, `verifySettledChild`, and result projection):
3214
- *
3215
- * - ledger: `admit`, `markReady`, `lookup`, `resolveAdmission`, `requestAbort`,
3216
- * `recordChildSettled`;
3217
- * - store: `materialize`, `append`, `read` (one page), `inspectTail`, `export`.
3218
- *
3219
- * Every other port operation is lane-local by construction and is NOT given an envelope:
3220
- * honesty over accidental distribution — the routing layer fails such calls fast and typed
3221
- * instead of quietly widening the distributed surface.
3222
- *
3223
- * Failures cross the boundary as the `PortFailure` union and re-decode on the caller side to
3224
- * the SAME tagged error types the local facet would have produced, so routed calls keep
3225
- * error-tag fidelity. `cause` chains inside `LedgerError`/`ConversationStoreError` travel as
3226
- * Schema defects and do not claim instance fidelity across Objects (plan §2.8).
3227
- */
3228
- /** Ceiling for protocol diagnostic strings; matches `AdmissionIndeterminate.reason`. */
3229
- const MAX_PORT_DIAGNOSTIC_LENGTH = 4096;
3230
- const BoundedDiagnostic = Schema.String.check(Schema.isMaxLength(MAX_PORT_DIAGNOSTIC_LENGTH));
3231
- /** Truncate a diagnostic string to the protocol's bounded diagnostic length. */
3232
- const boundPortDiagnostic = (value) => value.length > 4096 ? `${value.slice(0, MAX_PORT_DIAGNOSTIC_LENGTH - 3)}...` : value;
3233
- /**
3234
- * The envelope itself could not be honored: the receiving Object could not decode the
3235
- * request, or a response could not be encoded/decoded. It never carries port semantics —
3236
- * callers fold it into the operation's base error (`LedgerError`/`ConversationStoreError`),
3237
- * except `resolveAdmission`, which folds it into `AdmissionIndeterminate` because a
3238
- * non-answer is never proof of absence (SUB-031).
3239
- */
3240
- var PortProtocolError = class extends Schema.TaggedError()("PortProtocolError", { message: BoundedDiagnostic }) {};
3241
- /** Routed `SubmissionLedger.admit` — child establishment admits INTO the owning Object. */
3242
- var LedgerAdmitCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerAdmitCall")("LedgerAdmit", { request: AdmissionRequest }) {};
3243
- /** Routed `SubmissionLedger.markReady` for a Submission owned by another Object. */
3244
- var LedgerMarkReadyCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerMarkReadyCall")("LedgerMarkReady", { request: MarkReadyRequest }) {};
3245
- /** Routed `SubmissionLedger.lookup` (by identity or scoped idempotency key). */
3246
- var LedgerLookupCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerLookupCall")("LedgerLookup", { request: SubmissionLookup }) {};
3247
- /** Routed `SubmissionLedger.resolveAdmission` — the SUB-031 tri-state authority call. */
3248
- var LedgerResolveAdmissionCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerResolveAdmissionCall")("LedgerResolveAdmission", { request: SubmissionLookupByKey }) {};
3249
- /** Routed `SubmissionLedger.requestAbort` — abort propagation across Objects. */
3250
- var LedgerRequestAbortCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerRequestAbortCall")("LedgerRequestAbort", { request: AbortCommand }) {};
3251
- /** Routed `SubmissionLedger.recordChildSettled` — the child→parent durable notification. */
3252
- var LedgerRecordChildSettledCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerRecordChildSettledCall")("LedgerRecordChildSettled", { request: ChildSettledNotification }) {};
3253
- /** Routed `ConversationStore.materialize` against the owning Object. */
3254
- var StoreMaterializeCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreMaterializeCall")("StoreMaterialize", { request: ConversationMaterialization }) {};
3255
- /** Routed `ConversationStore.append` against the owning Object. */
3256
- var StoreAppendCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreAppendCall")("StoreAppend", { request: FencedAppendRequest }) {};
3257
- /** Routed one-page `ConversationStore.read`; the page bound is the request's own `limit`. */
3258
- var StoreReadPageCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreReadPageCall")("StoreReadPage", { request: ConversationRead }) {};
3259
- /** Routed `ConversationStore.inspectTail` against the owning Object. */
3260
- var StoreInspectTailCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailCall")("StoreInspectTail", { request: ConversationTailRequest }) {};
3261
- /** Routed `ConversationStore.export` against the owning Object. */
3262
- var StoreExportCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportCall")("StoreExport", { request: ConversationExportRequest }) {};
3263
- /** Every request that may cross a Durable Object boundary — the CLOSED route-capable subset. */
3264
- const PortRequest = Schema.Union([
3265
- LedgerAdmitCall,
3266
- LedgerMarkReadyCall,
3267
- LedgerLookupCall,
3268
- LedgerResolveAdmissionCall,
3269
- LedgerRequestAbortCall,
3270
- LedgerRecordChildSettledCall,
3271
- StoreMaterializeCall,
3272
- StoreAppendCall,
3273
- StoreReadPageCall,
3274
- StoreInspectTailCall,
3275
- StoreExportCall
3276
- ]);
3277
- var LedgerAdmitResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerAdmitResult")("LedgerAdmitResult", { result: AdmissionResult }) {};
3278
- var LedgerMarkReadyResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerMarkReadyResult")("LedgerMarkReadyResult", {}) {};
3279
- /** `submission` is absent exactly when the lookup answered `Option.none`. */
3280
- var LedgerLookupResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerLookupResult")("LedgerLookupResult", { submission: Schema.optionalKey(SubmissionSnapshot) }) {};
3281
- var LedgerResolveAdmissionResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerResolveAdmissionResult")("LedgerResolveAdmissionResult", { resolution: AdmissionResolution }) {};
3282
- var LedgerRequestAbortResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerRequestAbortResult")("LedgerRequestAbortResult", { intent: AbortIntent }) {};
3283
- var LedgerRecordChildSettledResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerRecordChildSettledResult")("LedgerRecordChildSettledResult", { outcome: ChildSettledOutcome }) {};
3284
- var StoreMaterializeResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreMaterializeResult")("StoreMaterializeResult", {}) {};
3285
- var StoreAppendResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreAppendResult")("StoreAppendResult", { result: AppendResult }) {};
3286
- /** One page of canonical records, bounded by the request's `limit` (≤ 1,024). */
3287
- var StoreReadPageResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreReadPageResult")("StoreReadPageResult", { records: Schema.Array(CanonicalRecordEnvelope).check(Schema.isMaxLength(1024)) }) {};
3288
- var StoreInspectTailResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailResult")("StoreInspectTailResult", { tail: ConversationTail }) {};
3289
- var StoreExportResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportResult")("StoreExportResult", { export: ConversationExport }) {};
3290
- /** Every successful routed result. Callers narrow by the tag their request implies. */
3291
- const PortResult = Schema.Union([
3292
- LedgerAdmitResult,
3293
- LedgerMarkReadyResult,
3294
- LedgerLookupResult,
3295
- LedgerResolveAdmissionResult,
3296
- LedgerRequestAbortResult,
3297
- LedgerRecordChildSettledResult,
3298
- StoreMaterializeResult,
3299
- StoreAppendResult,
3300
- StoreReadPageResult,
3301
- StoreInspectTailResult,
3302
- StoreExportResult
3303
- ]);
3304
- /**
3305
- * Every typed failure a route-capable operation can produce on its owning Object, plus the
3306
- * protocol's own `PortProtocolError`. Members re-decode to the SAME tagged classes the
3307
- * session ports declare, so a routed caller observes identical error tags and fields.
3308
- */
3309
- const PortFailure = Schema.Union([
3310
- AdmissionConflict,
3311
- SettlementConflict,
3312
- JoinedToHost,
3313
- LedgerError,
3314
- ConversationStoreError,
3315
- ConversationNotMaterialized,
3316
- AppendConflict,
3317
- FenceRejected,
3318
- PortProtocolError
3319
- ]);
3320
- /** The routed operation succeeded on its owning Object. */
3321
- var PortSucceeded = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/PortSucceeded")("PortSucceeded", { result: PortResult }) {};
3322
- /** The routed operation failed TYPED on its owning Object; the failure re-decodes verbatim. */
3323
- var PortFailed = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/PortFailed")("PortFailed", { failure: PortFailure }) {};
3324
- /** The uniform answer of one `portCall`: op-specific success or a re-decodable typed failure. */
3325
- const PortResponse = Schema.Union([PortSucceeded, PortFailed]);
3326
- const encodePortRequest = Schema.encodeEffect(PortRequest);
3327
- const decodePortRequest = Schema.decodeUnknownEffect(PortRequest);
3328
- const encodePortResponse = Schema.encodeEffect(PortResponse);
3329
- const decodePortResponse = Schema.decodeUnknownEffect(PortResponse);
3330
- //#endregion
3331
- //#region src/routing.ts
3332
- const ConversationIdSchema = ConversationMaterialization.fields.conversationId;
3333
- const decodeConversationId = Schema.decodeUnknownEffect(ConversationIdSchema);
3334
- /**
3335
- * The ledger row bound routable Submission identities must respect (mirrors the local
3336
- * facet's `MAX_IDENTIFIER_LENGTH`; the minting side already refuses longer identities typed
3337
- * at admission, so a longer identity presented here cannot name any stored row).
3338
- */
3339
- const MAX_ROUTABLE_SUBMISSION_ID_LENGTH = 1024;
3340
- /** The `{uuidv7}` head of a DC-minted routable Submission identity. */
3341
- const UUID_HEAD_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
3342
- /**
3343
- * A transport could not deliver a port request to (or an answer from) the owning
3344
- * Conversation's Durable Object. `retryable` carries the platform's own stub signal when one
3345
- * exists. This error never crosses the wire — it is the CALLER-side evidence that the
3346
- * authority was unreachable, which is exactly the case `AdmissionIndeterminate` was
3347
- * specified for (SUB-031).
3348
- */
3349
- var PortTransportError = class extends Schema.TaggedError()("PortTransportError", {
3350
- target: Schema.String,
3351
- message: Schema.String,
3352
- retryable: Schema.optionalKey(Schema.Boolean),
3353
- cause: Schema.optionalKey(Schema.Defect())
3354
- }) {};
3355
- /**
3356
- * Build a `PortTransportError` from an arbitrary thrown transport cause, preserving the
3357
- * platform stub's own `retryable` signal when present.
3358
- */
3359
- const portTransportFailure = (target, cause) => {
3360
- const message = cause instanceof Error ? cause.message : String(cause);
3361
- let retryable;
3362
- if (typeof cause === "object" && cause !== null && "retryable" in cause) {
3363
- const signal = cause.retryable;
3364
- if (typeof signal === "boolean") retryable = signal;
3365
- }
3366
- return PortTransportError.make({
3367
- target,
3368
- message: boundPortDiagnostic(message),
3369
- ...retryable === void 0 ? {} : { retryable },
3370
- cause
3371
- });
3372
- };
3373
- /**
3374
- * Delivery of Schema-encoded port envelopes to the Durable Object that owns a FOREIGN
3375
- * Conversation (plan §1.3, D-P6-3). The shipped implementation (platform-cloudflare, WP3)
3376
- * calls the owner's `portCall` over native Durable Object JS RPC via
3377
- * `namespace.idFromName(conversationId)`; the protocol is transport-agnostic and any carrier
3378
- * that moves the encoded envelopes verbatim satisfies this service. Implementations MUST
3379
- * surface every delivery problem as `PortTransportError` and must never fabricate an answer.
3380
- */
3381
- var ConversationPortTransport = class extends Context.Service()("@effect-agent/storage-cloudflare/ConversationPortTransport") {};
3382
- const LOCAL = { _tag: "local" };
3383
- /**
3384
- * Parse a DC-minted routable Submission identity — `{uuidv7}:{conversationId}`, split at the
3385
- * FIRST `:` because the Conversation tail may itself contain colons (D-P6-5). This adapter
3386
- * minted the format at admission and is the ONLY component that parses it; identities that do
3387
- * not carry the minted shape (no separator, non-UUID head, empty tail) fall back to the local
3388
- * facet, which is the only authority this Object can consult without inventing an owner.
3389
- * Identities beyond the ledger's 1,024-character row bound fail typed: the minting side
3390
- * refused them at admission, so they cannot name any stored row anywhere.
3391
- */
3392
- const routableSubmissionTarget = (localConversationId) => Effect.fn("DoPortRouting.routableSubmissionTarget")(function* (operation, submissionId) {
3393
- if (submissionId.length > MAX_ROUTABLE_SUBMISSION_ID_LENGTH) return yield* LedgerError.make({
3394
- operation,
3395
- message: `A Submission identity of ${submissionId.length} characters exceeds the ${MAX_ROUTABLE_SUBMISSION_ID_LENGTH}-character routable identity bound; admission refuses such identities, so it cannot name any stored row.`
3396
- });
3397
- const separator = submissionId.indexOf(":");
3398
- if (separator === -1) return LOCAL;
3399
- if (!UUID_HEAD_PATTERN.test(submissionId.slice(0, separator))) return LOCAL;
3400
- const tail = submissionId.slice(separator + 1);
3401
- if (tail === localConversationId) return LOCAL;
3402
- return yield* decodeConversationId(tail).pipe(Effect.map((conversationId) => ({
3403
- _tag: "foreign",
3404
- conversationId
3405
- })), Effect.orElseSucceed(() => LOCAL));
3406
- });
3407
- const isResultTag = (tag) => (result) => result._tag === tag;
3408
- const isAdmitConflict = (failure) => failure._tag === "AdmissionConflict";
3409
- const isAbortConflict = (failure) => failure._tag === "SettlementConflict" || failure._tag === "JoinedToHost";
3410
- const noExtraFailure = (_failure) => false;
3411
- const isFenceRejected = (failure) => failure._tag === "FenceRejected";
3412
- const isAppendFailure = (failure) => failure._tag === "ConversationNotMaterialized" || failure._tag === "AppendConflict" || failure._tag === "FenceRejected";
3413
- const isNotMaterialized = (failure) => failure._tag === "ConversationNotMaterialized";
3414
- /**
3415
- * The fail-fast refusal for any foreign operation OUTSIDE the closed route-capable subset
3416
- * (plan §1.3): honesty over accidental distribution.
3417
- */
3418
- const crossConversationLedgerError = (operation, target) => LedgerError.make({
3419
- operation,
3420
- message: `${operation} addressed to foreign Conversation ${target} is not route-capable; the closed cross-Object subset is admit, markReady, lookup, resolveAdmission, requestAbort, and recordChildSettled. Every other ledger operation is lane-local by construction and must execute inside the owning Conversation's Durable Object.`
3421
- });
3422
- const crossConversationStoreError = (operation, target) => ConversationStoreError.make({
3423
- operation,
3424
- message: `${operation} addressed to foreign Conversation ${target} is not route-capable; the closed cross-Object subset is materialize, append, read (paged), inspectTail, and export. Observation and checkpoints are lane-local by construction and must execute inside the owning Conversation's Durable Object.`
3425
- });
3426
- const makeTransportCall = (transport) => Effect.fn("DoPortRouting.transportCall")(function* (target, call) {
3427
- const encoded = yield* encodePortRequest(call).pipe(Effect.mapError((error) => PortProtocolError.make({ message: boundPortDiagnostic(`The port request could not be encoded: ${error.message}`) })));
3428
- return yield* decodePortResponse(yield* transport.call(target, encoded)).pipe(Effect.mapError((error) => PortProtocolError.make({ message: boundPortDiagnostic(`The port response could not be decoded: ${error.message}`) })));
3429
- });
3430
- const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServices")(function* (options) {
3431
- const local = yield* SubmissionLedger;
3432
- const transport = yield* ConversationPortTransport;
3433
- const transportCall = makeTransportCall(transport);
3434
- const submissionTarget = routableSubmissionTarget(options.localConversationId);
3435
- const routeFailure = (operation, target) => (error) => LedgerError.make({
3436
- operation,
3437
- message: boundPortDiagnostic(`Routed ${operation} to the Conversation Object owning ${target} failed: ${error.message}`),
3438
- cause: error
3439
- });
3440
- /**
3441
- * One routed ledger call: encode, deliver, decode, then narrow the uniform envelope to the
3442
- * operation's own result and failure surface. A foreign `LedgerError` is always in-channel;
3443
- * any failure outside the operation's declared surface — including protocol anomalies — is
3444
- * folded into a `LedgerError` naming the anomaly instead of being erased or re-thrown raw.
3445
- */
3446
- const foreignLedgerCall = (operation, target, call, resultTag, isExpectedFailure) => transportCall(target, call).pipe(Effect.mapError(routeFailure(operation, target)), Effect.flatMap((response) => {
3447
- if (response._tag === "PortFailed") {
3448
- const failure = response.failure;
3449
- if (isExpectedFailure(failure)) return Effect.fail(failure);
3450
- if (failure._tag === "LedgerError") return Effect.fail(failure);
3451
- return Effect.fail(LedgerError.make({
3452
- operation,
3453
- message: boundPortDiagnostic(`The Conversation Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
3454
- cause: failure
3455
- }));
3456
- }
3457
- const result = response.result;
3458
- if (!isResultTag(resultTag)(result)) return Effect.fail(LedgerError.make({
3459
- operation,
3460
- message: `The Conversation Object owning ${target} answered ${operation} with the mismatched result ${result._tag}; expected ${resultTag}.`
3461
- }));
3462
- return Effect.succeed(result);
3463
- }), Effect.withSpan("DoPortRouting.foreignLedgerCall", { attributes: {
3464
- operation,
3465
- target
3466
- } }));
3467
- /**
3468
- * Routed `resolveAdmission` — where the S2 tri-state becomes real (plan §1.3): when the
3469
- * owning Object cannot be reached, or its answer cannot be understood, the routed adapter
3470
- * answers `AdmissionIndeterminate{reason}` and NEVER `NotAdmitted` — an unreachable
3471
- * authority proves nothing, and only `NotAdmitted` permits an admission attempt (SUB-031).
3472
- * A typed `LedgerError` answered BY the authority still fails typed: the authority was
3473
- * reached and reported its own storage failure.
3474
- */
3475
- const resolveForeignAdmission = (target, request) => transportCall(target, LedgerResolveAdmissionCall.make({ request })).pipe(Effect.flatMap((response) => {
3476
- if (response._tag === "PortFailed") {
3477
- if (response.failure._tag === "LedgerError") return Effect.fail(response.failure);
3478
- return Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Conversation Object owning ${target} answered resolveAdmission with the out-of-contract failure ${response.failure._tag}: ${response.failure.message}`) }));
3479
- }
3480
- if (response.result._tag !== "LedgerResolveAdmissionResult") return Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Conversation Object owning ${target} answered resolveAdmission with the mismatched result ${response.result._tag}.`) }));
3481
- return Effect.succeed(response.result.resolution);
3482
- }), Effect.catchTags({
3483
- PortTransportError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Conversation Object owning ${target} is unreachable: ${error.message}`) })),
3484
- PortProtocolError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The answer of the Conversation Object owning ${target} could not be understood: ${error.message}`) }))
3485
- }), Effect.withSpan("DoPortRouting.resolveForeignAdmission", { attributes: { target } }));
3486
- const foreignLookupById = (operation, target, submissionId) => foreignLedgerCall(operation, target, LedgerLookupCall.make({ request: SubmissionLookupById.make({ submissionId }) }), "LedgerLookupResult", noExtraFailure).pipe(Effect.map((result) => result.submission === void 0 ? Option.none() : Option.some(result.submission)));
3487
- /**
3488
- * Enrich a LOCAL parent's recovery snapshot with the lane state of attached children whose
3489
- * rows live in other Durable Objects (plan §1.3): markers first (the local facet already
3490
- * consulted them), then a routed per-child `lookup` for any attached child that is neither
3491
- * local nor marker-settled. A transport failure surfaces as `LedgerError` so the alarm
3492
- * pass retries; the child's canonical Settlement remains the only authority (DUR-015).
3493
- */
3494
- const enrichChildAttachments = Effect.fn("DoPortRouting.enrichChildAttachments")(function* (snapshot) {
3495
- const operation = "ledger load recovery snapshot";
3496
- const attachments = new Map(snapshot.childAttachments.map((attachment) => [attachment.childSubmissionId, attachment]));
3497
- let enriched = false;
3498
- for (const reservation of snapshot.childReservations) {
3499
- const childSubmissionId = reservation.childSubmissionId;
3500
- if (childSubmissionId === void 0 || attachments.has(childSubmissionId)) continue;
3501
- const target = yield* submissionTarget(operation, childSubmissionId);
3502
- if (target._tag !== "foreign") continue;
3503
- const child = yield* foreignLookupById(operation, target.conversationId, childSubmissionId);
3504
- if (Option.isNone(child)) continue;
3505
- attachments.set(childSubmissionId, ChildAttachmentSnapshot.make({
3506
- toolCallId: reservation.parentToolCallId,
3507
- childSubmissionId,
3508
- childState: child.value.state,
3509
- ...child.value.settledOutcome === void 0 ? {} : { childOutcome: child.value.settledOutcome }
3510
- }));
3511
- enriched = true;
3512
- }
3513
- if (!enriched) return snapshot;
3514
- const ordered = [];
3515
- for (const reservation of snapshot.childReservations) {
3516
- if (reservation.childSubmissionId === void 0) continue;
3517
- const attachment = attachments.get(reservation.childSubmissionId);
3518
- if (attachment !== void 0) ordered.push(attachment);
3519
- }
3520
- return RecoverySnapshot.make({
3521
- ...snapshot,
3522
- childAttachments: ordered
3523
- });
3524
- });
3525
- const routed = SubmissionLedger.of({
3526
- capabilities: local.capabilities,
3527
- admit: (request) => request.conversationId === options.localConversationId ? local.admit(request) : foreignLedgerCall("ledger admit", request.conversationId, LedgerAdmitCall.make({ request }), "LedgerAdmitResult", isAdmitConflict).pipe(Effect.map((reply) => reply.result)),
3528
- markReady: (request) => submissionTarget("ledger mark ready", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markReady(request) : foreignLedgerCall("ledger mark ready", target.conversationId, LedgerMarkReadyCall.make({ request }), "LedgerMarkReadyResult", noExtraFailure).pipe(Effect.asVoid))),
3529
- lookup: (request) => request._tag === "SubmissionLookupById" ? submissionTarget("ledger lookup", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.lookup(request) : foreignLookupById("ledger lookup", target.conversationId, request.submissionId))) : request.conversationId === options.localConversationId ? local.lookup(request) : foreignLedgerCall("ledger lookup", request.conversationId, LedgerLookupCall.make({ request }), "LedgerLookupResult", noExtraFailure).pipe(Effect.map((result) => result.submission === void 0 ? Option.none() : Option.some(result.submission))),
3530
- resolveAdmission: (request) => request.conversationId === options.localConversationId ? local.resolveAdmission(request) : resolveForeignAdmission(request.conversationId, request),
3531
- requestAbort: (request) => submissionTarget("ledger request abort", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.requestAbort(request) : foreignLedgerCall("ledger request abort", target.conversationId, LedgerRequestAbortCall.make({ request }), "LedgerRequestAbortResult", isAbortConflict).pipe(Effect.map((reply) => reply.intent)))),
3532
- recordChildSettled: (request) => submissionTarget("ledger record child settled", request.parentSubmissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordChildSettled(request) : foreignLedgerCall("ledger record child settled", target.conversationId, LedgerRecordChildSettledCall.make({ request }), "LedgerRecordChildSettledResult", noExtraFailure).pipe(Effect.map((reply) => reply.outcome)))),
3533
- claim: (request) => request.conversationId === options.localConversationId ? local.claim(request) : Effect.fail(crossConversationLedgerError("ledger claim", request.conversationId)),
3534
- claimJoining: (request) => request.conversationId === options.localConversationId ? local.claimJoining(request) : Effect.fail(crossConversationLedgerError("ledger claim joining", request.conversationId)),
3535
- renewOwnership: (request) => submissionTarget("ledger renew ownership", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.renewOwnership(request) : Effect.fail(crossConversationLedgerError("ledger renew ownership", target.conversationId)))),
3536
- releaseOwnership: (request) => submissionTarget("ledger release ownership", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.releaseOwnership(request) : Effect.fail(crossConversationLedgerError("ledger release ownership", target.conversationId)))),
3537
- markInputApplied: (request) => submissionTarget("ledger mark input applied", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markInputApplied(request) : Effect.fail(crossConversationLedgerError("ledger mark input applied", target.conversationId)))),
3538
- reserveSettlement: (request) => submissionTarget("ledger reserve settlement", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.reserveSettlement(request) : Effect.fail(crossConversationLedgerError("ledger reserve settlement", target.conversationId)))),
3539
- finalizeSettlement: (request) => submissionTarget("ledger finalize settlement", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.finalizeSettlement(request) : Effect.fail(crossConversationLedgerError("ledger finalize settlement", target.conversationId)))),
3540
- markJoined: (request) => submissionTarget("ledger mark joined", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markJoined(request) : Effect.fail(crossConversationLedgerError("ledger mark joined", target.conversationId)))),
3541
- revertJoining: (request) => submissionTarget("ledger revert joining", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.revertJoining(request) : Effect.fail(crossConversationLedgerError("ledger revert joining", target.conversationId)))),
3542
- suspend: (request) => submissionTarget("ledger suspend", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.suspend(request) : Effect.fail(crossConversationLedgerError("ledger suspend", target.conversationId)))),
3543
- recordApprovalDecision: (command) => submissionTarget("ledger record approval decision", command.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordApprovalDecision(command) : Effect.fail(crossConversationLedgerError("ledger record approval decision", target.conversationId)))),
3544
- markUnknown: (request) => submissionTarget("ledger mark unknown", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markUnknown(request) : Effect.fail(crossConversationLedgerError("ledger mark unknown", target.conversationId)))),
3545
- recordUnknownResolution: (command) => submissionTarget("ledger record unknown resolution", command.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordUnknownResolution(command) : Effect.fail(crossConversationLedgerError("ledger record unknown resolution", target.conversationId)))),
3546
- reserveChildBudget: (request) => submissionTarget("ledger reserve child budget", request.parentSubmissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.reserveChildBudget(request) : Effect.fail(crossConversationLedgerError("ledger reserve child budget", target.conversationId)))),
3547
- attachChildToReservation: local.attachChildToReservation,
3548
- beginChildBudgetRelease: local.beginChildBudgetRelease,
3549
- releaseChildBudget: local.releaseChildBudget,
3550
- scanNonterminal: local.scanNonterminal,
3551
- loadRecoverySnapshot: (request) => submissionTarget("ledger load recovery snapshot", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.loadRecoverySnapshot(request).pipe(Effect.flatMap(enrichChildAttachments)) : Effect.fail(crossConversationLedgerError("ledger load recovery snapshot", target.conversationId))))
3552
- });
3553
- return Context.make(SubmissionLedger, routed);
3554
- });
3555
- const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices")(function* (options) {
3556
- const local = yield* ConversationStore;
3557
- const transport = yield* ConversationPortTransport;
3558
- const transportCall = makeTransportCall(transport);
3559
- const routeFailure = (operation, target) => (error) => ConversationStoreError.make({
3560
- operation,
3561
- message: boundPortDiagnostic(`Routed ${operation} to the Conversation Object owning ${target} failed: ${error.message}`),
3562
- cause: error
3563
- });
3564
- /** The store twin of `foreignLedgerCall` with `ConversationStoreError` as the base error. */
3565
- const foreignStoreCall = (operation, target, call, resultTag, isExpectedFailure) => transportCall(target, call).pipe(Effect.mapError(routeFailure(operation, target)), Effect.flatMap((response) => {
3566
- if (response._tag === "PortFailed") {
3567
- const failure = response.failure;
3568
- if (isExpectedFailure(failure)) return Effect.fail(failure);
3569
- if (failure._tag === "ConversationStoreError") return Effect.fail(failure);
3570
- return Effect.fail(ConversationStoreError.make({
3571
- operation,
3572
- message: boundPortDiagnostic(`The Conversation Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
3573
- cause: failure
3574
- }));
3575
- }
3576
- const result = response.result;
3577
- if (!isResultTag(resultTag)(result)) return Effect.fail(ConversationStoreError.make({
3578
- operation,
3579
- message: `The Conversation Object owning ${target} answered ${operation} with the mismatched result ${result._tag}; expected ${resultTag}.`
3580
- }));
3581
- return Effect.succeed(result);
3582
- }), Effect.withSpan("DoPortRouting.foreignStoreCall", { attributes: {
3583
- operation,
3584
- target
3585
- } }));
3586
- const routed = ConversationStore.of({
3587
- materialize: (request) => request.conversationId === options.localConversationId ? local.materialize(request) : foreignStoreCall("conversation materialize", request.conversationId, StoreMaterializeCall.make({ request }), "StoreMaterializeResult", isFenceRejected).pipe(Effect.asVoid),
3588
- append: (request) => request.conversationId === options.localConversationId ? local.append(request) : foreignStoreCall("conversation append", request.conversationId, StoreAppendCall.make({ request }), "StoreAppendResult", isAppendFailure).pipe(Effect.map((reply) => reply.result)),
3589
- read: (request) => request.conversationId === options.localConversationId ? local.read(request) : Stream.unwrap(foreignStoreCall("conversation read", request.conversationId, StoreReadPageCall.make({ request }), "StoreReadPageResult", isNotMaterialized).pipe(Effect.map((reply) => Stream.fromIterable(reply.records)))),
3590
- inspectTail: (request) => request.conversationId === options.localConversationId ? local.inspectTail(request) : foreignStoreCall("conversation inspect tail", request.conversationId, StoreInspectTailCall.make({ request }), "StoreInspectTailResult", isNotMaterialized).pipe(Effect.map((reply) => reply.tail)),
3591
- export: (request) => request.conversationId === options.localConversationId ? local.export(request) : foreignStoreCall("conversation export", request.conversationId, StoreExportCall.make({ request }), "StoreExportResult", isNotMaterialized).pipe(Effect.map((reply) => reply.export)),
3592
- observe: (request) => request.conversationId === options.localConversationId ? local.observe(request) : Stream.unwrap(Effect.fail(crossConversationStoreError("conversation observe", request.conversationId))),
3593
- saveCheckpoint: (request) => request.checkpoint.conversationId === options.localConversationId ? local.saveCheckpoint(request) : Effect.fail(crossConversationStoreError("conversation save checkpoint", request.checkpoint.conversationId)),
3594
- loadCheckpoint: (request) => request.conversationId === options.localConversationId ? local.loadCheckpoint(request) : Effect.fail(crossConversationStoreError("conversation load checkpoint", request.conversationId))
3595
- });
3596
- return Context.make(ConversationStore, routed);
3597
- });
3598
- /**
3599
- * Routing decorator over the LOCAL `SubmissionLedger` facet (plan §1.3): a request addressing
3600
- * this Object's Conversation executes locally; a route-capable request addressing another
3601
- * Conversation is Schema-encoded onto the `ConversationPortTransport` and executed by the
3602
- * owning Object's local facet; any other foreign request fails fast typed. Provide the WP1
3603
- * local facet (`submissionLedgerLayer`/`ledgerLayer`) and a transport to close it.
3604
- */
3605
- const routedSubmissionLedgerLayer = (options) => Layer.effectContext(makeRoutedLedgerServices(options));
3606
- /**
3607
- * Routing decorator over the LOCAL `ConversationStore` facet (plan §1.3): this-conversation
3608
- * requests execute locally; foreign materialize/append/read/inspectTail/export travel the
3609
- * transport; foreign observation and checkpoints fail fast typed.
3610
- */
3611
- const routedConversationStoreLayer = (options) => Layer.effectContext(makeRoutedStoreServices(options));
3612
- /** Fold one port operation's typed failures into the uniform response envelope. */
3613
- const capture = (effect) => effect.pipe(Effect.map((result) => PortSucceeded.make({ result })), Effect.catch((failure) => Effect.succeed(PortFailed.make({ failure }))));
3614
- /**
3615
- * Execute one decoded port request against THIS Object's LOCAL facets — the owner-side half
3616
- * of the routed ports (plan §1.3). Callers must provide the WP1 local facets, never the
3617
- * routed decorators: the routing layer already established that this Object owns the
3618
- * addressed Conversation, and re-routing here could bounce a request between Objects.
3619
- * Failures never escape — every typed port failure becomes a `PortFailed` envelope that
3620
- * re-decodes on the caller side.
3621
- */
3622
- const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(function* (request) {
3623
- switch (request._tag) {
3624
- case "LedgerAdmit": {
3625
- const ledger = yield* SubmissionLedger;
3626
- return yield* capture(ledger.admit(request.request).pipe(Effect.map((result) => LedgerAdmitResult.make({ result }))));
3627
- }
3628
- case "LedgerMarkReady": {
3629
- const ledger = yield* SubmissionLedger;
3630
- return yield* capture(ledger.markReady(request.request).pipe(Effect.map(() => LedgerMarkReadyResult.make({}))));
3631
- }
3632
- case "LedgerLookup": {
3633
- const ledger = yield* SubmissionLedger;
3634
- return yield* capture(ledger.lookup(request.request).pipe(Effect.map((submission) => Option.isSome(submission) ? LedgerLookupResult.make({ submission: submission.value }) : LedgerLookupResult.make({}))));
3635
- }
3636
- case "LedgerResolveAdmission": {
3637
- const ledger = yield* SubmissionLedger;
3638
- return yield* capture(ledger.resolveAdmission(request.request).pipe(Effect.map((resolution) => LedgerResolveAdmissionResult.make({ resolution }))));
3639
- }
3640
- case "LedgerRequestAbort": {
3641
- const ledger = yield* SubmissionLedger;
3642
- return yield* capture(ledger.requestAbort(request.request).pipe(Effect.map((intent) => LedgerRequestAbortResult.make({ intent }))));
3643
- }
3644
- case "LedgerRecordChildSettled": {
3645
- const ledger = yield* SubmissionLedger;
3646
- return yield* capture(ledger.recordChildSettled(request.request).pipe(Effect.map((outcome) => LedgerRecordChildSettledResult.make({ outcome }))));
3647
- }
3648
- case "StoreMaterialize": {
3649
- const store = yield* ConversationStore;
3650
- return yield* capture(store.materialize(request.request).pipe(Effect.map(() => StoreMaterializeResult.make({}))));
3651
- }
3652
- case "StoreAppend": {
3653
- const store = yield* ConversationStore;
3654
- return yield* capture(store.append(request.request).pipe(Effect.map((result) => StoreAppendResult.make({ result }))));
3655
- }
3656
- case "StoreReadPage": {
3657
- const store = yield* ConversationStore;
3658
- return yield* capture(store.read(request.request).pipe(Stream.runCollect, Effect.map((records) => StoreReadPageResult.make({ records: [...records] }))));
3659
- }
3660
- case "StoreInspectTail": {
3661
- const store = yield* ConversationStore;
3662
- return yield* capture(store.inspectTail(request.request).pipe(Effect.map((tail) => StoreInspectTailResult.make({ tail }))));
3663
- }
3664
- case "StoreExport": {
3665
- const store = yield* ConversationStore;
3666
- return yield* capture(store.export(request.request).pipe(Effect.map((conversationExport) => StoreExportResult.make({ export: conversationExport }))));
3667
- }
3668
- }
3669
- });
3670
- /**
3671
- * The last-resort wire fallback when even encoding a response fails: the literal encoded
3672
- * form of `PortFailed(PortProtocolError)` — tagged classes of bounded strings encode to
3673
- * exactly this shape, so no Schema round trip is needed to produce it.
3674
- */
3675
- const encodedProtocolFailure = (message) => ({
3676
- _tag: "PortFailed",
3677
- failure: {
3678
- _tag: "PortProtocolError",
3679
- message: boundPortDiagnostic(message)
3680
- }
3681
- });
3682
- /**
3683
- * The complete owner-side endpoint body for `portCall` (D-P6-3): decode the wire request,
3684
- * execute it against this Object's LOCAL facets, and answer with the encoded response
3685
- * envelope. Total by construction — a request that cannot be decoded, or a response that
3686
- * cannot be encoded, answers `PortFailed(PortProtocolError)` instead of throwing, so the
3687
- * transport never has to interpret exceptions as protocol answers.
3688
- */
3689
- const handleEncodedPortRequest = Effect.fn("DoPortRouting.handleEncodedPortRequest")(function* (encoded) {
3690
- return yield* encodePortResponse(yield* decodePortRequest(encoded).pipe(Effect.flatMap(executePortRequest), Effect.catch((error) => Effect.succeed(PortFailed.make({ failure: PortProtocolError.make({ message: boundPortDiagnostic(`The port request could not be decoded: ${error.message}`) }) }))))).pipe(Effect.catch((error) => Effect.succeed(encodedProtocolFailure(`The port response could not be encoded: ${error.message}`))));
3691
- });
3692
- //#endregion
3693
- export { ConversationPortTransport, CurrentDoStorageVersion, DEFAULT_MAX_STORED_VALUE_BYTES, DoAppendConflict, DoCheckpointConflict, DoFenceRejected, DoLedgerError, DoStorageCompatibilityError, DoStorageConfig, DoStorageConfigValue, DoStorageCorruptionError, DoStorageError, DoStorageFailpoint, DoStorageFailpointError, DoStorageFailpointLocation, DoStorageFailpointTestControl, DoValueBoundExceeded, LedgerAdmitCall, LedgerAdmitResult, LedgerLookupCall, LedgerLookupResult, LedgerMarkReadyCall, LedgerMarkReadyResult, LedgerRecordChildSettledCall, LedgerRecordChildSettledResult, LedgerRequestAbortCall, LedgerRequestAbortResult, LedgerResolveAdmissionCall, LedgerResolveAdmissionResult, MAX_PORT_DIAGNOSTIC_LENGTH, PortFailed, PortFailure, PortProtocolError, PortRequest, PortResponse, PortResult, PortSucceeded, PortTransportError, StoreAppendCall, StoreAppendResult, StoreExportCall, StoreExportResult, StoreInspectTailCall, StoreInspectTailResult, StoreMaterializeCall, StoreMaterializeResult, StoreReadPageCall, StoreReadPageResult, boundPortDiagnostic, conversationStoreLayer, decodePortRequest, decodePortResponse, doMigrations, encodePortRequest, encodePortResponse, evictionFailpointHandler, executePortRequest, handleEncodedPortRequest, layer, ledgerLayer, observationOffsetAt, portTransportFailure, routedConversationStoreLayer, routedSubmissionLedgerLayer, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer };
3694
-
3695
- //# sourceMappingURL=index.mjs.map
1
+ import { t as DoStorageConfig_exports } from "./DoStorageConfig.mjs";
2
+ import { t as DoStorageFailpoint_exports } from "./DoStorageFailpoint.mjs";
3
+ import { t as DoStorageError_exports } from "./DoStorageError.mjs";
4
+ import { t as DoMessageDeliveryStore_exports } from "./DoMessageDeliveryStore.mjs";
5
+ import { t as DoMemoryStore_exports } from "./DoMemoryStore.mjs";
6
+ import { t as DoScheduleStore_exports } from "./DoScheduleStore.mjs";
7
+ import { t as DoStorageVersion_exports } from "./DoStorageVersion.mjs";
8
+ import { t as DoThreadStore_exports } from "./DoThreadStore.mjs";
9
+ import { t as DoSubmissionLedger_exports } from "./DoSubmissionLedger.mjs";
10
+ import { t as DoSubscriptionStore_exports } from "./DoSubscriptionStore.mjs";
11
+ import { t as MemoryProtocol_exports } from "./MemoryProtocol.mjs";
12
+ import { t as PortProtocol_exports } from "./PortProtocol.mjs";
13
+ import { t as PortRouting_exports } from "./PortRouting.mjs";
14
+ export { DoMemoryStore_exports as DoMemoryStore, DoMessageDeliveryStore_exports as DoMessageDeliveryStore, DoScheduleStore_exports as DoScheduleStore, DoStorageConfig_exports as DoStorageConfig, DoStorageError_exports as DoStorageError, DoStorageFailpoint_exports as DoStorageFailpoint, DoStorageVersion_exports as DoStorageVersion, DoSubmissionLedger_exports as DoSubmissionLedger, DoSubscriptionStore_exports as DoSubscriptionStore, DoThreadStore_exports as DoThreadStore, MemoryProtocol_exports as MemoryProtocol, PortProtocol_exports as PortProtocol, PortRouting_exports as PortRouting };