@effect-agent/storage-cloudflare 0.1.0-beta.38 → 0.1.0-beta.40

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.
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
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, ScheduleCapacityError, ScheduleChange, ScheduleConflict, ScheduleDueCursor, ScheduleFailpoint, ScheduleId, ScheduleInstant, ScheduleKey, ScheduleNotFound, ScheduleOwner, SchedulePageRequest, ScheduleRecord, ScheduleStorageError, ScheduleStore, Settlement, SettlementConflict, SettlementFinalization, SettlementOutcome, SettlementReservation, SettlementReservationSnapshot, SubmissionLedger, SubmissionLookup, SubmissionLookupById, SubmissionLookupByKey, SubmissionSnapshot, SubmissionState, SuspendRequest, SuspensionReason, SuspensionSnapshot, UnknownResolution, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, applyScheduleChange, defaultSchedulingLimits, digestCanonicalBatch, scheduleDeadline, scheduleUsesCapacity, settlementFailureFromRecord, submissionAbortRecordId } from "@effect-agent/session";
1
+ import { t as DoStorageFailpoint } from "./do-storage-failpoint-C-Qtchs7.mjs";
2
+ import { AbortCommand, AbortIntent, AcceptedEvent, 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, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, DeliveryChange, Digest, EMPTY_TAIL_DIGEST, FenceRejected, FencedAppendRequest, InputAppliedMarker, JoinSnapshot, JoinedToHost, JoiningClaim, LedgerCapabilities, LedgerError, LoadCheckpointRequest, MarkInputAppliedRequest, MarkJoinedRequest, MarkReadyRequest, MarkUnknownRequest, ObservationOffset, OwnershipLost, OwnershipRenewal, OwnershipSnapshot, ParentLinkage, PersistedJson, ProducerEpoch, QueueSequence, RecordEnvelope, RecoverySnapshot, RecoverySnapshotRequest, ReleaseChildBudgetRequest, ReleaseOwnershipRequest, RenewOwnershipRequest, ReservedChildBudget, ReservedSettlement, RevertJoiningRequest, SaveCheckpointRequest, ScheduleCapacityError, ScheduleChange, ScheduleConflict, ScheduleDueCursor, ScheduleFailpoint, ScheduleId, ScheduleInstant, ScheduleKey, ScheduleNotFound, ScheduleOwner, SchedulePageRequest, ScheduleRecord, ScheduleStorageError, ScheduleStore, Settlement, SettlementConflict, SettlementFinalization, SettlementOutcome, SettlementReservation, SettlementReservationSnapshot, SourcePartition, SubmissionLedger, SubmissionLookup, SubmissionLookupById, SubmissionLookupByKey, SubmissionSnapshot, SubmissionState, SubscriptionDelivery, SubscriptionDeliveryKey, SubscriptionError, SubscriptionFailpoint, SubscriptionKey, SubscriptionLimits, SubscriptionName, SubscriptionRecord, SubscriptionScanCursors, SubscriptionStore, SuspendRequest, SuspensionReason, SuspensionSnapshot, ThreadCheckpoint, ThreadExport, ThreadExportRequest, ThreadMaterialization, ThreadNotMaterialized, ThreadObservation, ThreadRead, ThreadStore, ThreadStoreError, ThreadTail, ThreadTailRequest, UnknownResolution, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, applyScheduleChange, applySubscriptionDeliveryChange, defaultSchedulingLimits, digestCanonicalBatch, sameAcceptedEventIdentity, sameSourcePartition, scheduleDeadline, scheduleUsesCapacity, settlementFailureFromRecord, submissionAbortRecordId, subscriptionCanSelect, subscriptionDeliveryCanSelect, subscriptionDeliveryKeyString } from "@effect-agent/thread";
2
3
  import { Clock, Context, Crypto, DateTime, Duration, Effect, Layer, Option, Predicate, Ref, Result, Schema, Stream } from "effect";
3
4
  import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-do";
4
5
  import * as SqlClient from "effect/unstable/sql/SqlClient";
@@ -64,7 +65,7 @@ var DoAppendConflict = class extends Schema.TaggedError()("DoAppendConflict", {
64
65
  actualTailDigest: Schema.optionalKey(Schema.String)
65
66
  }) {};
66
67
  /**
67
- * A producer epoch does not match the Conversation's current writer registration. Appends
68
+ * A producer epoch does not match the Thread's current writer registration. Appends
68
69
  * require the exact registered epoch, so both older and newer unregistered epochs are fenced;
69
70
  * a newer epoch takes over by materializing first.
70
71
  */
@@ -91,7 +92,7 @@ const DoStorageFailpointLocation = Schema.Literals([
91
92
  "append:after-record-insert",
92
93
  "append:after-tail-update",
93
94
  "append:after",
94
- "export:after-conversation-read",
95
+ "export:after-thread-read",
95
96
  "save-checkpoint:before",
96
97
  "save-checkpoint:after",
97
98
  "ledger:admit:before",
@@ -145,32 +146,25 @@ var DoStorageFailpointError = class extends Schema.TaggedError()("DoStorageFailp
145
146
  };
146
147
  //#endregion
147
148
  //#region src/migrations.ts
149
+ /** The current storage version recorded in `effect_agent_meta`. */
150
+ const CurrentDoStorageVersion = 2;
148
151
  /**
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:
152
+ * The Thread Durable Object schema shares its thread and ledger tables with Node/SQLite.
153
+ * Schedules and subscriptions use separate Durable Objects. Two DC-specific additions:
160
154
  *
161
155
  * 1. `effect_agent_meta` replaces `PRAGMA user_version` as the exact-or-fresh version gate —
162
156
  * a meta table is portable regardless of which PRAGMAs Durable Object SQL storage allows.
163
157
  * 2. `effect_agent_child_settlements` is the durable cross-store notification marker the
164
158
  * SubmissionLedger port contract mandates for cross-store adapters (`suspend`'s covering
165
- * check and `recordChildSettled`'s wake both consult it): parent and child Conversations
159
+ * check and `recordChildSettled`'s wake both consult it): parent and child Threads
166
160
  * live in different Durable Objects, so a child settlement reported before the parent's
167
161
  * suspend commits must be observable from the PARENT's own storage.
168
162
  */
169
- const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversation_object": Effect.gen(function* () {
163
+ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_thread_object": Effect.gen(function* () {
170
164
  const sql = yield* SqlClient.SqlClient;
171
165
  yield* sql`
172
- CREATE TABLE effect_agent_conversations (
173
- conversation_id TEXT PRIMARY KEY NOT NULL,
166
+ CREATE TABLE effect_agent_threads (
167
+ thread_id TEXT PRIMARY KEY NOT NULL,
174
168
  created_at TEXT NOT NULL,
175
169
  tail_sequence INTEGER NOT NULL,
176
170
  tail_digest TEXT NOT NULL,
@@ -179,53 +173,53 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
179
173
  `.withoutTransform;
180
174
  yield* sql`
181
175
  CREATE TABLE effect_agent_canonical_batches (
182
- conversation_id TEXT NOT NULL,
176
+ thread_id TEXT NOT NULL,
183
177
  batch_id TEXT NOT NULL,
184
178
  first_sequence INTEGER NOT NULL,
185
179
  last_sequence INTEGER NOT NULL,
186
180
  batch_digest TEXT NOT NULL,
187
181
  tail_digest TEXT NOT NULL,
188
182
  batch_json TEXT NOT NULL,
189
- PRIMARY KEY (conversation_id, batch_id),
190
- FOREIGN KEY (conversation_id)
191
- REFERENCES effect_agent_conversations(conversation_id)
183
+ PRIMARY KEY (thread_id, batch_id),
184
+ FOREIGN KEY (thread_id)
185
+ REFERENCES effect_agent_threads(thread_id)
192
186
  ON DELETE RESTRICT
193
187
  )
194
188
  `.withoutTransform;
195
189
  yield* sql`
196
190
  CREATE TABLE effect_agent_canonical_records (
197
- conversation_id TEXT NOT NULL,
191
+ thread_id TEXT NOT NULL,
198
192
  sequence INTEGER NOT NULL,
199
193
  record_id TEXT NOT NULL,
200
194
  batch_id TEXT NOT NULL,
201
195
  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)
196
+ PRIMARY KEY (thread_id, sequence),
197
+ UNIQUE (thread_id, record_id),
198
+ FOREIGN KEY (thread_id, batch_id)
199
+ REFERENCES effect_agent_canonical_batches(thread_id, batch_id)
206
200
  ON DELETE RESTRICT
207
201
  )
208
202
  `.withoutTransform;
209
203
  yield* sql`
210
204
  CREATE INDEX effect_agent_canonical_records_batch
211
- ON effect_agent_canonical_records (conversation_id, batch_id, sequence)
205
+ ON effect_agent_canonical_records (thread_id, batch_id, sequence)
212
206
  `.withoutTransform;
213
207
  yield* sql`
214
208
  CREATE TABLE effect_agent_checkpoints (
215
- conversation_id TEXT NOT NULL,
209
+ thread_id TEXT NOT NULL,
216
210
  through_sequence INTEGER NOT NULL,
217
211
  tail_digest TEXT NOT NULL,
218
212
  checkpoint_json TEXT NOT NULL,
219
- PRIMARY KEY (conversation_id, through_sequence),
220
- FOREIGN KEY (conversation_id)
221
- REFERENCES effect_agent_conversations(conversation_id)
213
+ PRIMARY KEY (thread_id, through_sequence),
214
+ FOREIGN KEY (thread_id)
215
+ REFERENCES effect_agent_threads(thread_id)
222
216
  ON DELETE RESTRICT
223
217
  )
224
218
  `.withoutTransform;
225
219
  yield* sql`
226
220
  CREATE TABLE effect_agent_submissions (
227
221
  submission_id TEXT PRIMARY KEY NOT NULL,
228
- conversation_id TEXT NOT NULL,
222
+ thread_id TEXT NOT NULL,
229
223
  queue_sequence INTEGER NOT NULL,
230
224
  principal TEXT NOT NULL,
231
225
  idempotency_key TEXT NOT NULL,
@@ -248,8 +242,8 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
248
242
  unknown_tool_call_ids_json TEXT,
249
243
  parent_submission_id TEXT,
250
244
  parent_tool_call_id TEXT,
251
- UNIQUE (conversation_id, principal, idempotency_key),
252
- UNIQUE (conversation_id, queue_sequence)
245
+ UNIQUE (thread_id, principal, idempotency_key),
246
+ UNIQUE (thread_id, queue_sequence)
253
247
  )
254
248
  `.withoutTransform;
255
249
  yield* sql`
@@ -277,7 +271,7 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
277
271
  CREATE TABLE effect_agent_attempts (
278
272
  attempt_id TEXT PRIMARY KEY NOT NULL,
279
273
  submission_id TEXT NOT NULL,
280
- conversation_id TEXT NOT NULL,
274
+ thread_id TEXT NOT NULL,
281
275
  owner_producer_id TEXT NOT NULL,
282
276
  producer_epoch INTEGER NOT NULL,
283
277
  claimed_at TEXT NOT NULL,
@@ -377,7 +371,7 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
377
371
  `.withoutTransform;
378
372
  yield* sql`
379
373
  INSERT INTO effect_agent_meta (key, value)
380
- VALUES ('storage_version', ${String(1)})
374
+ VALUES ('storage_version', ${String(2)})
381
375
  `.withoutTransform;
382
376
  }) });
383
377
  //#endregion
@@ -390,10 +384,8 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
390
384
  */
391
385
  const BoundedStoredText$1 = Schema.String.check(Schema.isMaxLength(2e6));
392
386
  const BoundedIdentifier$1 = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
393
- const MAX_RECORDS_PER_CONVERSATION = 65536;
387
+ const MAX_RECORDS_PER_THREAD = 65536;
394
388
  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
389
  const isSqlError = Schema.is(SqlError);
398
390
  const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
399
391
  const chunked = (values, size) => {
@@ -403,8 +395,8 @@ const chunked = (values, size) => {
403
395
  };
404
396
  var DoMetaRow = class extends Schema.Class("DoMetaRow")({ value: Schema.NonEmptyString.check(Schema.isMaxLength(128)) }) {};
405
397
  var DoNameRow = class extends Schema.Class("DoNameRow")({ name: BoundedIdentifier$1 }) {};
406
- var ConversationRow = class extends Schema.Class("ConversationRow")({
407
- conversation_id: BoundedIdentifier$1,
398
+ var ThreadRow = class extends Schema.Class("ThreadRow")({
399
+ thread_id: BoundedIdentifier$1,
408
400
  created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
409
401
  producer_epoch: ProducerEpoch,
410
402
  tail_digest: BoundedStoredText$1,
@@ -414,21 +406,21 @@ var BatchRow = class extends Schema.Class("BatchRow")({
414
406
  batch_digest: BoundedStoredText$1,
415
407
  batch_id: BoundedIdentifier$1,
416
408
  batch_json: BoundedStoredText$1,
417
- conversation_id: BoundedIdentifier$1,
409
+ thread_id: BoundedIdentifier$1,
418
410
  first_sequence: CanonicalSequence,
419
411
  last_sequence: CanonicalSequence,
420
412
  tail_digest: BoundedStoredText$1
421
413
  }) {};
422
414
  var RecordRow = class extends Schema.Class("RecordRow")({
423
415
  batch_id: BoundedIdentifier$1,
424
- conversation_id: BoundedIdentifier$1,
416
+ thread_id: BoundedIdentifier$1,
425
417
  record_id: BoundedIdentifier$1,
426
418
  record_json: BoundedStoredText$1,
427
419
  sequence: CanonicalSequence
428
420
  }) {};
429
421
  var CheckpointRow = class extends Schema.Class("CheckpointRow")({
430
422
  checkpoint_json: BoundedStoredText$1,
431
- conversation_id: BoundedIdentifier$1,
423
+ thread_id: BoundedIdentifier$1,
432
424
  tail_digest: BoundedStoredText$1,
433
425
  through_sequence: CanonicalSequence
434
426
  }) {};
@@ -440,7 +432,7 @@ var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-cloudfl
440
432
  batchDigest: BoundedStoredText$1,
441
433
  batchId: BoundedIdentifier$1,
442
434
  batchJson: BoundedStoredText$1,
443
- conversationId: BoundedIdentifier$1,
435
+ threadId: BoundedIdentifier$1,
444
436
  expectedTailDigest: BoundedStoredText$1,
445
437
  expectedTailSequence: CanonicalSequence,
446
438
  producerEpoch: ProducerEpoch,
@@ -454,36 +446,36 @@ var RawAppendResult = class extends Schema.Class("@effect-agent/storage-cloudfla
454
446
  tailDigest: BoundedStoredText$1
455
447
  }) {};
456
448
  var RawReadRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawReadRequest")({
457
- conversationId: BoundedIdentifier$1,
449
+ threadId: BoundedIdentifier$1,
458
450
  fromSequenceExclusive: CanonicalSequence,
459
451
  limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
460
452
  }) {};
461
453
  var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-cloudflare/RawCheckpoint")({
462
454
  checkpointJson: BoundedStoredText$1,
463
- conversationId: BoundedIdentifier$1,
455
+ threadId: BoundedIdentifier$1,
464
456
  tailDigest: BoundedStoredText$1,
465
457
  throughSequence: CanonicalSequence
466
458
  }) {};
467
- var RawConversationExport = class extends Schema.Class("@effect-agent/storage-cloudflare/RawConversationExport")({
459
+ var RawThreadExport = class extends Schema.Class("@effect-agent/storage-cloudflare/RawThreadExport")({
468
460
  batches: Schema.Array(BatchRow),
469
461
  checkpoints: Schema.Array(CheckpointRow),
470
- conversation: ConversationRow,
462
+ thread: ThreadRow,
471
463
  records: Schema.Array(RecordRow)
472
464
  }) {};
473
- const noFailpoint$1 = () => Effect.void;
465
+ const noFailpoint = () => Effect.void;
474
466
  const storageError = (operation) => (error) => DoStorageError.make({
475
467
  cause: error,
476
468
  operation,
477
469
  message: error.message
478
470
  });
479
471
  /** Decode raw Durable Object SQLite rows against a Schema, reporting failures as typed corruption. */
480
- const decodeRows$1 = Effect.fn((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
472
+ const decodeRows$2 = Effect.fn((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
481
473
  table,
482
474
  rowKey,
483
475
  message: String(error)
484
476
  }))));
485
477
  /** Decode exactly one raw row against a Schema, reporting failures as typed corruption. */
486
- const decodeSingleRow = Effect.fn((schema, table, rowKey, rows) => decodeRows$1(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(DoStorageCorruptionError.make({
478
+ const decodeSingleRow = Effect.fn((schema, table, rowKey, rows) => decodeRows$2(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(DoStorageCorruptionError.make({
487
479
  table,
488
480
  rowKey,
489
481
  message: `Expected exactly one row but found ${decoded.length}.`
@@ -497,7 +489,7 @@ const REQUIRED_TABLES = [
497
489
  "effect_agent_checkpoints",
498
490
  "effect_agent_child_reservations",
499
491
  "effect_agent_child_settlements",
500
- "effect_agent_conversations",
492
+ "effect_agent_threads",
501
493
  "effect_agent_meta",
502
494
  "effect_agent_settlement_reservations",
503
495
  "effect_agent_submission_ownership",
@@ -511,14 +503,14 @@ const REQUIRED_TABLES = [
511
503
  * through output gates) and no busy timeout (a Durable Object has exactly one writer): the
512
504
  * Node machinery those served has no DC analogue and is deliberately absent.
513
505
  */
514
- const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint$1, maxStoredValueBytes) {
506
+ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint, maxStoredValueBytes) {
515
507
  const metaTableRows = yield* sql`
516
508
  SELECT name
517
509
  FROM sqlite_master
518
510
  WHERE type = 'table'
519
511
  AND name = 'effect_agent_meta'
520
512
  `.pipe(Effect.mapError(storageError("read storage version table")));
521
- if ((yield* decodeRows$1(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_meta", metaTableRows)).length === 0) {
513
+ if ((yield* decodeRows$2(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_meta", metaTableRows)).length === 0) {
522
514
  const existingRows = yield* sql`
523
515
  SELECT name
524
516
  FROM sqlite_master
@@ -526,9 +518,9 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
526
518
  AND name LIKE 'effect_agent_%'
527
519
  ORDER BY name
528
520
  `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
529
- if ((yield* decodeRows$1(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* DoStorageCompatibilityError.make({
521
+ if ((yield* decodeRows$2(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* DoStorageCompatibilityError.make({
530
522
  actualVersion: 0,
531
- supportedVersion: 1,
523
+ supportedVersion: 2,
532
524
  message: "The Durable Object contains unversioned Effect Agent tables. Reset the development namespace explicitly; refusing to mutate ambiguous stored data."
533
525
  });
534
526
  yield* SqliteMigrator.run({ loader: doMigrations }).pipe(Effect.provideService(SqlClient.SqlClient, sql), Effect.mapError((error) => DoStorageError.make({
@@ -543,12 +535,12 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
543
535
  WHERE key = 'storage_version'
544
536
  `.pipe(Effect.mapError(storageError("read storage version")));
545
537
  const version = yield* decodeSingleRow(Schema.Array(DoMetaRow), "effect_agent_meta", "storage_version", versionRows);
546
- if (version.value !== String(1)) {
538
+ if (version.value !== String(2)) {
547
539
  const actualVersion = Number.parseInt(version.value, 10);
548
540
  return yield* DoStorageCompatibilityError.make({
549
541
  actualVersion: Number.isSafeInteger(actualVersion) ? actualVersion : -1,
550
- supportedVersion: 1,
551
- 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.`
542
+ supportedVersion: 2,
543
+ message: `The Durable Object uses private-development storage version ${version.value}; this build supports exactly version 2. Replace the development namespace explicitly; automatic stored-data migrations are not provided during private development.`
552
544
  });
553
545
  }
554
546
  }
@@ -559,9 +551,9 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
559
551
  AND name IN ${sql.in([...REQUIRED_TABLES])}
560
552
  ORDER BY name
561
553
  `.pipe(Effect.mapError(storageError("verify storage tables")));
562
- if ((yield* decodeRows$1(Schema.Array(DoNameRow), "sqlite_master", "required_tables", requiredRows)).length !== REQUIRED_TABLES.length) return yield* DoStorageCompatibilityError.make({
563
- actualVersion: 1,
564
- supportedVersion: 1,
554
+ if ((yield* decodeRows$2(Schema.Array(DoNameRow), "sqlite_master", "required_tables", requiredRows)).length !== REQUIRED_TABLES.length) return yield* DoStorageCompatibilityError.make({
555
+ actualVersion: 2,
556
+ supportedVersion: 2,
565
557
  message: "The Durable Object claims the current format but is missing required tables. Reset the corrupt private-development data."
566
558
  });
567
559
  return makeJournal(sql, failpoint, maxStoredValueBytes);
@@ -589,45 +581,45 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
589
581
  * transaction.
590
582
  */
591
583
  const withWriteTransaction = (operation) => (effect) => sql.withTransaction(effect).pipe(Effect.mapError((error) => isSqlError(error) ? storageError(operation)(error) : error), Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }));
592
- const materialize = Effect.fn("DoJournal.materialize")(function* (conversationId, createdAt, emptyTailDigest, producerEpoch) {
593
- if (conversationId.length > MAX_IDENTIFIER_LENGTH$1) return yield* DoStorageError.make({
594
- operation: "materialize conversation",
595
- message: "Conversation identity exceeds the Durable Object storage bounds."
584
+ const materialize = Effect.fn("DoJournal.materialize")(function* (threadId, createdAt, emptyTailDigest, producerEpoch) {
585
+ if (threadId.length > MAX_IDENTIFIER_LENGTH$1) return yield* DoStorageError.make({
586
+ operation: "materialize thread",
587
+ message: "Thread identity exceeds the Durable Object storage bounds."
596
588
  });
597
- yield* checkValueBound("materialize conversation", emptyTailDigest);
589
+ yield* checkValueBound("materialize thread", emptyTailDigest);
598
590
  yield* withWriteTransaction("materialize transaction")(Effect.gen(function* () {
599
591
  const existingRows = yield* sql`
600
592
  SELECT
601
- conversation_id,
593
+ thread_id,
602
594
  created_at,
603
595
  tail_sequence,
604
596
  tail_digest,
605
597
  producer_epoch
606
- FROM effect_agent_conversations
607
- WHERE conversation_id = ${conversationId}
608
- `.pipe(Effect.mapError(storageError("read materialized conversation")));
609
- const existing = yield* decodeRows$1(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, existingRows);
598
+ FROM effect_agent_threads
599
+ WHERE thread_id = ${threadId}
600
+ `.pipe(Effect.mapError(storageError("read materialized thread")));
601
+ const existing = yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, existingRows);
610
602
  if (existing.length > 1) return yield* DoStorageCorruptionError.make({
611
- table: "effect_agent_conversations",
612
- rowKey: conversationId,
613
- message: "A conversation primary key returned more than one row."
603
+ table: "effect_agent_threads",
604
+ rowKey: threadId,
605
+ message: "A thread primary key returned more than one row."
614
606
  });
615
607
  if (existing.length === 0) {
616
608
  yield* sql`
617
- INSERT INTO effect_agent_conversations (
618
- conversation_id,
609
+ INSERT INTO effect_agent_threads (
610
+ thread_id,
619
611
  created_at,
620
612
  tail_sequence,
621
613
  tail_digest,
622
614
  producer_epoch
623
615
  ) VALUES (
624
- ${conversationId},
616
+ ${threadId},
625
617
  ${createdAt},
626
618
  0,
627
619
  ${emptyTailDigest},
628
620
  ${producerEpoch}
629
621
  )
630
- `.pipe(Effect.mapError(storageError("materialize conversation")));
622
+ `.pipe(Effect.mapError(storageError("materialize thread")));
631
623
  return;
632
624
  }
633
625
  if (producerEpoch < existing[0].producer_epoch) return yield* DoFenceRejected.make({
@@ -636,27 +628,27 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
636
628
  message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
637
629
  });
638
630
  if (producerEpoch > existing[0].producer_epoch) yield* sql`
639
- UPDATE effect_agent_conversations
631
+ UPDATE effect_agent_threads
640
632
  SET producer_epoch = ${producerEpoch}
641
- WHERE conversation_id = ${conversationId}
633
+ WHERE thread_id = ${threadId}
642
634
  `.pipe(Effect.mapError(storageError("advance materialization epoch")));
643
635
  }));
644
636
  });
645
- const getConversation = Effect.fn("DoJournal.getConversation")(function* (conversationId) {
637
+ const getThread = Effect.fn("DoJournal.getThread")(function* (threadId) {
646
638
  const rows = yield* sql`
647
639
  SELECT
648
- conversation_id,
640
+ thread_id,
649
641
  created_at,
650
642
  tail_sequence,
651
643
  tail_digest,
652
644
  producer_epoch
653
- FROM effect_agent_conversations
654
- WHERE conversation_id = ${conversationId}
655
- `.pipe(Effect.mapError(storageError("read conversation")));
656
- return yield* decodeRows$1(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, rows);
645
+ FROM effect_agent_threads
646
+ WHERE thread_id = ${threadId}
647
+ `.pipe(Effect.mapError(storageError("read thread")));
648
+ return yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
657
649
  });
658
650
  const append = Effect.fn("DoJournal.append")(function* (request) {
659
- 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({
651
+ if (request.threadId.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({
660
652
  operation: "append canonical batch",
661
653
  message: "Canonical identifiers exceed the Durable Object storage bounds."
662
654
  });
@@ -670,25 +662,25 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
670
662
  message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
671
663
  reason: "record-identity"
672
664
  });
673
- const conversationRows = yield* sql`
665
+ const threadRows = yield* sql`
674
666
  SELECT
675
- conversation_id,
667
+ thread_id,
676
668
  created_at,
677
669
  tail_sequence,
678
670
  tail_digest,
679
671
  producer_epoch
680
- FROM effect_agent_conversations
681
- WHERE conversation_id = ${request.conversationId}
672
+ FROM effect_agent_threads
673
+ WHERE thread_id = ${request.threadId}
682
674
  `.pipe(Effect.mapError(storageError("read append tail")));
683
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", request.conversationId, conversationRows);
684
- if (request.producerEpoch !== conversation.producer_epoch) return yield* DoFenceRejected.make({
675
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", request.threadId, threadRows);
676
+ if (request.producerEpoch !== thread.producer_epoch) return yield* DoFenceRejected.make({
685
677
  producerEpoch: request.producerEpoch,
686
- actualEpoch: conversation.producer_epoch,
687
- message: `Producer epoch ${request.producerEpoch} is not the current epoch ${conversation.producer_epoch}.`
678
+ actualEpoch: thread.producer_epoch,
679
+ message: `Producer epoch ${request.producerEpoch} is not the current epoch ${thread.producer_epoch}.`
688
680
  });
689
681
  const batchRows = yield* sql`
690
682
  SELECT
691
- conversation_id,
683
+ thread_id,
692
684
  batch_id,
693
685
  first_sequence,
694
686
  last_sequence,
@@ -696,13 +688,13 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
696
688
  tail_digest,
697
689
  batch_json
698
690
  FROM effect_agent_canonical_batches
699
- WHERE conversation_id = ${request.conversationId}
691
+ WHERE thread_id = ${request.threadId}
700
692
  AND batch_id = ${request.batchId}
701
693
  `.pipe(Effect.mapError(storageError("read idempotent batch")));
702
- const batches = yield* decodeRows$1(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.conversationId}/${request.batchId}`, batchRows);
694
+ const batches = yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.threadId}/${request.batchId}`, batchRows);
703
695
  if (batches.length > 1) return yield* DoStorageCorruptionError.make({
704
696
  table: "effect_agent_canonical_batches",
705
- rowKey: `${request.conversationId}/${request.batchId}`,
697
+ rowKey: `${request.threadId}/${request.batchId}`,
706
698
  message: "A canonical batch primary key returned more than one row."
707
699
  });
708
700
  if (batches.length === 1) {
@@ -718,37 +710,37 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
718
710
  tailDigest: existing.tail_digest
719
711
  });
720
712
  }
721
- if (request.expectedTailSequence !== conversation.tail_sequence || request.expectedTailDigest !== conversation.tail_digest) return yield* DoAppendConflict.make({
722
- message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${conversation.tail_sequence}/${conversation.tail_digest}.`,
713
+ if (request.expectedTailSequence !== thread.tail_sequence || request.expectedTailDigest !== thread.tail_digest) return yield* DoAppendConflict.make({
714
+ message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${thread.tail_sequence}/${thread.tail_digest}.`,
723
715
  reason: "tail",
724
- actualTailSequence: conversation.tail_sequence,
725
- actualTailDigest: conversation.tail_digest
716
+ actualTailSequence: thread.tail_sequence,
717
+ actualTailDigest: thread.tail_digest
726
718
  });
727
- if (conversation.tail_sequence + request.records.length > MAX_RECORDS_PER_CONVERSATION) return yield* DoStorageError.make({
719
+ if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) return yield* DoStorageError.make({
728
720
  operation: "append canonical batch",
729
- message: `Conversation record limit ${MAX_RECORDS_PER_CONVERSATION} would be exceeded.`
721
+ message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`
730
722
  });
731
723
  const existingRecords = [];
732
- for (const chunk of chunked(recordIds, MAX_BOUND_PARAMETERS - 10)) {
724
+ for (const chunk of chunked(recordIds, 90)) {
733
725
  const existingRecordRows = yield* sql`
734
726
  SELECT
735
- conversation_id,
727
+ thread_id,
736
728
  sequence,
737
729
  record_id,
738
730
  batch_id,
739
731
  record_json
740
732
  FROM effect_agent_canonical_records
741
- WHERE conversation_id = ${request.conversationId}
733
+ WHERE thread_id = ${request.threadId}
742
734
  AND record_id IN ${sql.in([...chunk])}
743
735
  ORDER BY sequence
744
736
  `.pipe(Effect.mapError(storageError("check canonical record identities")));
745
- existingRecords.push(...yield* decodeRows$1(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}/record_ids`, existingRecordRows));
737
+ existingRecords.push(...yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/record_ids`, existingRecordRows));
746
738
  }
747
739
  if (existingRecords.length > 0) return yield* DoAppendConflict.make({
748
740
  message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
749
741
  reason: "record-identity"
750
742
  });
751
- const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(conversation.tail_sequence + 1).pipe(Effect.mapError((error) => DoStorageError.make({
743
+ const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(thread.tail_sequence + 1).pipe(Effect.mapError((error) => DoStorageError.make({
752
744
  cause: error,
753
745
  operation: "append canonical batch",
754
746
  message: error.message
@@ -760,7 +752,7 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
760
752
  })));
761
753
  yield* sql`
762
754
  INSERT INTO effect_agent_canonical_batches (
763
- conversation_id,
755
+ thread_id,
764
756
  batch_id,
765
757
  first_sequence,
766
758
  last_sequence,
@@ -768,7 +760,7 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
768
760
  tail_digest,
769
761
  batch_json
770
762
  ) VALUES (
771
- ${request.conversationId},
763
+ ${request.threadId},
772
764
  ${request.batchId},
773
765
  ${firstSequence},
774
766
  ${lastSequence},
@@ -781,13 +773,13 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
781
773
  yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
782
774
  yield* sql`
783
775
  INSERT INTO effect_agent_canonical_records (
784
- conversation_id,
776
+ thread_id,
785
777
  sequence,
786
778
  record_id,
787
779
  batch_id,
788
780
  record_json
789
781
  ) VALUES (
790
- ${request.conversationId},
782
+ ${request.threadId},
791
783
  ${firstSequence + index},
792
784
  ${record.recordId},
793
785
  ${request.batchId},
@@ -797,13 +789,13 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
797
789
  yield* failpoint("append:after-record-insert");
798
790
  }), { discard: true });
799
791
  yield* sql`
800
- UPDATE effect_agent_conversations
792
+ UPDATE effect_agent_threads
801
793
  SET
802
794
  tail_sequence = ${lastSequence},
803
795
  tail_digest = ${request.tailDigest},
804
796
  producer_epoch = ${request.producerEpoch}
805
- WHERE conversation_id = ${request.conversationId}
806
- `.pipe(Effect.mapError(storageError("advance conversation tail")));
797
+ WHERE thread_id = ${request.threadId}
798
+ `.pipe(Effect.mapError(storageError("advance thread tail")));
807
799
  yield* failpoint("append:after-tail-update");
808
800
  return RawAppendResult.make({
809
801
  firstSequence,
@@ -816,36 +808,36 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
816
808
  const read = Effect.fn("DoJournal.read")(function* (request) {
817
809
  const rows = yield* sql`
818
810
  SELECT
819
- conversation_id,
811
+ thread_id,
820
812
  sequence,
821
813
  record_id,
822
814
  batch_id,
823
815
  record_json
824
816
  FROM effect_agent_canonical_records
825
- WHERE conversation_id = ${request.conversationId}
817
+ WHERE thread_id = ${request.threadId}
826
818
  AND sequence > ${request.fromSequenceExclusive}
827
819
  ORDER BY sequence
828
820
  LIMIT ${request.limit}
829
821
  `.pipe(Effect.mapError(storageError("read canonical records")));
830
- return yield* decodeRows$1(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}>${request.fromSequenceExclusive}`, rows);
822
+ return yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}>${request.fromSequenceExclusive}`, rows);
831
823
  });
832
- const exportConversation = Effect.fn("DoJournal.exportConversation")(function* (conversationId) {
824
+ const exportThread = Effect.fn("DoJournal.exportThread")(function* (threadId) {
833
825
  return yield* sql.withTransaction(Effect.gen(function* () {
834
- const conversationRows = yield* sql`
826
+ const threadRows = yield* sql`
835
827
  SELECT
836
- conversation_id,
828
+ thread_id,
837
829
  created_at,
838
830
  tail_sequence,
839
831
  tail_digest,
840
832
  producer_epoch
841
- FROM effect_agent_conversations
842
- WHERE conversation_id = ${conversationId}
843
- `.pipe(Effect.mapError(storageError("export conversation")));
844
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, conversationRows);
845
- yield* failpoint("export:after-conversation-read");
833
+ FROM effect_agent_threads
834
+ WHERE thread_id = ${threadId}
835
+ `.pipe(Effect.mapError(storageError("export thread")));
836
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", threadId, threadRows);
837
+ yield* failpoint("export:after-thread-read");
846
838
  const batchRows = yield* sql`
847
839
  SELECT
848
- conversation_id,
840
+ thread_id,
849
841
  batch_id,
850
842
  first_sequence,
851
843
  last_sequence,
@@ -853,71 +845,71 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
853
845
  tail_digest,
854
846
  batch_json
855
847
  FROM effect_agent_canonical_batches
856
- WHERE conversation_id = ${conversationId}
848
+ WHERE thread_id = ${threadId}
857
849
  ORDER BY first_sequence
858
850
  `.pipe(Effect.mapError(storageError("export canonical batches")));
859
851
  const recordRows = yield* sql`
860
852
  SELECT
861
- conversation_id,
853
+ thread_id,
862
854
  sequence,
863
855
  record_id,
864
856
  batch_id,
865
857
  record_json
866
858
  FROM effect_agent_canonical_records
867
- WHERE conversation_id = ${conversationId}
859
+ WHERE thread_id = ${threadId}
868
860
  ORDER BY sequence
869
861
  `.pipe(Effect.mapError(storageError("export canonical records")));
870
862
  const checkpointRows = yield* sql`
871
863
  SELECT
872
- conversation_id,
864
+ thread_id,
873
865
  through_sequence,
874
866
  tail_digest,
875
867
  checkpoint_json
876
868
  FROM effect_agent_checkpoints
877
- WHERE conversation_id = ${conversationId}
869
+ WHERE thread_id = ${threadId}
878
870
  ORDER BY through_sequence
879
871
  `.pipe(Effect.mapError(storageError("export checkpoints")));
880
- return RawConversationExport.make({
881
- conversation,
882
- batches: yield* decodeRows$1(Schema.Array(BatchRow), "effect_agent_canonical_batches", conversationId, batchRows),
883
- records: yield* decodeRows$1(Schema.Array(RecordRow), "effect_agent_canonical_records", conversationId, recordRows),
884
- checkpoints: yield* decodeRows$1(Schema.Array(CheckpointRow), "effect_agent_checkpoints", conversationId, checkpointRows)
872
+ return RawThreadExport.make({
873
+ thread,
874
+ batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", threadId, batchRows),
875
+ records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", threadId, recordRows),
876
+ checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", threadId, checkpointRows)
885
877
  });
886
878
  })).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(storageError("export transaction")(error))));
887
879
  });
888
880
  const saveCheckpoint = Effect.fn("DoJournal.saveCheckpoint")(function* (checkpoint) {
889
- if (checkpoint.conversationId.length > MAX_IDENTIFIER_LENGTH$1) return yield* DoStorageError.make({
881
+ if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH$1) return yield* DoStorageError.make({
890
882
  operation: "save checkpoint",
891
883
  message: "Checkpoint identity exceeds the Durable Object storage bounds."
892
884
  });
893
885
  yield* checkValueBound("save checkpoint", checkpoint.checkpointJson);
894
886
  yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
895
- const conversationRows = yield* sql`
887
+ const threadRows = yield* sql`
896
888
  SELECT
897
- conversation_id,
889
+ thread_id,
898
890
  created_at,
899
891
  tail_sequence,
900
892
  tail_digest,
901
893
  producer_epoch
902
- FROM effect_agent_conversations
903
- WHERE conversation_id = ${checkpoint.conversationId}
894
+ FROM effect_agent_threads
895
+ WHERE thread_id = ${checkpoint.threadId}
904
896
  `.pipe(Effect.mapError(storageError("read checkpoint tail")));
905
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", checkpoint.conversationId, conversationRows);
906
- if (checkpoint.throughSequence > conversation.tail_sequence) return yield* DoCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${conversation.tail_sequence}.` });
897
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", checkpoint.threadId, threadRows);
898
+ if (checkpoint.throughSequence > thread.tail_sequence) return yield* DoCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${thread.tail_sequence}.` });
907
899
  const checkpointRows = yield* sql`
908
900
  SELECT
909
- conversation_id,
901
+ thread_id,
910
902
  through_sequence,
911
903
  tail_digest,
912
904
  checkpoint_json
913
905
  FROM effect_agent_checkpoints
914
- WHERE conversation_id = ${checkpoint.conversationId}
906
+ WHERE thread_id = ${checkpoint.threadId}
915
907
  AND through_sequence = ${checkpoint.throughSequence}
916
908
  `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
917
- const existing = yield* decodeRows$1(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.conversationId}/${checkpoint.throughSequence}`, checkpointRows);
909
+ const existing = yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.threadId}/${checkpoint.throughSequence}`, checkpointRows);
918
910
  if (existing.length > 1) return yield* DoStorageCorruptionError.make({
919
911
  table: "effect_agent_checkpoints",
920
- rowKey: `${checkpoint.conversationId}/${checkpoint.throughSequence}`,
912
+ rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
921
913
  message: "A checkpoint primary key returned more than one row."
922
914
  });
923
915
  if (existing.length === 1) {
@@ -926,12 +918,12 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
926
918
  }
927
919
  yield* sql`
928
920
  INSERT INTO effect_agent_checkpoints (
929
- conversation_id,
921
+ thread_id,
930
922
  through_sequence,
931
923
  tail_digest,
932
924
  checkpoint_json
933
925
  ) VALUES (
934
- ${checkpoint.conversationId},
926
+ ${checkpoint.threadId},
935
927
  ${checkpoint.throughSequence},
936
928
  ${checkpoint.tailDigest},
937
929
  ${checkpoint.checkpointJson}
@@ -939,34 +931,34 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
939
931
  `.pipe(Effect.mapError(storageError("insert checkpoint")));
940
932
  }));
941
933
  });
942
- const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (conversationId, atOrBeforeSequence) {
934
+ const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (threadId, atOrBeforeSequence) {
943
935
  const rows = yield* sql`
944
936
  SELECT
945
- conversation_id,
937
+ thread_id,
946
938
  through_sequence,
947
939
  tail_digest,
948
940
  checkpoint_json
949
941
  FROM effect_agent_checkpoints
950
- WHERE conversation_id = ${conversationId}
942
+ WHERE thread_id = ${threadId}
951
943
  AND through_sequence <= ${atOrBeforeSequence}
952
944
  ORDER BY through_sequence DESC
953
945
  LIMIT 1
954
946
  `.pipe(Effect.mapError(storageError("load checkpoint")));
955
- return yield* decodeRows$1(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${conversationId}<=${atOrBeforeSequence}`, rows);
947
+ return yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${threadId}<=${atOrBeforeSequence}`, rows);
956
948
  });
957
949
  return {
958
950
  append,
959
951
  checkValueBound,
960
- exportConversation,
961
- getConversation,
962
- getTailDigestAt: Effect.fn("DoJournal.getTailDigestAt")(function* (conversationId, sequence) {
952
+ exportThread,
953
+ getThread,
954
+ getTailDigestAt: Effect.fn("DoJournal.getTailDigestAt")(function* (threadId, sequence) {
963
955
  if (sequence === 0) {
964
- const conversations = yield* getConversation(conversationId);
965
- return conversations.length === 0 ? [] : [conversations[0].tail_sequence === 0 ? conversations[0].tail_digest : void 0].filter((value) => value !== void 0);
956
+ const threads = yield* getThread(threadId);
957
+ return threads.length === 0 ? [] : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : void 0].filter((value) => value !== void 0);
966
958
  }
967
959
  const rows = yield* sql`
968
960
  SELECT
969
- conversation_id,
961
+ thread_id,
970
962
  batch_id,
971
963
  first_sequence,
972
964
  last_sequence,
@@ -974,10 +966,10 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
974
966
  tail_digest,
975
967
  batch_json
976
968
  FROM effect_agent_canonical_batches
977
- WHERE conversation_id = ${conversationId}
969
+ WHERE thread_id = ${threadId}
978
970
  AND last_sequence = ${sequence}
979
971
  `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
980
- return (yield* decodeRows$1(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${conversationId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
972
+ return (yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${threadId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
981
973
  }),
982
974
  loadCheckpoint,
983
975
  materialize,
@@ -985,19 +977,19 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
985
977
  saveCheckpoint,
986
978
  scanStoredPayloads: Effect.fn("DoJournal.scanStoredPayloads")(function* () {
987
979
  return yield* sql.withTransaction(Effect.gen(function* () {
988
- const conversations = yield* sql`
980
+ const threads = yield* sql`
989
981
  SELECT
990
- conversation_id,
982
+ thread_id,
991
983
  created_at,
992
984
  tail_sequence,
993
985
  tail_digest,
994
986
  producer_epoch
995
- FROM effect_agent_conversations
996
- ORDER BY conversation_id
997
- `.pipe(Effect.mapError(storageError("scan conversations")));
987
+ FROM effect_agent_threads
988
+ ORDER BY thread_id
989
+ `.pipe(Effect.mapError(storageError("scan threads")));
998
990
  const batches = yield* sql`
999
991
  SELECT
1000
- conversation_id,
992
+ thread_id,
1001
993
  batch_id,
1002
994
  first_sequence,
1003
995
  last_sequence,
@@ -1005,32 +997,32 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
1005
997
  tail_digest,
1006
998
  batch_json
1007
999
  FROM effect_agent_canonical_batches
1008
- ORDER BY conversation_id, first_sequence
1000
+ ORDER BY thread_id, first_sequence
1009
1001
  `.pipe(Effect.mapError(storageError("scan canonical batches")));
1010
1002
  const records = yield* sql`
1011
1003
  SELECT
1012
- conversation_id,
1004
+ thread_id,
1013
1005
  sequence,
1014
1006
  record_id,
1015
1007
  batch_id,
1016
1008
  record_json
1017
1009
  FROM effect_agent_canonical_records
1018
- ORDER BY conversation_id, sequence
1010
+ ORDER BY thread_id, sequence
1019
1011
  `.pipe(Effect.mapError(storageError("scan canonical records")));
1020
1012
  const checkpoints = yield* sql`
1021
1013
  SELECT
1022
- conversation_id,
1014
+ thread_id,
1023
1015
  through_sequence,
1024
1016
  tail_digest,
1025
1017
  checkpoint_json
1026
1018
  FROM effect_agent_checkpoints
1027
- ORDER BY conversation_id, through_sequence
1019
+ ORDER BY thread_id, through_sequence
1028
1020
  `.pipe(Effect.mapError(storageError("scan checkpoints")));
1029
1021
  return {
1030
- conversations: yield* decodeRows$1(Schema.Array(ConversationRow), "effect_agent_conversations", "startup_scan", conversations),
1031
- batches: yield* decodeRows$1(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
1032
- records: yield* decodeRows$1(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
1033
- checkpoints: yield* decodeRows$1(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
1022
+ threads: yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", "startup_scan", threads),
1023
+ batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
1024
+ records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
1025
+ checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
1034
1026
  };
1035
1027
  })).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(storageError("startup scan transaction")(error))));
1036
1028
  }),
@@ -1081,78 +1073,44 @@ var DoStorageConfigValue = class extends Schema.Class("@effect-agent/storage-clo
1081
1073
  /** Explicit Durable Object storage configuration authority. */
1082
1074
  var DoStorageConfig = class extends Context.Service()("@effect-agent/storage-cloudflare/DoStorageConfig") {};
1083
1075
  //#endregion
1084
- //#region src/do-storage-failpoint.ts
1085
- const noFailpoint = () => Effect.void;
1086
- /** Test-only control for replacing the active Durable Object failpoint handler. */
1087
- var DoStorageFailpointTestControl = class extends Context.Service()("@effect-agent/storage-cloudflare/DoStorageFailpointTestControl") {};
1088
- /** Explicit fault-injection authority used at Durable Object storage operation boundaries. */
1089
- var DoStorageFailpoint = class DoStorageFailpoint extends Context.Service()("@effect-agent/storage-cloudflare/DoStorageFailpoint") {
1090
- /** Production default: no fault injection. */
1091
- static layer = Layer.succeed(this)({ hit: noFailpoint });
1092
- /** Reusable test Layer with a control service backed by the same handler Ref. */
1093
- static layerTest = Layer.effectContext(Effect.gen(function* () {
1094
- const handler = yield* Ref.make(noFailpoint);
1095
- return Context.make(DoStorageFailpoint, DoStorageFailpoint.of({ hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))) })).pipe(Context.add(DoStorageFailpointTestControl, DoStorageFailpointTestControl.of({
1096
- clear: Ref.set(handler, noFailpoint),
1097
- setHandler: (next) => Ref.set(handler, next)
1098
- })));
1099
- }));
1100
- };
1101
- /**
1102
- * The DC-specific eviction failpoint mode: instead of failing typed, an armed hit evicts the
1103
- * Durable Object through an injected `evict` thunk — in production-shaped harnesses that thunk
1104
- * is `() => ctx.abort()`, the platform's real failure mode. `ctx.abort()` never returns (it
1105
- * throws while destroying the in-memory instance and every in-flight implicit or explicit
1106
- * storage transaction rolls back), so an armed hit ends the current Attempt exactly like an
1107
- * unannounced platform eviction; DO storage — the only correctness-critical state — survives
1108
- * for the next incarnation, which the persisted alarm wakes without any incoming request.
1109
- *
1110
- * The handles stay injected: this package never imports `cloudflare:workers`, so the harness
1111
- * that owns a `DurableObjectState` supplies the thunk.
1112
- */
1113
- const evictionFailpointHandler = (options) => (location) => options.isArmed(location).pipe(Effect.flatMap((armed) => armed ? Effect.sync(() => {
1114
- options.evict(location);
1115
- throw new Error(`Durable Object eviction did not interrupt execution at ${location}.`);
1116
- }) : Effect.void));
1117
- //#endregion
1118
- //#region src/do-conversation-store.ts
1119
- const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
1076
+ //#region src/do-thread-store.ts
1077
+ const OffsetText = Schema.String.check(Schema.isMaxLength(4096));
1120
1078
  const DO_OFFSET_PREFIX = "effect-agent-do@1:";
1121
1079
  const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
1122
1080
  const isDigest = Schema.is(Digest);
1123
1081
  const isDoFenceRejected = Schema.is(DoFenceRejected);
1124
1082
  const isDoAppendConflict = Schema.is(DoAppendConflict);
1125
1083
  const isDoCheckpointConflict = Schema.is(DoCheckpointConflict);
1126
- const storeError = (operation, error) => ConversationStoreError.make({
1084
+ const storeError = (operation, error) => ThreadStoreError.make({
1127
1085
  cause: error,
1128
1086
  operation,
1129
1087
  message: error.message
1130
1088
  });
1131
- const schemaStoreError = (operation, error) => ConversationStoreError.make({
1089
+ const schemaStoreError = (operation, error) => ThreadStoreError.make({
1132
1090
  cause: error,
1133
1091
  operation,
1134
1092
  message: error.message
1135
1093
  });
1136
- const makeOffset = Effect.fn(function* (conversationId, sequence) {
1137
- 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)));
1094
+ const makeOffset = Effect.fn(function* (threadId, sequence) {
1095
+ return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.flatMap((validatedSequence) => Schema.decodeUnknownEffect(ObservationOffset)(`${DO_OFFSET_PREFIX}${encodeURIComponent(threadId)}:${validatedSequence}`)), Effect.mapError((error) => schemaStoreError("encode observation offset", error)));
1138
1096
  });
1139
- const parseOffset = Effect.fn(function* (conversationId, offset) {
1097
+ const parseOffset = Effect.fn(function* (threadId, offset) {
1140
1098
  if (offset === void 0) return ZERO_CANONICAL_SEQUENCE;
1141
1099
  const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
1142
- const conversationPrefix = `${DO_OFFSET_PREFIX}${encodeURIComponent(conversationId)}:`;
1143
- if (!text.startsWith(conversationPrefix)) return yield* ConversationStoreError.make({
1100
+ const threadPrefix = `${DO_OFFSET_PREFIX}${encodeURIComponent(threadId)}:`;
1101
+ if (!text.startsWith(threadPrefix)) return yield* ThreadStoreError.make({
1144
1102
  operation: "decode observation offset",
1145
- message: "The observation offset belongs to a different adapter, storage version, or Conversation."
1103
+ message: "The observation offset belongs to a different adapter, storage version, or Thread."
1146
1104
  });
1147
- const sequenceText = text.slice(conversationPrefix.length);
1148
- if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield* ConversationStoreError.make({
1105
+ const sequenceText = text.slice(threadPrefix.length);
1106
+ if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield* ThreadStoreError.make({
1149
1107
  operation: "decode observation offset",
1150
1108
  message: "The observation offset is malformed."
1151
1109
  });
1152
1110
  return yield* Schema.decodeUnknownEffect(CanonicalSequence)(Number(sequenceText)).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
1153
1111
  });
1154
- const mapFence = (conversationId, error) => FenceRejected.make({
1155
- conversationId,
1112
+ const mapFence = (threadId, error) => FenceRejected.make({
1113
+ threadId,
1156
1114
  actualEpoch: error.actualEpoch,
1157
1115
  attemptedEpoch: error.producerEpoch
1158
1116
  });
@@ -1163,18 +1121,18 @@ const encodeCanonicalBatch = Effect.fn(function* (batch) {
1163
1121
  return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalBatch))(batch).pipe(Effect.mapError((error) => schemaStoreError("encode canonical batch", error)));
1164
1122
  });
1165
1123
  const encodeCheckpoint = Effect.fn(function* (checkpoint) {
1166
- return yield* Schema.encodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpoint).pipe(Effect.mapError((error) => schemaStoreError("encode checkpoint", error)));
1124
+ return yield* Schema.encodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint).pipe(Effect.mapError((error) => schemaStoreError("encode checkpoint", error)));
1167
1125
  });
1168
1126
  const decodeEnvelope = Effect.fn(function* (row) {
1169
- const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) => ConversationStoreError.make({
1127
+ const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) => ThreadStoreError.make({
1170
1128
  operation: "decode canonical record",
1171
1129
  message: error.message
1172
1130
  })));
1173
- const conversationId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.conversationId)(row.conversation_id).pipe(Effect.mapError((error) => schemaStoreError("decode conversation identity", error)));
1174
- const offset = yield* makeOffset(conversationId, row.sequence);
1131
+ const threadId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.threadId)(row.thread_id).pipe(Effect.mapError((error) => schemaStoreError("decode thread identity", error)));
1132
+ const offset = yield* makeOffset(threadId, row.sequence);
1175
1133
  const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(row.batch_id).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
1176
1134
  return CanonicalRecordEnvelope.make({
1177
- conversationId,
1135
+ threadId,
1178
1136
  batchId,
1179
1137
  sequence: row.sequence,
1180
1138
  offset,
@@ -1182,18 +1140,18 @@ const decodeEnvelope = Effect.fn(function* (row) {
1182
1140
  });
1183
1141
  });
1184
1142
  const decodeCheckpoint = Effect.fn(function* (checkpointJson) {
1185
- return yield* Schema.decodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpointJson).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint", error)));
1143
+ return yield* Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpointJson).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint", error)));
1186
1144
  });
1187
- const requireConversation = Effect.fn("DoConversationStore.requireConversation")(function* (journal, conversationId) {
1188
- const rows = yield* journal.getConversation(conversationId).pipe(Effect.mapError((error) => storeError("read conversation", error)));
1189
- if (rows.length === 0) return yield* ConversationNotMaterialized.make({ conversationId });
1145
+ const requireThread = Effect.fn("DoThreadStore.requireThread")(function* (journal, threadId) {
1146
+ const rows = yield* journal.getThread(threadId).pipe(Effect.mapError((error) => storeError("read thread", error)));
1147
+ if (rows.length === 0) return yield* ThreadNotMaterialized.make({ threadId });
1190
1148
  return rows[0];
1191
1149
  });
1192
- const tailDigestAt = Effect.fn("DoConversationStore.tailDigestAt")(function* (journal, conversationId, sequence) {
1150
+ const tailDigestAt = Effect.fn("DoThreadStore.tailDigestAt")(function* (journal, threadId, sequence) {
1193
1151
  if (sequence === 0) return EMPTY_TAIL_DIGEST;
1194
- const digests = yield* journal.getTailDigestAt(conversationId, sequence).pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
1152
+ const digests = yield* journal.getTailDigestAt(threadId, sequence).pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
1195
1153
  if (digests.length !== 1) return yield* CheckpointRejected.make({
1196
- conversationId,
1154
+ threadId,
1197
1155
  reason: "digest-mismatch"
1198
1156
  });
1199
1157
  return yield* Schema.decodeUnknownEffect(Digest)(digests[0]).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)));
@@ -1212,14 +1170,14 @@ const groupByKey = (rows, key) => {
1212
1170
  * and re-digested against the canonical chain. Routine opens skip this scan: per-operation
1213
1171
  * Schema decoding plus the digest chain already fail clearly on corrupt rows.
1214
1172
  */
1215
- const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloads")(function* (journal, crypto) {
1173
+ const decodeStartupPayloads = Effect.fn("DoThreadStore.decodeStartupPayloads")(function* (journal, crypto) {
1216
1174
  const stored = yield* journal.scanStoredPayloads();
1217
1175
  const batches = yield* Effect.forEach(stored.batches, (batch) => Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(Effect.map((decoded) => ({
1218
1176
  decoded,
1219
1177
  row: batch
1220
1178
  })), Effect.mapError((error) => DoStorageCorruptionError.make({
1221
1179
  table: "effect_agent_canonical_batches",
1222
- rowKey: `${batch.conversation_id}/${batch.batch_id}`,
1180
+ rowKey: `${batch.thread_id}/${batch.batch_id}`,
1223
1181
  message: error.message
1224
1182
  }))));
1225
1183
  const records = yield* Effect.forEach(stored.records, (record) => Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(Effect.map((decoded) => ({
@@ -1227,31 +1185,31 @@ const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloa
1227
1185
  row: record
1228
1186
  })), Effect.mapError((error) => DoStorageCorruptionError.make({
1229
1187
  table: "effect_agent_canonical_records",
1230
- rowKey: `${record.conversation_id}/${record.sequence}`,
1188
+ rowKey: `${record.thread_id}/${record.sequence}`,
1231
1189
  message: error.message
1232
1190
  }))));
1233
- const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpoint.checkpoint_json).pipe(Effect.map((decoded) => ({
1191
+ const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(Effect.map((decoded) => ({
1234
1192
  decoded,
1235
1193
  row: checkpoint
1236
1194
  })), Effect.mapError((error) => DoStorageCorruptionError.make({
1237
1195
  table: "effect_agent_checkpoints",
1238
- rowKey: `${checkpoint.conversation_id}/${checkpoint.through_sequence}`,
1196
+ rowKey: `${checkpoint.thread_id}/${checkpoint.through_sequence}`,
1239
1197
  message: error.message
1240
1198
  }))));
1241
- const batchesByConversation = groupByKey(batches, ({ row }) => row.conversation_id);
1242
- const recordsByConversation = groupByKey(records, ({ row }) => row.conversation_id);
1243
- const checkpointsByConversation = groupByKey(checkpoints, ({ row }) => row.conversation_id);
1244
- const materializedIds = new Set(stored.conversations.map((conversation) => conversation.conversation_id));
1245
- for (const conversation of stored.conversations) {
1246
- const conversationBatches = batchesByConversation.get(conversation.conversation_id) ?? [];
1247
- const conversationRecords = recordsByConversation.get(conversation.conversation_id) ?? [];
1248
- const conversationCheckpoints = checkpointsByConversation.get(conversation.conversation_id) ?? [];
1249
- const recordsByBatch = groupByKey(conversationRecords, ({ row }) => row.batch_id);
1199
+ const batchesByThread = groupByKey(batches, ({ row }) => row.thread_id);
1200
+ const recordsByThread = groupByKey(records, ({ row }) => row.thread_id);
1201
+ const checkpointsByThread = groupByKey(checkpoints, ({ row }) => row.thread_id);
1202
+ const materializedIds = new Set(stored.threads.map((thread) => thread.thread_id));
1203
+ for (const thread of stored.threads) {
1204
+ const threadBatches = batchesByThread.get(thread.thread_id) ?? [];
1205
+ const threadRecords = recordsByThread.get(thread.thread_id) ?? [];
1206
+ const threadCheckpoints = checkpointsByThread.get(thread.thread_id) ?? [];
1207
+ const recordsByBatch = groupByKey(threadRecords, ({ row }) => row.batch_id);
1250
1208
  let previousDigest = EMPTY_TAIL_DIGEST;
1251
1209
  let expectedSequence = 1;
1252
1210
  const tailDigests = /* @__PURE__ */ new Map([[0, EMPTY_TAIL_DIGEST]]);
1253
- for (const { decoded: canonicalBatch, row: batchRow } of conversationBatches) {
1254
- const key = `${batchRow.conversation_id}/${batchRow.batch_id}`;
1211
+ for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
1212
+ const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
1255
1213
  if (canonicalBatch.batchId !== batchRow.batch_id || batchRow.first_sequence !== expectedSequence || batchRow.last_sequence !== batchRow.first_sequence + canonicalBatch.records.length - 1) return yield* DoStorageCorruptionError.make({
1256
1214
  table: "effect_agent_canonical_batches",
1257
1215
  rowKey: key,
@@ -1296,24 +1254,24 @@ const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloa
1296
1254
  expectedSequence = batchRow.last_sequence + 1;
1297
1255
  tailDigests.set(batchRow.last_sequence, digest);
1298
1256
  }
1299
- if (conversationRecords.length !== conversation.tail_sequence || conversation.tail_sequence !== expectedSequence - 1 || conversation.tail_digest !== previousDigest) return yield* DoStorageCorruptionError.make({
1300
- table: "effect_agent_conversations",
1301
- rowKey: conversation.conversation_id,
1302
- message: "Conversation tail does not match its canonical batch chain."
1257
+ if (threadRecords.length !== thread.tail_sequence || thread.tail_sequence !== expectedSequence - 1 || thread.tail_digest !== previousDigest) return yield* DoStorageCorruptionError.make({
1258
+ table: "effect_agent_threads",
1259
+ rowKey: thread.thread_id,
1260
+ message: "Thread tail does not match its canonical batch chain."
1303
1261
  });
1304
- 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({
1262
+ for (const checkpoint of threadCheckpoints) if (checkpoint.decoded.threadId !== thread.thread_id || checkpoint.decoded.throughSequence !== checkpoint.row.through_sequence || checkpoint.decoded.tailDigest !== checkpoint.row.tail_digest || tailDigests.get(checkpoint.row.through_sequence) !== checkpoint.row.tail_digest) return yield* DoStorageCorruptionError.make({
1305
1263
  table: "effect_agent_checkpoints",
1306
- rowKey: `${conversation.conversation_id}/${checkpoint.row.through_sequence}`,
1264
+ rowKey: `${thread.thread_id}/${checkpoint.row.through_sequence}`,
1307
1265
  message: "Checkpoint identity or digest is not bound to a canonical batch tail."
1308
1266
  });
1309
1267
  }
1310
- 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({
1311
- table: "effect_agent_conversations",
1268
+ if (batches.some(({ row }) => !materializedIds.has(row.thread_id)) || records.some(({ row }) => !materializedIds.has(row.thread_id)) || checkpoints.some(({ row }) => !materializedIds.has(row.thread_id))) return yield* DoStorageCorruptionError.make({
1269
+ table: "effect_agent_threads",
1312
1270
  rowKey: "startup_scan",
1313
- message: "Canonical rows exist without a materialized Conversation."
1271
+ message: "Canonical rows exist without a materialized Thread."
1314
1272
  });
1315
1273
  });
1316
- const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* () {
1274
+ const makeServices$2 = Effect.fn("DoThreadStore.makeServices")(function* () {
1317
1275
  const config = yield* DoStorageConfig;
1318
1276
  const failpoint = yield* DoStorageFailpoint;
1319
1277
  const sql = yield* SqlClient.SqlClient;
@@ -1322,16 +1280,16 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
1322
1280
  if (config.verifyOnOpen) yield* decodeStartupPayloads(journal, crypto);
1323
1281
  const provideCrypto = (effect) => Effect.provideService(effect, Crypto.Crypto, crypto);
1324
1282
  const hitFailpoint = Effect.fn((location) => failpoint.hit(location).pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))));
1325
- const materialize = Effect.fn("DoConversationStore.materialize")(function* (request) {
1326
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationMaterialization))(request).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
1283
+ const materialize = Effect.fn("DoThreadStore.materialize")(function* (request) {
1284
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadMaterialization))(request).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
1327
1285
  const now = yield* Clock.currentTimeMillis;
1328
1286
  yield* hitFailpoint("materialize:before");
1329
- 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)));
1287
+ yield* journal.materialize(validated.threadId, new Date(now).toISOString(), EMPTY_TAIL_DIGEST, validated.producerEpoch).pipe(Effect.mapError((error) => error._tag === "DoFenceRejected" ? mapFence(validated.threadId, error) : storeError("materialize thread", error)));
1330
1288
  yield* hitFailpoint("materialize:after");
1331
1289
  });
1332
- const append = Effect.fn("DoConversationStore.append")(function* (request) {
1290
+ const append = Effect.fn("DoThreadStore.append")(function* (request) {
1333
1291
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
1334
- yield* requireConversation(journal, validated.conversationId);
1292
+ yield* requireThread(journal, validated.threadId);
1335
1293
  const tailDigest = yield* provideCrypto(digestCanonicalBatch(validated.expectedTailDigest, validated.batch)).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
1336
1294
  const batchJson = yield* encodeCanonicalBatch(validated.batch);
1337
1295
  const rawRecords = yield* Effect.forEach(validated.batch.records, (record) => encodeCanonicalRecord(record).pipe(Effect.map((recordJson) => ({
@@ -1339,7 +1297,7 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
1339
1297
  recordJson
1340
1298
  }))));
1341
1299
  const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
1342
- conversationId: validated.conversationId,
1300
+ threadId: validated.threadId,
1343
1301
  batchId: validated.batch.batchId,
1344
1302
  batchDigest: tailDigest,
1345
1303
  batchJson,
@@ -1351,15 +1309,15 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
1351
1309
  }).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
1352
1310
  yield* hitFailpoint("append:before");
1353
1311
  const result = yield* journal.append(rawRequest).pipe(Effect.mapError((error) => {
1354
- if (isDoFenceRejected(error)) return mapFence(validated.conversationId, error);
1312
+ if (isDoFenceRejected(error)) return mapFence(validated.threadId, error);
1355
1313
  if (isDoAppendConflict(error)) return error.actualTailSequence !== void 0 && isDigest(error.actualTailDigest) ? AppendConflict.make({
1356
- conversationId: validated.conversationId,
1314
+ threadId: validated.threadId,
1357
1315
  batchId: validated.batch.batchId,
1358
1316
  reason: error.reason,
1359
1317
  actualTailSequence: error.actualTailSequence,
1360
1318
  actualTailDigest: error.actualTailDigest
1361
1319
  }) : AppendConflict.make({
1362
- conversationId: validated.conversationId,
1320
+ threadId: validated.threadId,
1363
1321
  batchId: validated.batch.batchId,
1364
1322
  reason: error.reason
1365
1323
  });
@@ -1368,30 +1326,30 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
1368
1326
  yield* hitFailpoint("append:after");
1369
1327
  return result;
1370
1328
  });
1371
- const loadRecords = Effect.fn("DoConversationStore.loadRecords")(function* (request) {
1329
+ const loadRecords = Effect.fn("DoThreadStore.loadRecords")(function* (request) {
1372
1330
  const rows = yield* journal.read(request).pipe(Effect.mapError((error) => storeError("read canonical records", error)));
1373
1331
  return yield* Effect.forEach(rows, decodeEnvelope);
1374
1332
  });
1375
- const readEffect = Effect.fn("DoConversationStore.read")(function* (request) {
1376
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationRead))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation read", error)));
1377
- yield* requireConversation(journal, validated.conversationId);
1333
+ const readEffect = Effect.fn("DoThreadStore.read")(function* (request) {
1334
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadRead))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread read", error)));
1335
+ yield* requireThread(journal, validated.threadId);
1378
1336
  const records = yield* loadRecords(RawReadRequest.make({
1379
- conversationId: validated.conversationId,
1337
+ threadId: validated.threadId,
1380
1338
  fromSequenceExclusive: validated.afterSequence ?? ZERO_CANONICAL_SEQUENCE,
1381
1339
  limit: validated.limit
1382
1340
  }));
1383
1341
  return Stream.fromIterable(records);
1384
1342
  });
1385
1343
  const read = (request) => Stream.unwrap(readEffect(request));
1386
- const observeEffect = Effect.fn("DoConversationStore.observe")(function* (request) {
1387
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationObservation))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation observation", error)));
1388
- yield* requireConversation(journal, validated.conversationId);
1389
- const initialSequence = yield* parseOffset(validated.conversationId, validated.afterOffset);
1344
+ const observeEffect = Effect.fn("DoThreadStore.observe")(function* (request) {
1345
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadObservation))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread observation", error)));
1346
+ yield* requireThread(journal, validated.threadId);
1347
+ const initialSequence = yield* parseOffset(validated.threadId, validated.afterOffset);
1390
1348
  const cursor = yield* Ref.make(initialSequence);
1391
- const poll = Effect.fn("DoConversationStore.observePoll")(function* () {
1349
+ const poll = Effect.fn("DoThreadStore.observePoll")(function* () {
1392
1350
  const fromSequenceExclusive = yield* Ref.get(cursor);
1393
1351
  const records = yield* loadRecords(RawReadRequest.make({
1394
- conversationId: validated.conversationId,
1352
+ threadId: validated.threadId,
1395
1353
  fromSequenceExclusive,
1396
1354
  limit: 1024
1397
1355
  }));
@@ -1405,96 +1363,98 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
1405
1363
  return Stream.fromIterableEffectRepeat(poll());
1406
1364
  });
1407
1365
  const observe = (request) => Stream.unwrap(observeEffect(request));
1408
- const exportConversation = Effect.fn("DoConversationStore.export")(function* (request) {
1409
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationExportRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation export", error)));
1410
- yield* requireConversation(journal, validated.conversationId);
1411
- const exported = yield* journal.exportConversation(validated.conversationId).pipe(Effect.mapError((error) => storeError("export conversation", error)));
1366
+ const exportThread = Effect.fn("DoThreadStore.export")(function* (request) {
1367
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadExportRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread export", error)));
1368
+ yield* requireThread(journal, validated.threadId);
1369
+ const exported = yield* journal.exportThread(validated.threadId).pipe(Effect.mapError((error) => storeError("export thread", error)));
1412
1370
  const records = yield* Effect.forEach(exported.records, decodeEnvelope);
1413
- if (records.length > 65536) return yield* ConversationStoreError.make({
1414
- operation: "decode conversation export",
1415
- message: "The conversation exceeds the current export record limit."
1371
+ if (records.length > 65536) return yield* ThreadStoreError.make({
1372
+ operation: "decode thread export",
1373
+ message: "The thread exceeds the current export record limit."
1416
1374
  });
1417
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.conversation.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
1418
- return ConversationExport.make({
1419
- format: "effect-agent/conversation@1",
1420
- conversationId: validated.conversationId,
1421
- tailSequence: exported.conversation.tail_sequence,
1375
+ const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
1376
+ return ThreadExport.make({
1377
+ format: "effect-agent/thread@1",
1378
+ threadId: validated.threadId,
1379
+ tailSequence: exported.thread.tail_sequence,
1422
1380
  tailDigest,
1423
1381
  records
1424
1382
  });
1425
1383
  });
1426
- const inspectTail = Effect.fn("DoConversationStore.inspectTail")(function* (request) {
1427
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationTailRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
1428
- const conversation = yield* requireConversation(journal, validated.conversationId);
1429
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(conversation.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode tail digest", error)));
1430
- return ConversationTail.make({
1431
- conversationId: validated.conversationId,
1432
- tailSequence: conversation.tail_sequence,
1384
+ const inspectTail = Effect.fn("DoThreadStore.inspectTail")(function* (request) {
1385
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadTailRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
1386
+ const thread = yield* requireThread(journal, validated.threadId);
1387
+ const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode tail digest", error)));
1388
+ return ThreadTail.make({
1389
+ threadId: validated.threadId,
1390
+ tailSequence: thread.tail_sequence,
1433
1391
  tailDigest,
1434
- producerEpoch: conversation.producer_epoch
1392
+ producerEpoch: thread.producer_epoch
1435
1393
  });
1436
1394
  });
1437
- const saveCheckpoint = Effect.fn("DoConversationStore.saveCheckpoint")(function* (request) {
1395
+ const saveCheckpoint = Effect.fn("DoThreadStore.saveCheckpoint")(function* (request) {
1438
1396
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
1439
- const conversation = yield* requireConversation(journal, validated.checkpoint.conversationId);
1440
- if (validated.checkpoint.throughSequence > conversation.tail_sequence) return yield* CheckpointRejected.make({
1441
- conversationId: validated.checkpoint.conversationId,
1397
+ const thread = yield* requireThread(journal, validated.checkpoint.threadId);
1398
+ if (validated.checkpoint.throughSequence > thread.tail_sequence) return yield* CheckpointRejected.make({
1399
+ threadId: validated.checkpoint.threadId,
1442
1400
  reason: "ahead-of-tail"
1443
1401
  });
1444
- if ((yield* tailDigestAt(journal, validated.checkpoint.conversationId, validated.checkpoint.throughSequence)) !== validated.checkpoint.tailDigest) return yield* CheckpointRejected.make({
1445
- conversationId: validated.checkpoint.conversationId,
1402
+ if ((yield* tailDigestAt(journal, validated.checkpoint.threadId, validated.checkpoint.throughSequence)) !== validated.checkpoint.tailDigest) return yield* CheckpointRejected.make({
1403
+ threadId: validated.checkpoint.threadId,
1446
1404
  reason: "digest-mismatch"
1447
1405
  });
1448
1406
  const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
1449
1407
  const raw = RawCheckpoint.make({
1450
- conversationId: validated.checkpoint.conversationId,
1408
+ threadId: validated.checkpoint.threadId,
1451
1409
  throughSequence: validated.checkpoint.throughSequence,
1452
1410
  tailDigest: validated.checkpoint.tailDigest,
1453
1411
  checkpointJson
1454
1412
  });
1455
1413
  yield* hitFailpoint("save-checkpoint:before");
1456
1414
  yield* journal.saveCheckpoint(raw).pipe(Effect.mapError((error) => isDoCheckpointConflict(error) ? CheckpointRejected.make({
1457
- conversationId: validated.checkpoint.conversationId,
1415
+ threadId: validated.checkpoint.threadId,
1458
1416
  reason: "digest-mismatch"
1459
1417
  }) : storeError("save checkpoint", error)));
1460
1418
  yield* hitFailpoint("save-checkpoint:after");
1461
1419
  });
1462
- const loadCheckpoint = Effect.fn("DoConversationStore.loadCheckpoint")(function* (request) {
1420
+ const loadCheckpoint = Effect.fn("DoThreadStore.loadCheckpoint")(function* (request) {
1463
1421
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
1464
- const conversation = yield* requireConversation(journal, validated.conversationId);
1465
- const rows = yield* journal.loadCheckpoint(validated.conversationId, validated.atOrBeforeSequence ?? conversation.tail_sequence).pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
1422
+ const thread = yield* requireThread(journal, validated.threadId);
1423
+ const rows = yield* journal.loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence).pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
1466
1424
  if (rows.length === 0) return Option.none();
1467
- if (rows.length !== 1) return yield* ConversationStoreError.make({
1425
+ if (rows.length !== 1) return yield* ThreadStoreError.make({
1468
1426
  operation: "load checkpoint",
1469
1427
  message: `Expected at most one checkpoint row but found ${rows.length}.`
1470
1428
  });
1471
1429
  const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
1472
- if ((yield* tailDigestAt(journal, checkpoint.conversationId, checkpoint.throughSequence)) !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
1473
- conversationId: checkpoint.conversationId,
1430
+ if ((yield* tailDigestAt(journal, checkpoint.threadId, checkpoint.throughSequence)) !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
1431
+ threadId: checkpoint.threadId,
1474
1432
  reason: "digest-mismatch"
1475
1433
  });
1476
1434
  return Option.some(checkpoint);
1477
1435
  });
1478
- const conversationStore = ConversationStore.of({
1436
+ const threadStore = ThreadStore.of({
1479
1437
  append,
1480
- export: exportConversation,
1438
+ export: exportThread,
1481
1439
  inspectTail,
1482
- loadCheckpoint,
1483
1440
  materialize,
1484
1441
  observe,
1485
1442
  read,
1486
- saveCheckpoint
1443
+ checkpoints: {
1444
+ save: saveCheckpoint,
1445
+ load: loadCheckpoint
1446
+ }
1487
1447
  });
1488
- return Context.make(ConversationStore, conversationStore);
1448
+ return Context.make(ThreadStore, threadStore);
1489
1449
  });
1490
1450
  /**
1491
- * Durable Object Conversation Store implementation with configuration, failpoint, SQL, and
1451
+ * Durable Object Thread Store implementation with configuration, failpoint, SQL, and
1492
1452
  * Crypto authority kept visible in its input channel.
1493
1453
  */
1494
- const conversationStoreLayer = Layer.effectContext(makeServices$2());
1454
+ const threadStoreLayer = Layer.effectContext(makeServices$2());
1495
1455
  /**
1496
1456
  * Validated Durable Object storage configuration Layer with the documented defaults applied.
1497
- * Shared by the ConversationStore and SubmissionLedger convenience layers so their defaults
1457
+ * Shared by the ThreadStore and SubmissionLedger convenience layers so their defaults
1498
1458
  * cannot drift.
1499
1459
  */
1500
1460
  const storageConfigLayer = (options) => Layer.effect(DoStorageConfig)(Schema.decodeUnknownEffect(DoStorageConfigValue)({
@@ -1510,12 +1470,12 @@ const storageConfigLayer = (options) => Layer.effect(DoStorageConfig)(Schema.dec
1510
1470
  /** The failpoint Layer selected by convenience options: explicit handler or the no-op default. */
1511
1471
  const storageFailpointLayer = (options) => options.failpoint === void 0 ? DoStorageFailpoint.layer : Layer.succeed(DoStorageFailpoint)({ hit: options.failpoint });
1512
1472
  /**
1513
- * A composition-root convenience Layer for canonical Conversations inside one Durable Object,
1473
+ * A composition-root convenience Layer for canonical Threads inside one Durable Object,
1514
1474
  * built over `ctx.storage`. Durable accepted work is served by the separate SubmissionLedger
1515
1475
  * port; point both at the SAME `ctx.storage` so claims fence the same producer epochs
1516
1476
  * (ADR-0011 D7's "same file" rule, transposed to one object's private database).
1517
1477
  */
1518
- const layer = (options) => Layer.unwrap(Effect.map(DoStorageConfig, (config) => conversationStoreLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(DoStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ storage: options.storage }), BrowserCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
1478
+ const layer = (options) => Layer.unwrap(Effect.map(DoStorageConfig, (config) => threadStoreLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(DoStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ storage: options.storage }), BrowserCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
1519
1479
  /** Create an adapter-owned resumable observation offset for a known canonical sequence. */
1520
1480
  const observationOffsetAt = makeOffset;
1521
1481
  //#endregion
@@ -1537,7 +1497,7 @@ const WOKEN = "woken";
1537
1497
  const MAX_IDENTIFIER_LENGTH = 1024;
1538
1498
  var SubmissionRow = class extends Schema.Class("SubmissionRow")({
1539
1499
  submission_id: BoundedIdentifier,
1540
- conversation_id: BoundedIdentifier,
1500
+ thread_id: BoundedIdentifier,
1541
1501
  queue_sequence: QueueSequence,
1542
1502
  principal: BoundedIdentifier,
1543
1503
  idempotency_key: BoundedIdentifier,
@@ -1625,7 +1585,7 @@ var MaxQueueSequenceRow = class extends Schema.Class("MaxQueueSequenceRow")({ ma
1625
1585
  var CanonicalRecordIdRow = class extends Schema.Class("CanonicalRecordIdRow")({ record_id: BoundedIdentifier }) {};
1626
1586
  const SUBMISSION_COLUMNS = `
1627
1587
  submission_id,
1628
- conversation_id,
1588
+ thread_id,
1629
1589
  queue_sequence,
1630
1590
  principal,
1631
1591
  idempotency_key,
@@ -1662,7 +1622,7 @@ const CHILD_RESERVATION_COLUMNS = `
1662
1622
  release_began_at,
1663
1623
  released_at
1664
1624
  `;
1665
- /** The branded ToolCallId schema, reached through the session port so no core import is needed. */
1625
+ /** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
1666
1626
  const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
1667
1627
  const ToolCallIdList = Schema.Array(ToolCallIdSchema);
1668
1628
  const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
@@ -1736,7 +1696,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
1736
1696
  iso: new Date(millis).toISOString()
1737
1697
  }));
1738
1698
  const timestampMillis = (operation, rowKey) => (timestamp) => decodeUtcInstant(timestamp).pipe(Effect.map(DateTime.toEpochMillis), Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submission_ownership", rowKey, error.message)));
1739
- const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows$1(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1699
+ const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1740
1700
  const readSubmission = Effect.fn("DoSubmissionLedger.readSubmission")(function* (operation, submissionId) {
1741
1701
  const rows = yield* sql`
1742
1702
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
@@ -1767,23 +1727,23 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
1767
1727
  FROM effect_agent_submission_ownership
1768
1728
  WHERE submission_id = ${submissionId}
1769
1729
  `.pipe(Effect.mapError(sqlFailure(operation)));
1770
- const decoded = yield* decodeRows$1(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1730
+ const decoded = yield* decodeRows$2(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1771
1731
  if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submission_ownership", submissionId, "An ownership primary key returned more than one row.");
1772
1732
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1773
1733
  });
1774
- const conversationEpoch = Effect.fn("DoSubmissionLedger.conversationEpoch")(function* (operation, conversationId) {
1775
- const conversations = yield* journal.getConversation(conversationId).pipe(Effect.mapError(internalFailure(operation)));
1776
- return conversations.length === 0 ? EPOCH_ZERO : conversations[0].producer_epoch;
1734
+ const threadEpoch = Effect.fn("DoSubmissionLedger.threadEpoch")(function* (operation, threadId) {
1735
+ const threads = yield* journal.getThread(threadId).pipe(Effect.mapError(internalFailure(operation)));
1736
+ return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
1777
1737
  });
1778
1738
  /**
1779
1739
  * Verify inside the surrounding write transaction that the presented token still owns the
1780
1740
  * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
1781
- * Conversation's current producer epoch (DUR-006).
1741
+ * Thread's current producer epoch (DUR-006).
1782
1742
  */
1783
1743
  const requireOwnership = Effect.fn("DoSubmissionLedger.requireOwnership")(function* (operation, submission, ownershipToken) {
1784
1744
  const ownership = yield* readOwnership(operation, submission.submission_id);
1785
1745
  if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
1786
- const actualEpoch = yield* conversationEpoch(operation, submission.conversation_id);
1746
+ const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
1787
1747
  const submissionId = yield* Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId)(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
1788
1748
  return yield* OwnershipLost.make({
1789
1749
  submissionId,
@@ -1798,7 +1758,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
1798
1758
  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.");
1799
1759
  return yield* decodeSubmissionSnapshotUnknown({
1800
1760
  submissionId: row.submission_id,
1801
- conversationId: row.conversation_id,
1761
+ threadId: row.thread_id,
1802
1762
  queueSequence: row.queue_sequence,
1803
1763
  principal: row.principal,
1804
1764
  idempotencyKey: row.idempotency_key,
@@ -1832,7 +1792,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
1832
1792
  FROM effect_agent_settlement_reservations
1833
1793
  WHERE submission_id = ${submissionId}
1834
1794
  `.pipe(Effect.mapError(sqlFailure(operation)));
1835
- const decoded = yield* decodeRows$1(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1795
+ const decoded = yield* decodeRows$2(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1836
1796
  if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", submissionId, "A settlement reservation primary key returned more than one row.");
1837
1797
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1838
1798
  });
@@ -1847,7 +1807,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
1847
1807
  FROM effect_agent_abort_intents
1848
1808
  WHERE submission_id = ${submissionId}
1849
1809
  `.pipe(Effect.mapError(sqlFailure(operation)));
1850
- const decoded = yield* decodeRows$1(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1810
+ const decoded = yield* decodeRows$2(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1851
1811
  if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_abort_intents", submissionId, "An abort intent primary key returned more than one row.");
1852
1812
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1853
1813
  });
@@ -1867,7 +1827,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
1867
1827
  WHERE parent_submission_id = ${parentSubmissionId}
1868
1828
  ORDER BY child_submission_id ASC
1869
1829
  `.pipe(Effect.mapError(sqlFailure(operation)));
1870
- return yield* decodeRows$1(Schema.Array(ChildSettlementMarkerRow), "effect_agent_child_settlements", parentSubmissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1830
+ return yield* decodeRows$2(Schema.Array(ChildSettlementMarkerRow), "effect_agent_child_settlements", parentSubmissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1871
1831
  });
1872
1832
  /**
1873
1833
  * Whether one listed child is provably settled from THIS store: either its own row lives
@@ -1880,7 +1840,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
1880
1840
  const childRow = yield* readSubmission(operation, childSubmissionId);
1881
1841
  return Option.isSome(childRow) && childRow.value.state === "settled";
1882
1842
  });
1883
- const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$1(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1843
+ const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1884
1844
  const readChildReservation = Effect.fn("DoSubmissionLedger.readChildReservation")(function* (operation, reservationId) {
1885
1845
  const rows = yield* sql`
1886
1846
  SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
@@ -1933,7 +1893,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
1933
1893
  WHERE submission_id = ${submissionId}
1934
1894
  ORDER BY tool_call_id ASC
1935
1895
  `.pipe(Effect.mapError(sqlFailure(operation)));
1936
- return yield* decodeRows$1(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1896
+ return yield* decodeRows$2(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1937
1897
  });
1938
1898
  const approvalIntentFromRow = Effect.fn("DoSubmissionLedger.approvalIntentFromRow")(function* (operation, row) {
1939
1899
  return yield* decodeApprovalDecisionIntent({
@@ -1958,7 +1918,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
1958
1918
  WHERE submission_id = ${submissionId}
1959
1919
  ORDER BY tool_call_id ASC
1960
1920
  `.pipe(Effect.mapError(sqlFailure(operation)));
1961
- return yield* decodeRows$1(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1921
+ return yield* decodeRows$2(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1962
1922
  });
1963
1923
  const unknownResolutionIntentFromRow = Effect.fn("DoSubmissionLedger.unknownResolutionIntentFromRow")(function* (operation, row) {
1964
1924
  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)));
@@ -1981,18 +1941,18 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
1981
1941
  * derived from the shared canonical-records table using the deterministic abort record
1982
1942
  * identity, never from a cached ledger marker.
1983
1943
  */
1984
- const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (operation, conversationId, submissionId) {
1944
+ const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (operation, threadId, submissionId) {
1985
1945
  const recordId = submissionAbortRecordId(submissionId);
1986
1946
  const rows = yield* sql`
1987
1947
  SELECT record_id
1988
1948
  FROM effect_agent_canonical_records
1989
- WHERE conversation_id = ${conversationId}
1949
+ WHERE thread_id = ${threadId}
1990
1950
  AND record_id = ${recordId}
1991
1951
  `.pipe(Effect.mapError(sqlFailure(operation)));
1992
- return (yield* decodeRows$1(Schema.Array(CanonicalRecordIdRow), "effect_agent_canonical_records", `${conversationId}/${recordId}`, rows).pipe(Effect.mapError(internalFailure(operation)))).length === 0 ? void 0 : recordId;
1952
+ return (yield* decodeRows$2(Schema.Array(CanonicalRecordIdRow), "effect_agent_canonical_records", `${threadId}/${recordId}`, rows).pipe(Effect.mapError(internalFailure(operation)))).length === 0 ? void 0 : recordId;
1993
1953
  });
1994
1954
  const abortIntentFromRow = Effect.fn("DoSubmissionLedger.abortIntentFromRow")(function* (operation, submission, submissionId, row) {
1995
- const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.conversation_id, submissionId);
1955
+ const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, submissionId);
1996
1956
  return yield* decodeAbortIntent({
1997
1957
  submissionId: row.submission_id,
1998
1958
  author: row.author,
@@ -2008,19 +1968,19 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2008
1968
  const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(Effect.mapError(internalFailure(operation)));
2009
1969
  yield* journal.checkValueBound(operation, inputJson).pipe(Effect.mapError(internalFailure(operation)));
2010
1970
  const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(Effect.mapError(internalFailure(operation)));
2011
- const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.conversationId}`;
1971
+ const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.threadId}`;
2012
1972
  if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) return yield* LedgerError.make({
2013
1973
  operation,
2014
- message: `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Conversation identity.`
1974
+ message: `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Thread identity.`
2015
1975
  });
2016
1976
  const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
2017
1977
  yield* hitFailpoint("ledger:admit:before", operation);
2018
1978
  const result = yield* inWriteTransaction(operation, Effect.gen(function* () {
2019
- const keyRowKey = `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`;
1979
+ const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
2020
1980
  const existingRows = yield* sql`
2021
1981
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2022
1982
  FROM effect_agent_submissions
2023
- WHERE conversation_id = ${validated.conversationId}
1983
+ WHERE thread_id = ${validated.threadId}
2024
1984
  AND principal = ${validated.principal}
2025
1985
  AND idempotency_key = ${validated.idempotencyKey}
2026
1986
  `.pipe(Effect.mapError(sqlFailure(operation)));
@@ -2029,7 +1989,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2029
1989
  if (existing.length === 1) {
2030
1990
  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;
2031
1991
  if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) return yield* AdmissionConflict.make({
2032
- conversationId: validated.conversationId,
1992
+ threadId: validated.threadId,
2033
1993
  principal: validated.principal,
2034
1994
  idempotencyKey: validated.idempotencyKey,
2035
1995
  existingInputDigest: existing[0].input_digest,
@@ -2046,15 +2006,15 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2046
2006
  const maxRows = yield* sql`
2047
2007
  SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
2048
2008
  FROM effect_agent_submissions
2049
- WHERE conversation_id = ${validated.conversationId}
2009
+ WHERE thread_id = ${validated.threadId}
2050
2010
  `.pipe(Effect.mapError(sqlFailure(operation)));
2051
- const decodedMax = yield* decodeRows$1(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.conversationId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
2011
+ const decodedMax = yield* decodeRows$2(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.threadId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
2052
2012
  const queueSequence = yield* decodeQueueSequence((decodedMax[0]?.max_queue_sequence ?? 0) + 1).pipe(Effect.mapError(internalFailure(operation)));
2053
2013
  const now = yield* currentInstant;
2054
2014
  yield* sql`
2055
2015
  INSERT INTO effect_agent_submissions (
2056
2016
  submission_id,
2057
- conversation_id,
2017
+ thread_id,
2058
2018
  queue_sequence,
2059
2019
  principal,
2060
2020
  idempotency_key,
@@ -2070,7 +2030,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2070
2030
  parent_tool_call_id
2071
2031
  ) VALUES (
2072
2032
  ${mintedSubmissionId},
2073
- ${validated.conversationId},
2033
+ ${validated.threadId},
2074
2034
  ${queueSequence},
2075
2035
  ${validated.principal},
2076
2036
  ${validated.idempotencyKey},
@@ -2123,12 +2083,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2123
2083
  const rows = yield* sql`
2124
2084
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2125
2085
  FROM effect_agent_submissions
2126
- WHERE conversation_id = ${validated.conversationId}
2086
+ WHERE thread_id = ${validated.threadId}
2127
2087
  AND principal = ${validated.principal}
2128
2088
  AND idempotency_key = ${validated.idempotencyKey}
2129
2089
  `.pipe(Effect.mapError(sqlFailure(operation)));
2130
- const decoded = yield* decodeSubmissionRows(operation, `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2131
- 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.");
2090
+ const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2091
+ if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, "An admission idempotency key returned more than one row.");
2132
2092
  if (decoded.length === 0) return Option.none();
2133
2093
  return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
2134
2094
  });
@@ -2138,12 +2098,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2138
2098
  const rows = yield* sql`
2139
2099
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2140
2100
  FROM effect_agent_submissions
2141
- WHERE conversation_id = ${validated.conversationId}
2101
+ WHERE thread_id = ${validated.threadId}
2142
2102
  AND principal = ${validated.principal}
2143
2103
  AND idempotency_key = ${validated.idempotencyKey}
2144
2104
  `.pipe(Effect.mapError(sqlFailure(operation)));
2145
- const decoded = yield* decodeSubmissionRows(operation, `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2146
- 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.");
2105
+ const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2106
+ if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, "An admission idempotency key returned more than one row.");
2147
2107
  if (decoded.length === 0) return AdmissionNotAdmitted.make();
2148
2108
  return AdmissionAdmitted.make({ submission: yield* decodeSubmissionSnapshot(operation, decoded[0]) });
2149
2109
  });
@@ -2157,12 +2117,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2157
2117
  const headRows = yield* sql`
2158
2118
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2159
2119
  FROM effect_agent_submissions
2160
- WHERE conversation_id = ${validated.conversationId}
2120
+ WHERE thread_id = ${validated.threadId}
2161
2121
  AND state <> 'settled'
2162
2122
  ORDER BY queue_sequence ASC
2163
2123
  LIMIT 1
2164
2124
  `.pipe(Effect.mapError(sqlFailure(operation)));
2165
- const heads = yield* decodeSubmissionRows(operation, validated.conversationId, headRows);
2125
+ const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
2166
2126
  if (heads.length === 0) return Option.none();
2167
2127
  const head = heads[0];
2168
2128
  if (head.state === "joining" || head.state === "joined" || head.state === "suspended" || head.state === "unknown" && Option.isNone(yield* readAbortIntent(operation, head.submission_id))) return Option.none();
@@ -2171,19 +2131,19 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2171
2131
  if (Option.isSome(ownership)) {
2172
2132
  if ((yield* timestampMillis(operation, head.submission_id)(ownership.value.lease_expires_at)) > now.millis) return Option.none();
2173
2133
  }
2174
- const conversations = yield* journal.getConversation(head.conversation_id).pipe(Effect.mapError(internalFailure(operation)));
2134
+ const threads = yield* journal.getThread(head.thread_id).pipe(Effect.mapError(internalFailure(operation)));
2175
2135
  let producerEpoch;
2176
- if (conversations.length === 0) {
2136
+ if (threads.length === 0) {
2177
2137
  producerEpoch = 1;
2178
2138
  yield* sql`
2179
- INSERT INTO effect_agent_conversations (
2180
- conversation_id,
2139
+ INSERT INTO effect_agent_threads (
2140
+ thread_id,
2181
2141
  created_at,
2182
2142
  tail_sequence,
2183
2143
  tail_digest,
2184
2144
  producer_epoch
2185
2145
  ) VALUES (
2186
- ${head.conversation_id},
2146
+ ${head.thread_id},
2187
2147
  ${now.iso},
2188
2148
  0,
2189
2149
  ${EMPTY_TAIL_DIGEST},
@@ -2191,11 +2151,11 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2191
2151
  )
2192
2152
  `.pipe(Effect.mapError(sqlFailure(operation)));
2193
2153
  } else {
2194
- producerEpoch = conversations[0].producer_epoch + 1;
2154
+ producerEpoch = threads[0].producer_epoch + 1;
2195
2155
  yield* sql`
2196
- UPDATE effect_agent_conversations
2156
+ UPDATE effect_agent_threads
2197
2157
  SET producer_epoch = ${producerEpoch}
2198
- WHERE conversation_id = ${head.conversation_id}
2158
+ WHERE thread_id = ${head.thread_id}
2199
2159
  `.pipe(Effect.mapError(sqlFailure(operation)));
2200
2160
  }
2201
2161
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
@@ -2226,14 +2186,14 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2226
2186
  INSERT INTO effect_agent_attempts (
2227
2187
  attempt_id,
2228
2188
  submission_id,
2229
- conversation_id,
2189
+ thread_id,
2230
2190
  owner_producer_id,
2231
2191
  producer_epoch,
2232
2192
  claimed_at
2233
2193
  ) VALUES (
2234
2194
  ${attemptId},
2235
2195
  ${head.submission_id},
2236
- ${head.conversation_id},
2196
+ ${head.thread_id},
2237
2197
  ${validated.producerId},
2238
2198
  ${producerEpoch},
2239
2199
  ${now.iso}
@@ -2416,7 +2376,8 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2416
2376
  submissionId: validated.submissionId,
2417
2377
  existingOutcome: reservation.value.outcome
2418
2378
  });
2419
- const settlementFailure = settlementFailureFromRecord(yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, error.message))));
2379
+ const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, error.message)));
2380
+ const settlementFailure = settlementFailureFromRecord(reservationRecord);
2420
2381
  if (reservation.value.outcome === "failed" !== (settlementFailure !== void 0)) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, "The reserved outcome and canonical failure diagnostic disagree.");
2421
2382
  const submission = yield* requireSubmission(operation, validated.submissionId);
2422
2383
  if (submission.state === "settled") {
@@ -2494,7 +2455,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2494
2455
  ${now.iso}
2495
2456
  )
2496
2457
  `.pipe(Effect.mapError(sqlFailure(operation)));
2497
- const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.conversation_id, validated.submissionId);
2458
+ const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, validated.submissionId);
2498
2459
  return yield* decodeAbortIntent({
2499
2460
  submissionId: validated.submissionId,
2500
2461
  author: validated.author,
@@ -2512,19 +2473,19 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
2512
2473
  yield* hitFailpoint("ledger:claim-joining:before", operation);
2513
2474
  const claims = yield* inWriteTransaction(operation, Effect.gen(function* () {
2514
2475
  const host = yield* requireSubmission(operation, validated.hostSubmissionId);
2515
- if (host.conversation_id !== validated.conversationId) return yield* LedgerError.make({
2476
+ if (host.thread_id !== validated.threadId) return yield* LedgerError.make({
2516
2477
  operation,
2517
- message: `Host submission ${validated.hostSubmissionId} does not belong to conversation ${validated.conversationId}.`
2478
+ message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`
2518
2479
  });
2519
2480
  yield* requireOwnership(operation, host, validated.ownershipToken);
2520
2481
  const laterRows = yield* sql`
2521
2482
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2522
2483
  FROM effect_agent_submissions
2523
- WHERE conversation_id = ${validated.conversationId}
2484
+ WHERE thread_id = ${validated.threadId}
2524
2485
  AND queue_sequence > ${host.queue_sequence}
2525
2486
  ORDER BY queue_sequence ASC
2526
2487
  `.pipe(Effect.mapError(sqlFailure(operation)));
2527
- const later = yield* decodeSubmissionRows(operation, validated.conversationId, laterRows);
2488
+ const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
2528
2489
  const claimed = [];
2529
2490
  for (const row of later) {
2530
2491
  if (claimed.length >= validated.maxCount) break;
@@ -3034,27 +2995,27 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
3034
2995
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3035
2996
  FROM effect_agent_submissions
3036
2997
  WHERE state <> 'settled'
3037
- ORDER BY conversation_id ASC, queue_sequence ASC
2998
+ ORDER BY thread_id ASC, queue_sequence ASC
3038
2999
  LIMIT ${SCAN_PAGE_SIZE}
3039
3000
  ` : sql`
3040
3001
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3041
3002
  FROM effect_agent_submissions
3042
3003
  WHERE state <> 'settled'
3043
3004
  AND (
3044
- conversation_id > ${cursor.conversationId}
3005
+ thread_id > ${cursor.threadId}
3045
3006
  OR (
3046
- conversation_id = ${cursor.conversationId}
3007
+ thread_id = ${cursor.threadId}
3047
3008
  AND queue_sequence > ${cursor.queueSequence}
3048
3009
  )
3049
3010
  )
3050
- ORDER BY conversation_id ASC, queue_sequence ASC
3011
+ ORDER BY thread_id ASC, queue_sequence ASC
3051
3012
  LIMIT ${SCAN_PAGE_SIZE}
3052
3013
  `).pipe(Effect.mapError(sqlFailure(operation)));
3053
3014
  const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
3054
3015
  const snapshots = yield* Effect.forEach(decoded, (row) => decodeSubmissionSnapshot(operation, row));
3055
3016
  const last = decoded[decoded.length - 1];
3056
3017
  return [snapshots, last === void 0 || decoded.length < SCAN_PAGE_SIZE ? Option.none() : Option.some({
3057
- conversationId: last.conversation_id,
3018
+ threadId: last.thread_id,
3058
3019
  queueSequence: last.queue_sequence
3059
3020
  })];
3060
3021
  });
@@ -3211,12 +3172,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
3211
3172
  const submissionLedgerLayer = Layer.effectContext(makeServices$1());
3212
3173
  /**
3213
3174
  * A composition-root convenience Layer for the durable Submission Ledger. Point it at the
3214
- * same `ctx.storage` as the ConversationStore so claims fence the same producer epochs.
3175
+ * same `ctx.storage` as the ThreadStore so claims fence the same producer epochs.
3215
3176
  */
3216
3177
  const ledgerLayer = (options) => Layer.unwrap(Effect.map(DoStorageConfig, (config) => submissionLedgerLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(DoStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ storage: options.storage }), BrowserCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
3217
3178
  //#endregion
3218
3179
  //#region src/do-schedule-store.ts
3219
- const CURRENT_SCHEDULE_STORE_VERSION = 1;
3180
+ const CURRENT_SCHEDULE_STORE_VERSION = 2;
3220
3181
  const StoredScheduleJson = Schema.String.check(Schema.isMaxLength(19e5));
3221
3182
  const StoredDeadline = Schema.NullOr(ScheduleInstant);
3222
3183
  var ScheduleRow = class extends Schema.Class("@effect-agent/storage-cloudflare/ScheduleRow")({
@@ -3246,26 +3207,26 @@ var ScheduleTableNameRow = class extends Schema.Class("@effect-agent/storage-clo
3246
3207
  var DoScheduleTransaction = class extends Context.Service()("@effect-agent/storage-cloudflare/DoScheduleTransaction") {};
3247
3208
  /** Platform driver operations that use the same schedule-state and alarm transaction. */
3248
3209
  var DoScheduleAlarmControl = class extends Context.Service()("@effect-agent/storage-cloudflare/DoScheduleAlarmControl") {};
3249
- const unavailable = (operation) => ScheduleStorageError.make({
3210
+ const unavailable$1 = (operation) => ScheduleStorageError.make({
3250
3211
  operation,
3251
3212
  reason: "unavailable"
3252
3213
  });
3253
- const corrupt = (operation) => ScheduleStorageError.make({
3214
+ const corrupt$1 = (operation) => ScheduleStorageError.make({
3254
3215
  operation,
3255
3216
  reason: "corrupt"
3256
3217
  });
3257
- const decodeRows = Effect.fn("DoScheduleStore.decodeRows")(function* (schema, rows, operation) {
3258
- return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt(operation)));
3218
+ const decodeRows$1 = Effect.fn("DoScheduleStore.decodeRows")(function* (schema, rows, operation) {
3219
+ return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt$1(operation)));
3259
3220
  });
3260
- const decodeBoundary = (schema, value, operation) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt(operation)));
3221
+ const decodeBoundary = (schema, value, operation) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt$1(operation)));
3261
3222
  const decodeRecord = Effect.fn("DoScheduleStore.decodeRecord")(function* (row) {
3262
- const record = yield* Schema.decodeEffect(Schema.fromJsonString(ScheduleRecord))(row.record_json).pipe(Effect.mapError(() => corrupt("decode schedule")));
3263
- if (record.owner.tenantId !== row.tenant_id || record.owner.ownerId !== row.owner_id || record.scheduleId !== row.schedule_id || scheduleDeadline(record) !== row.deadline_at_millis) return yield* corrupt("decode schedule index");
3223
+ const record = yield* Schema.decodeEffect(Schema.fromJsonString(ScheduleRecord))(row.record_json).pipe(Effect.mapError(() => corrupt$1("decode schedule")));
3224
+ if (record.owner.tenantId !== row.tenant_id || record.owner.ownerId !== row.owner_id || record.scheduleId !== row.schedule_id || scheduleDeadline(record) !== row.deadline_at_millis) return yield* corrupt$1("decode schedule index");
3264
3225
  return record;
3265
3226
  });
3266
3227
  const encodeRecord = Effect.fn("DoScheduleStore.encodeRecord")(function* (record) {
3267
- const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(ScheduleRecord))(record).pipe(Effect.mapError(() => corrupt("encode schedule")));
3268
- return yield* Schema.decodeUnknownEffect(StoredScheduleJson)(encoded).pipe(Effect.mapError(() => corrupt("encode schedule bounds")));
3228
+ const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(ScheduleRecord))(record).pipe(Effect.mapError(() => corrupt$1("encode schedule")));
3229
+ return yield* Schema.decodeUnknownEffect(StoredScheduleJson)(encoded).pipe(Effect.mapError(() => corrupt$1("encode schedule bounds")));
3269
3230
  });
3270
3231
  const initializeScheduleStore = Effect.fn("DoScheduleStore.initialize")(function* () {
3271
3232
  const sql = yield* SqlClient.SqlClient;
@@ -3279,12 +3240,12 @@ const initializeScheduleStore = Effect.fn("DoScheduleStore.initialize")(function
3279
3240
  'effect_agent_schedules'
3280
3241
  )
3281
3242
  ORDER BY name
3282
- `.pipe(Effect.mapError(() => unavailable(operation)));
3283
- const tables = yield* decodeRows(Schema.Array(ScheduleTableNameRow), rawTables, operation);
3243
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3244
+ const tables = yield* decodeRows$1(Schema.Array(ScheduleTableNameRow), rawTables, operation);
3284
3245
  const hasState = tables.some((row) => row.name === "effect_agent_schedule_store_state");
3285
3246
  const hasSchedules = tables.some((row) => row.name === "effect_agent_schedules");
3286
3247
  if (!hasState) {
3287
- if (hasSchedules) return yield* corrupt(operation);
3248
+ if (hasSchedules) return yield* corrupt$1(operation);
3288
3249
  yield* sql.withTransaction(Effect.gen(function* () {
3289
3250
  yield* sql`
3290
3251
  CREATE TABLE effect_agent_schedule_store_state (
@@ -3318,17 +3279,17 @@ const initializeScheduleStore = Effect.fn("DoScheduleStore.initialize")(function
3318
3279
  singleton, storage_version, alarm_generation
3319
3280
  ) VALUES (1, ${CURRENT_SCHEDULE_STORE_VERSION}, 0)
3320
3281
  `.withoutTransform;
3321
- })).pipe(Effect.mapError(() => unavailable(operation)));
3282
+ })).pipe(Effect.mapError(() => unavailable$1(operation)));
3322
3283
  return;
3323
3284
  }
3324
- if (!hasSchedules) return yield* corrupt(operation);
3285
+ if (!hasSchedules) return yield* corrupt$1(operation);
3325
3286
  const rawState = yield* sql`
3326
3287
  SELECT storage_version, alarm_generation
3327
3288
  FROM effect_agent_schedule_store_state
3328
3289
  WHERE singleton = 1
3329
- `.pipe(Effect.mapError(() => unavailable(operation)));
3330
- const state = yield* decodeRows(Schema.Array(ScheduleStoreStateRow), rawState, operation);
3331
- if (state.length !== 1 || state[0].storage_version !== CURRENT_SCHEDULE_STORE_VERSION) return yield* corrupt(operation);
3290
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3291
+ const state = yield* decodeRows$1(Schema.Array(ScheduleStoreStateRow), rawState, operation);
3292
+ if (state.length !== 1 || state[0].storage_version !== CURRENT_SCHEDULE_STORE_VERSION) return yield* corrupt$1(state.length === 1 ? `${operation}: incompatible storage version ${state[0].storage_version}; expected ${CURRENT_SCHEDULE_STORE_VERSION}` : `${operation}: invalid storage version row`);
3332
3293
  });
3333
3294
  const makeServices = Effect.gen(function* () {
3334
3295
  const sql = yield* SqlClient.SqlClient;
@@ -3342,13 +3303,13 @@ const makeServices = Effect.gen(function* () {
3342
3303
  WHERE tenant_id = ${key.owner.tenantId}
3343
3304
  AND owner_id = ${key.owner.ownerId}
3344
3305
  AND schedule_id = ${key.scheduleId}
3345
- `.pipe(Effect.mapError(() => unavailable(operation)));
3346
- return yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
3306
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3307
+ return yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
3347
3308
  });
3348
3309
  const readOne = Effect.fn("DoScheduleStore.readOne")(function* (key, operation) {
3349
3310
  const rows = yield* readRows(key, operation);
3350
3311
  if (rows.length === 0) return null;
3351
- if (rows.length !== 1) return yield* corrupt(operation);
3312
+ if (rows.length !== 1) return yield* corrupt$1(operation);
3352
3313
  return yield* decodeRecord(rows[0]);
3353
3314
  });
3354
3315
  const readNextDeadline = Effect.fn("DoScheduleStore.readNextDeadline")(function* (owner, operation) {
@@ -3356,15 +3317,15 @@ const makeServices = Effect.gen(function* () {
3356
3317
  SELECT MIN(deadline_at_millis) AS deadline_at_millis
3357
3318
  FROM effect_agent_schedules
3358
3319
  WHERE deadline_at_millis IS NOT NULL
3359
- `.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
3320
+ `.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
3360
3321
  SELECT MIN(deadline_at_millis) AS deadline_at_millis
3361
3322
  FROM effect_agent_schedules
3362
3323
  WHERE tenant_id = ${owner.tenantId}
3363
3324
  AND owner_id = ${owner.ownerId}
3364
3325
  AND deadline_at_millis IS NOT NULL
3365
- `.pipe(Effect.mapError(() => unavailable(operation)));
3366
- const decoded = yield* decodeRows(Schema.Array(ScheduleDeadlineRow), rows, operation);
3367
- if (decoded.length !== 1) return yield* corrupt(operation);
3326
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3327
+ const decoded = yield* decodeRows$1(Schema.Array(ScheduleDeadlineRow), rows, operation);
3328
+ if (decoded.length !== 1) return yield* corrupt$1(operation);
3368
3329
  return decoded[0].deadline_at_millis;
3369
3330
  });
3370
3331
  const replaceAlarm = Effect.fn("DoScheduleStore.replaceAlarm")(function* (replace, deadlineAtMillis, operation) {
@@ -3373,9 +3334,9 @@ const makeServices = Effect.gen(function* () {
3373
3334
  SET alarm_generation = alarm_generation + 1
3374
3335
  WHERE singleton = 1
3375
3336
  RETURNING storage_version, alarm_generation
3376
- `.pipe(Effect.mapError(() => unavailable(operation)));
3377
- const state = yield* decodeRows(Schema.Array(ScheduleStoreStateRow), rawState, operation);
3378
- if (state.length !== 1 || state[0].storage_version !== CURRENT_SCHEDULE_STORE_VERSION) return yield* corrupt(operation);
3337
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3338
+ const state = yield* decodeRows$1(Schema.Array(ScheduleStoreStateRow), rawState, operation);
3339
+ if (state.length !== 1 || state[0].storage_version !== CURRENT_SCHEDULE_STORE_VERSION) return yield* corrupt$1(operation);
3379
3340
  yield* scheduleFailpoint.hit("schedule:alarm:before");
3380
3341
  yield* replace({
3381
3342
  deadlineAtMillis,
@@ -3406,9 +3367,9 @@ const makeServices = Effect.gen(function* () {
3406
3367
  AND owner_id = ${canonical.owner.ownerId}
3407
3368
  AND (json_extract(record_json, '$.pending') IS NOT NULL OR
3408
3369
  (json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
3409
- `.pipe(Effect.mapError(() => unavailable(operation)));
3410
- const counts = yield* decodeRows(Schema.Array(ScheduleCountRow), rawCounts, operation);
3411
- if (counts.length !== 1) return yield* corrupt(operation);
3370
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3371
+ const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
3372
+ if (counts.length !== 1) return yield* corrupt$1(operation);
3412
3373
  if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
3413
3374
  yield* scheduleFailpoint.hit("schedule:insert:before");
3414
3375
  yield* sql`
@@ -3421,7 +3382,7 @@ const makeServices = Effect.gen(function* () {
3421
3382
  ${scheduleDeadline(canonical)},
3422
3383
  ${recordJson}
3423
3384
  )
3424
- `.pipe(Effect.mapError(() => unavailable(operation)));
3385
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3425
3386
  const deadline = yield* readNextDeadline(void 0, operation);
3426
3387
  yield* replaceAlarm(replace, deadline, operation);
3427
3388
  return {
@@ -3446,7 +3407,7 @@ const makeServices = Effect.gen(function* () {
3446
3407
  AND owner_id = ${request.owner.ownerId}
3447
3408
  ORDER BY schedule_id
3448
3409
  LIMIT ${request.limit + 1}
3449
- `.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
3410
+ `.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
3450
3411
  SELECT tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
3451
3412
  FROM effect_agent_schedules
3452
3413
  WHERE tenant_id = ${request.owner.tenantId}
@@ -3454,8 +3415,8 @@ const makeServices = Effect.gen(function* () {
3454
3415
  AND schedule_id > ${request.after}
3455
3416
  ORDER BY schedule_id
3456
3417
  LIMIT ${request.limit + 1}
3457
- `.pipe(Effect.mapError(() => unavailable(operation)));
3458
- const decoded = yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
3418
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3419
+ const decoded = yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
3459
3420
  const records = yield* Effect.forEach(decoded, decodeRecord);
3460
3421
  const hasNext = records.length > request.limit;
3461
3422
  const items = hasNext ? records.slice(0, request.limit) : records;
@@ -3480,9 +3441,9 @@ const makeServices = Effect.gen(function* () {
3480
3441
  WHERE tenant_id = ${key.owner.tenantId} AND owner_id = ${key.owner.ownerId}
3481
3442
  AND (json_extract(record_json, '$.pending') IS NOT NULL OR
3482
3443
  (json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
3483
- `.pipe(Effect.mapError(() => unavailable(operation)));
3484
- const counts = yield* decodeRows(Schema.Array(ScheduleCountRow), rawCounts, operation);
3485
- if (counts.length !== 1) return yield* corrupt(operation);
3444
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3445
+ const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
3446
+ if (counts.length !== 1) return yield* corrupt$1(operation);
3486
3447
  if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
3487
3448
  }
3488
3449
  if (next === current) return {
@@ -3497,7 +3458,7 @@ const makeServices = Effect.gen(function* () {
3497
3458
  WHERE tenant_id = ${canonicalKey.owner.tenantId}
3498
3459
  AND owner_id = ${canonicalKey.owner.ownerId}
3499
3460
  AND schedule_id = ${canonicalKey.scheduleId}
3500
- `.pipe(Effect.mapError(() => unavailable(operation)));
3461
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3501
3462
  const deadline = yield* readNextDeadline(void 0, operation);
3502
3463
  yield* replaceAlarm(replace, deadline, operation);
3503
3464
  return {
@@ -3510,7 +3471,7 @@ const makeServices = Effect.gen(function* () {
3510
3471
  });
3511
3472
  const due = Effect.fn("DoScheduleStore.due")(function* (nowMillis, limit, owner, after) {
3512
3473
  const operation = "query due schedules";
3513
- const cursor = after === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(Effect.mapError(() => corrupt(operation)));
3474
+ const cursor = after === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(Effect.mapError(() => corrupt$1(operation)));
3514
3475
  const continuation = cursor === void 0 ? sql`1 = 1` : sql`
3515
3476
  (deadline_at_millis, tenant_id, owner_id, schedule_id) >
3516
3477
  (${cursor.deadlineAtMillis}, ${cursor.owner.tenantId}, ${cursor.owner.ownerId}, ${cursor.scheduleId})`;
@@ -3520,7 +3481,7 @@ const makeServices = Effect.gen(function* () {
3520
3481
  WHERE deadline_at_millis <= ${nowMillis} AND ${continuation}
3521
3482
  ORDER BY deadline_at_millis, tenant_id, owner_id, schedule_id
3522
3483
  LIMIT ${limit}
3523
- `.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
3484
+ `.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
3524
3485
  SELECT tenant_id, owner_id, schedule_id, deadline_at_millis
3525
3486
  FROM effect_agent_schedules
3526
3487
  WHERE tenant_id = ${owner.tenantId}
@@ -3528,8 +3489,8 @@ const makeServices = Effect.gen(function* () {
3528
3489
  AND deadline_at_millis <= ${nowMillis} AND ${continuation}
3529
3490
  ORDER BY deadline_at_millis, schedule_id
3530
3491
  LIMIT ${limit}
3531
- `.pipe(Effect.mapError(() => unavailable(operation)));
3532
- return (yield* decodeRows(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
3492
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3493
+ return (yield* decodeRows$1(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
3533
3494
  owner: {
3534
3495
  tenantId: row.tenant_id,
3535
3496
  ownerId: row.owner_id
@@ -3571,18 +3532,724 @@ const makeServices = Effect.gen(function* () {
3571
3532
  */
3572
3533
  const scheduleStoreLayer = Layer.effectContext(makeServices);
3573
3534
  //#endregion
3535
+ //#region src/do-subscription-store.ts
3536
+ const CURRENT_SUBSCRIPTION_STORE_VERSION = 2;
3537
+ const StoredJson = Schema.String.check(Schema.isMaxLength(19e5));
3538
+ const CountRow = Schema.Struct({ count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) });
3539
+ const SequenceRow = Schema.Struct({ sequence: Schema.Natural });
3540
+ const ScanRow = Schema.Struct({
3541
+ event_scan_cursor: Schema.String,
3542
+ delivery_scan_cursor: Schema.String,
3543
+ recovery_scan_cursor: Schema.Natural
3544
+ });
3545
+ const JsonRow = Schema.Struct({ record_json: StoredJson });
3546
+ const RegistrationRow = Schema.Struct({
3547
+ owner_id: Schema.String,
3548
+ subscription_id: Schema.String,
3549
+ ordinal: Schema.Natural,
3550
+ source_name: Schema.String,
3551
+ source_version: Schema.String,
3552
+ matching_key: Schema.String,
3553
+ state: SubscriptionRecord.fields.state,
3554
+ expires_at_millis: Schema.Number,
3555
+ recovery_at_millis: Schema.NullOr(Schema.Number),
3556
+ record_json: StoredJson
3557
+ });
3558
+ const EventRow = Schema.Struct({
3559
+ event_id: Schema.String,
3560
+ source_name: Schema.String,
3561
+ source_version: Schema.String,
3562
+ matching_key: Schema.String,
3563
+ payload_digest: Digest,
3564
+ cutoff: Schema.Natural,
3565
+ cursor: Schema.Natural,
3566
+ routing_complete: Schema.Number,
3567
+ next_attempt_at_millis: Schema.Number,
3568
+ record_json: StoredJson
3569
+ });
3570
+ const DeliveryRow = Schema.Struct({
3571
+ owner_id: Schema.String,
3572
+ subscription_id: Schema.String,
3573
+ event_id: Schema.String,
3574
+ delivery_key: Schema.String,
3575
+ state: SubscriptionDelivery.fields.state,
3576
+ next_attempt_at_millis: Schema.Number,
3577
+ record_json: StoredJson
3578
+ });
3579
+ const StoreStateRow = Schema.Struct({
3580
+ storage_version: Schema.Int,
3581
+ alarm_generation: Schema.Natural
3582
+ });
3583
+ /** One Durable Object transaction combining subscription SQL and its single native alarm. */
3584
+ var DoSubscriptionTransaction = class extends Context.Service()("@effect-agent/storage-cloudflare/DoSubscriptionTransaction") {};
3585
+ var DoSubscriptionAlarmControl = class extends Context.Service()("@effect-agent/storage-cloudflare/DoSubscriptionAlarmControl") {};
3586
+ const error = (reason, code) => SubscriptionError.make({
3587
+ reason,
3588
+ code
3589
+ });
3590
+ const unavailable = (operation) => error("storage", operation);
3591
+ const corrupt = (operation) => error("corrupt", operation);
3592
+ const bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
3593
+ const validate = (schema, value, code) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => error("validation", code)));
3594
+ const encode = (schema, value, code) => Schema.encodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
3595
+ const decode = (schema, value, code) => Schema.decodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
3596
+ const decodeRows = (schema, rows, code) => Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(Effect.mapError(() => corrupt(code)));
3597
+ const sameDeliveryIdentity = (left, right) => subscriptionDeliveryKeyString(left.key) === subscriptionDeliveryKeyString(right.key) && left.deliveryId === right.deliveryId && left.source.name === right.source.name && left.source.version === right.source.version && left.threadId === right.threadId && left.admissionKey === right.admissionKey && left.subscriptionFingerprint === right.subscriptionFingerprint && left.eventDigest === right.eventDigest;
3598
+ const initializeDoSubscriptionStore = Effect.fn("DoSubscriptionStore.initialize")(function* () {
3599
+ const sql = yield* SqlClient.SqlClient;
3600
+ const names = yield* sql`
3601
+ SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'effect_agent_subscription_%'
3602
+ `.pipe(Effect.mapError(() => unavailable("inspect subscription storage")));
3603
+ const decodedNames = yield* decodeRows(Schema.Struct({ name: Schema.String }), names, "inspect subscription storage");
3604
+ const expected = /* @__PURE__ */ new Set([
3605
+ "effect_agent_subscription_store_state",
3606
+ "effect_agent_subscription_sequences",
3607
+ "effect_agent_subscriptions",
3608
+ "effect_agent_subscription_events",
3609
+ "effect_agent_subscription_deliveries"
3610
+ ]);
3611
+ const hasState = decodedNames.some(({ name }) => name === "effect_agent_subscription_store_state");
3612
+ if (!hasState && decodedNames.length > 0) return yield* corrupt("partial subscription storage");
3613
+ if (!hasState) {
3614
+ yield* sql.withTransaction(Effect.gen(function* () {
3615
+ yield* sql`CREATE TABLE effect_agent_subscription_store_state (
3616
+ singleton INTEGER PRIMARY KEY NOT NULL CHECK(singleton=1), storage_version INTEGER NOT NULL, alarm_generation INTEGER NOT NULL
3617
+ )`.withoutTransform;
3618
+ yield* sql`CREATE TABLE effect_agent_subscription_sequences (
3619
+ tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, sequence INTEGER NOT NULL,
3620
+ event_scan_cursor TEXT NOT NULL, delivery_scan_cursor TEXT NOT NULL, recovery_scan_cursor INTEGER NOT NULL,
3621
+ PRIMARY KEY (tenant_id, source_address)
3622
+ )`.withoutTransform;
3623
+ yield* sql`CREATE TABLE effect_agent_subscriptions (
3624
+ tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, owner_id TEXT NOT NULL, subscription_id TEXT NOT NULL,
3625
+ ordinal INTEGER NOT NULL, source_name TEXT NOT NULL, source_version TEXT NOT NULL, matching_key TEXT NOT NULL,
3626
+ state TEXT NOT NULL, expires_at_millis INTEGER NOT NULL, recovery_at_millis INTEGER, record_json TEXT NOT NULL,
3627
+ PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id), UNIQUE (tenant_id, source_address, ordinal)
3628
+ )`.withoutTransform;
3629
+ yield* sql`CREATE INDEX effect_agent_subscriptions_owner ON effect_agent_subscriptions (tenant_id, source_address, owner_id, ordinal)`.withoutTransform;
3630
+ yield* sql`CREATE INDEX effect_agent_subscriptions_candidates ON effect_agent_subscriptions (tenant_id, source_address, source_name, source_version, matching_key, ordinal)`.withoutTransform;
3631
+ yield* sql`CREATE INDEX effect_agent_subscriptions_recovery ON effect_agent_subscriptions (tenant_id, source_address, recovery_at_millis, ordinal) WHERE recovery_at_millis IS NOT NULL`.withoutTransform;
3632
+ yield* sql`CREATE TABLE effect_agent_subscription_events (
3633
+ tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, event_id TEXT NOT NULL, source_name TEXT NOT NULL,
3634
+ source_version TEXT NOT NULL, matching_key TEXT NOT NULL, payload_digest TEXT NOT NULL, cutoff INTEGER NOT NULL,
3635
+ cursor INTEGER NOT NULL, routing_complete INTEGER NOT NULL, next_attempt_at_millis INTEGER NOT NULL, record_json TEXT NOT NULL,
3636
+ PRIMARY KEY (tenant_id, source_address, event_id)
3637
+ )`.withoutTransform;
3638
+ yield* sql`CREATE INDEX effect_agent_subscription_events_pending ON effect_agent_subscription_events (tenant_id, source_address, routing_complete, next_attempt_at_millis, event_id)`.withoutTransform;
3639
+ yield* sql`CREATE TABLE effect_agent_subscription_deliveries (
3640
+ tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, owner_id TEXT NOT NULL, subscription_id TEXT NOT NULL,
3641
+ event_id TEXT NOT NULL, delivery_key TEXT NOT NULL, state TEXT NOT NULL, next_attempt_at_millis INTEGER NOT NULL,
3642
+ record_json TEXT NOT NULL, PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id, event_id),
3643
+ UNIQUE (tenant_id, source_address, delivery_key)
3644
+ )`.withoutTransform;
3645
+ yield* sql`CREATE INDEX effect_agent_subscription_deliveries_pending ON effect_agent_subscription_deliveries (tenant_id, source_address, state, next_attempt_at_millis, delivery_key)`.withoutTransform;
3646
+ yield* sql`CREATE INDEX effect_agent_subscription_deliveries_registration ON effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, delivery_key)`.withoutTransform;
3647
+ yield* sql`INSERT INTO effect_agent_subscription_store_state (singleton, storage_version, alarm_generation)
3648
+ VALUES (1, ${CURRENT_SUBSCRIPTION_STORE_VERSION}, 0)`.withoutTransform;
3649
+ })).pipe(Effect.mapError(() => unavailable("initialize subscription storage")));
3650
+ return;
3651
+ }
3652
+ if (decodedNames.length !== expected.size || decodedNames.some(({ name }) => !expected.has(name))) return yield* corrupt("subscription storage tables");
3653
+ const rows = yield* sql`SELECT storage_version, alarm_generation FROM effect_agent_subscription_store_state WHERE singleton=1`.pipe(Effect.mapError(() => unavailable("read subscription storage version")));
3654
+ const state = yield* decodeRows(StoreStateRow, rows, "read subscription storage version");
3655
+ if (state.length !== 1 || state[0].storage_version !== CURRENT_SUBSCRIPTION_STORE_VERSION) return yield* corrupt(state.length === 1 ? `incompatible subscription storage version ${state[0].storage_version}; expected ${CURRENT_SUBSCRIPTION_STORE_VERSION}` : "invalid subscription storage version row");
3656
+ });
3657
+ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function* (owned) {
3658
+ const partition = yield* validate(SourcePartition, owned, "partition");
3659
+ const sql = yield* SqlClient.SqlClient;
3660
+ const failpoint = yield* SubscriptionFailpoint;
3661
+ const transactions = yield* DoSubscriptionTransaction;
3662
+ yield* initializeDoSubscriptionStore();
3663
+ yield* sql`
3664
+ INSERT INTO effect_agent_subscription_sequences (
3665
+ tenant_id, source_address, sequence, event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
3666
+ ) VALUES (${partition.tenantId}, ${partition.address}, 0, '', '', 0) ON CONFLICT DO NOTHING
3667
+ `.pipe(Effect.mapError(() => unavailable("initialize subscription partition")));
3668
+ const query = (effect, code) => effect.pipe(Effect.mapError(() => unavailable(code)));
3669
+ const readIndexedDeadline = Effect.fn("DoSubscriptionStore.readIndexedDeadline")(function* () {
3670
+ const scanRows = yield* query(sql`
3671
+ SELECT event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor FROM effect_agent_subscription_sequences
3672
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3673
+ `, "read scan cursor deadline");
3674
+ const scans = yield* decodeRows(ScanRow, scanRows, "read scan cursor deadline");
3675
+ if (scans.length !== 1) return yield* corrupt("scan cursor deadline");
3676
+ if (scans[0].event_scan_cursor !== "" || scans[0].delivery_scan_cursor !== "" || scans[0].recovery_scan_cursor !== 0) return 0;
3677
+ const rows = yield* query(sql`
3678
+ SELECT MIN(deadline) AS deadline FROM (
3679
+ SELECT next_attempt_at_millis AS deadline FROM effect_agent_subscription_events
3680
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND routing_complete=0
3681
+ UNION ALL SELECT next_attempt_at_millis FROM effect_agent_subscription_deliveries
3682
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
3683
+ UNION ALL SELECT recovery_at_millis FROM effect_agent_subscriptions
3684
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active' AND recovery_at_millis IS NOT NULL
3685
+ )
3686
+ `, "read subscription deadline");
3687
+ const decoded = yield* decodeRows(Schema.Struct({ deadline: Schema.NullOr(Schema.Number) }), rows, "read subscription deadline");
3688
+ if (decoded.length !== 1) return yield* corrupt("subscription deadline");
3689
+ return decoded[0].deadline;
3690
+ });
3691
+ const replaceAlarm = Effect.fn("DoSubscriptionStore.replaceAlarm")(function* (replace, deadlineAtMillis) {
3692
+ const rows = yield* query(sql`
3693
+ UPDATE effect_agent_subscription_store_state SET alarm_generation=alarm_generation+1
3694
+ WHERE singleton=1 RETURNING storage_version, alarm_generation
3695
+ `, "advance subscription alarm generation");
3696
+ const state = yield* decodeRows(StoreStateRow, rows, "advance subscription alarm generation");
3697
+ if (state.length !== 1 || state[0].storage_version !== CURRENT_SUBSCRIPTION_STORE_VERSION) return yield* corrupt("subscription alarm state");
3698
+ yield* failpoint.hit("subscription:alarm:before");
3699
+ yield* replace({
3700
+ deadlineAtMillis,
3701
+ generation: state[0].alarm_generation
3702
+ });
3703
+ yield* failpoint.hit("subscription:alarm:after");
3704
+ });
3705
+ const transact = (effect) => transactions.run((replace) => Effect.gen(function* () {
3706
+ const value = yield* effect;
3707
+ yield* replaceAlarm(replace, yield* readIndexedDeadline());
3708
+ return value;
3709
+ }));
3710
+ const requirePartition = (candidate, code) => sameSourcePartition(candidate, partition) ? Effect.void : Effect.fail(error("validation", code));
3711
+ const requireKey = Effect.fn("SqliteSubscriptionStore.requireKey")(function* (input, code) {
3712
+ const key = yield* validate(SubscriptionKey, input, code);
3713
+ yield* requirePartition(key.partition, code);
3714
+ return key;
3715
+ });
3716
+ const readRegistration = Effect.fn("SqliteSubscriptionStore.readRegistration")(function* (key, code) {
3717
+ const rows = yield* query(sql`
3718
+ SELECT owner_id, subscription_id, ordinal, source_name, source_version, matching_key, state,
3719
+ expires_at_millis, recovery_at_millis, record_json FROM effect_agent_subscriptions
3720
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3721
+ AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId}
3722
+ `, code);
3723
+ const decodedRows = yield* decodeRows(RegistrationRow, rows, code);
3724
+ if (decodedRows.length > 1) return yield* corrupt(code);
3725
+ const row = decodedRows[0];
3726
+ if (row === void 0) return null;
3727
+ const record = yield* decode(SubscriptionRecord, row.record_json, code);
3728
+ if (!sameSourcePartition(record.key.partition, partition) || record.key.ownerId !== row.owner_id || record.key.subscriptionId !== row.subscription_id || record.ordinal !== row.ordinal || record.configuration.source.name !== row.source_name || record.configuration.source.version !== row.source_version || record.configuration.matchingKey !== row.matching_key || record.state !== row.state || record.configuration.expiresAtMillis !== row.expires_at_millis || (record.recovery?.nextAttemptAtMillis ?? null) !== row.recovery_at_millis) return yield* corrupt(`${code}-projection`);
3729
+ return record;
3730
+ });
3731
+ const readEvent = Effect.fn("SqliteSubscriptionStore.readEvent")(function* (eventId, code) {
3732
+ const rows = yield* query(sql`
3733
+ SELECT event_id, source_name, source_version, matching_key, payload_digest, cutoff, cursor,
3734
+ routing_complete, next_attempt_at_millis, record_json FROM effect_agent_subscription_events
3735
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${eventId}
3736
+ `, code);
3737
+ const decodedRows = yield* decodeRows(EventRow, rows, code);
3738
+ if (decodedRows.length > 1) return yield* corrupt(code);
3739
+ const row = decodedRows[0];
3740
+ if (row === void 0) return null;
3741
+ const event = yield* decode(AcceptedEvent, row.record_json, code);
3742
+ if (!sameSourcePartition(event.partition, partition) || event.eventId !== row.event_id || event.source.name !== row.source_name || event.source.version !== row.source_version || event.matchingKey !== row.matching_key || event.payloadDigest !== row.payload_digest || event.cutoff !== row.cutoff || event.cursor !== row.cursor || (event.routingComplete ? 1 : 0) !== row.routing_complete || event.nextAttemptAtMillis !== row.next_attempt_at_millis) return yield* corrupt(`${code}-projection`);
3743
+ return event;
3744
+ });
3745
+ const readDelivery = Effect.fn("SqliteSubscriptionStore.readDelivery")(function* (key, code) {
3746
+ const rows = yield* query(sql`
3747
+ SELECT owner_id, subscription_id, event_id, delivery_key, state, next_attempt_at_millis, record_json
3748
+ FROM effect_agent_subscription_deliveries
3749
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3750
+ AND owner_id=${key.subscription.ownerId} AND subscription_id=${key.subscription.subscriptionId}
3751
+ AND event_id=${key.eventId}
3752
+ `, code);
3753
+ const decodedRows = yield* decodeRows(DeliveryRow, rows, code);
3754
+ if (decodedRows.length > 1) return yield* corrupt(code);
3755
+ const row = decodedRows[0];
3756
+ if (row === void 0) return null;
3757
+ const delivery = yield* decode(SubscriptionDelivery, row.record_json, code);
3758
+ if (!sameSourcePartition(delivery.key.subscription.partition, partition) || delivery.key.subscription.ownerId !== row.owner_id || delivery.key.subscription.subscriptionId !== row.subscription_id || delivery.key.eventId !== row.event_id || subscriptionDeliveryKeyString(delivery.key) !== row.delivery_key || delivery.state !== row.state || delivery.retry.nextAttemptAtMillis !== row.next_attempt_at_millis) return yield* corrupt(`${code}-projection`);
3759
+ return delivery;
3760
+ });
3761
+ const count = Effect.fn("SqliteSubscriptionStore.count")(function* (statement, code) {
3762
+ const rows = yield* query(statement, code);
3763
+ const decoded = yield* decodeRows(CountRow, rows, code);
3764
+ if (decoded.length !== 1) return yield* corrupt(code);
3765
+ return decoded[0].count;
3766
+ });
3767
+ const nextSequence = Effect.fn("SqliteSubscriptionStore.nextSequence")(function* () {
3768
+ const rows = yield* query(sql`
3769
+ UPDATE effect_agent_subscription_sequences SET sequence=sequence+1
3770
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3771
+ RETURNING sequence
3772
+ `, "advance subscription sequence");
3773
+ const decoded = yield* decodeRows(SequenceRow, rows, "advance subscription sequence");
3774
+ if (decoded.length !== 1) return yield* corrupt("subscription sequence");
3775
+ return decoded[0].sequence;
3776
+ });
3777
+ const writeRegistration = Effect.fn("SqliteSubscriptionStore.writeRegistration")(function* (record) {
3778
+ const json = yield* encode(SubscriptionRecord, record, "encode subscription");
3779
+ yield* query(sql`
3780
+ UPDATE effect_agent_subscriptions SET state=${record.state}, expires_at_millis=${record.configuration.expiresAtMillis},
3781
+ recovery_at_millis=${record.recovery?.nextAttemptAtMillis ?? null}, record_json=${json}
3782
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3783
+ AND owner_id=${record.key.ownerId} AND subscription_id=${record.key.subscriptionId}
3784
+ `, "write subscription");
3785
+ });
3786
+ const writeEvent = Effect.fn("SqliteSubscriptionStore.writeEvent")(function* (event) {
3787
+ const json = yield* encode(AcceptedEvent, event, "encode event");
3788
+ yield* query(sql`
3789
+ UPDATE effect_agent_subscription_events SET cursor=${event.cursor}, routing_complete=${event.routingComplete ? 1 : 0},
3790
+ next_attempt_at_millis=${event.nextAttemptAtMillis}, record_json=${json}
3791
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${event.eventId}
3792
+ `, "write event");
3793
+ });
3794
+ const writeDelivery = Effect.fn("SqliteSubscriptionStore.writeDelivery")(function* (delivery) {
3795
+ const json = yield* encode(SubscriptionDelivery, delivery, "encode delivery");
3796
+ yield* query(sql`
3797
+ UPDATE effect_agent_subscription_deliveries SET state=${delivery.state},
3798
+ next_attempt_at_millis=${delivery.retry.nextAttemptAtMillis}, record_json=${json}
3799
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3800
+ AND owner_id=${delivery.key.subscription.ownerId} AND subscription_id=${delivery.key.subscription.subscriptionId}
3801
+ AND event_id=${delivery.key.eventId}
3802
+ `, "write delivery");
3803
+ });
3804
+ const register = Effect.fn("SqliteSubscriptionStore.register")(function* (input, inputLimits) {
3805
+ const record = yield* validate(SubscriptionRecord, input, "register-record");
3806
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
3807
+ yield* requirePartition(record.key.partition, "register-partition");
3808
+ const result = yield* transact(Effect.gen(function* () {
3809
+ const existing = yield* readRegistration(record.key, "register-existing");
3810
+ if (existing !== null) {
3811
+ if (existing.creationFingerprint !== record.creationFingerprint) return yield* error("conflict", "registration-identity");
3812
+ return {
3813
+ value: existing,
3814
+ changed: false
3815
+ };
3816
+ }
3817
+ if (bytes(record.configuration.context) > limits.maxContextBytes) return yield* error("capacity", "context-bytes");
3818
+ if (bytes(record.configuration.parameters) > limits.maxPayloadBytes) return yield* error("capacity", "parameters-bytes");
3819
+ if (record.configuration.expiresAtMillis - record.createdAtMillis > limits.maxLifetimeMillis) return yield* error("capacity", "lifetime");
3820
+ if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count registrations")) >= limits.maxRegistrations) return yield* error("capacity", "registrations");
3821
+ if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${record.key.ownerId}`, "count owner registrations")) >= limits.maxRegistrationsPerOwner) return yield* error("capacity", "owner-registrations");
3822
+ const assigned = {
3823
+ ...record,
3824
+ ordinal: yield* nextSequence()
3825
+ };
3826
+ const json = yield* encode(SubscriptionRecord, assigned, "encode registration");
3827
+ yield* failpoint.hit("subscription:register:before");
3828
+ yield* query(sql`
3829
+ INSERT INTO effect_agent_subscriptions (tenant_id, source_address, owner_id, subscription_id, ordinal,
3830
+ source_name, source_version, matching_key, state, expires_at_millis, recovery_at_millis, record_json)
3831
+ VALUES (${partition.tenantId}, ${partition.address}, ${assigned.key.ownerId}, ${assigned.key.subscriptionId}, ${assigned.ordinal},
3832
+ ${assigned.configuration.source.name}, ${assigned.configuration.source.version}, ${assigned.configuration.matchingKey}, ${assigned.state},
3833
+ ${assigned.configuration.expiresAtMillis}, ${assigned.recovery?.nextAttemptAtMillis ?? null}, ${json})
3834
+ `, "insert registration");
3835
+ return {
3836
+ value: assigned,
3837
+ changed: true
3838
+ };
3839
+ }));
3840
+ if (result.changed) yield* failpoint.hit("subscription:register:after");
3841
+ return result.value;
3842
+ });
3843
+ const get = Effect.fn("SqliteSubscriptionStore.get")(function* (input) {
3844
+ return yield* readRegistration(yield* requireKey(input, "get-key"), "get subscription");
3845
+ });
3846
+ const list = Effect.fn("SqliteSubscriptionStore.list")(function* (ownerId, after, limit) {
3847
+ const rows = yield* query(sql`
3848
+ SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3849
+ AND owner_id=${ownerId} AND ordinal>${after} ORDER BY ordinal LIMIT ${limit}
3850
+ `, "list subscriptions");
3851
+ const decoded = yield* decodeRows(Schema.Struct({
3852
+ owner_id: Schema.String,
3853
+ subscription_id: Schema.String,
3854
+ ordinal: Schema.Natural
3855
+ }), rows, "list subscriptions");
3856
+ return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.listRecord")(function* (row) {
3857
+ const record = yield* readRegistration({
3858
+ partition,
3859
+ ownerId: row.owner_id,
3860
+ subscriptionId: row.subscription_id
3861
+ }, "list subscription");
3862
+ if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("list subscription projection");
3863
+ return record;
3864
+ }));
3865
+ });
3866
+ const cancel = Effect.fn("SqliteSubscriptionStore.cancel")(function* (input) {
3867
+ const key = yield* requireKey(input, "cancel-key");
3868
+ const result = yield* transact(Effect.gen(function* () {
3869
+ const current = yield* readRegistration(key, "cancel subscription");
3870
+ if (current === null) return yield* error("not-found", "subscription");
3871
+ if (current.state === "cancelled") return {
3872
+ value: current,
3873
+ changed: false
3874
+ };
3875
+ const updated = {
3876
+ ...current,
3877
+ state: "cancelled",
3878
+ recovery: null
3879
+ };
3880
+ yield* failpoint.hit("subscription:cancel:before");
3881
+ yield* writeRegistration(updated);
3882
+ return {
3883
+ value: updated,
3884
+ changed: true
3885
+ };
3886
+ }));
3887
+ if (result.changed) yield* failpoint.hit("subscription:cancel:after");
3888
+ return result.value;
3889
+ });
3890
+ const accept = Effect.fn("SqliteSubscriptionStore.accept")(function* (input, inputLimits) {
3891
+ const event = yield* validate(AcceptedEvent, input, "accept-event");
3892
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "accept-limits");
3893
+ yield* requirePartition(event.partition, "accept-partition");
3894
+ const result = yield* transact(Effect.gen(function* () {
3895
+ const existing = yield* readEvent(event.eventId, "accept event");
3896
+ if (existing !== null) {
3897
+ if (!sameAcceptedEventIdentity(existing, event)) return yield* error("conflict", "event-identity");
3898
+ return {
3899
+ value: existing,
3900
+ changed: false
3901
+ };
3902
+ }
3903
+ if (bytes(event.payload) > limits.maxPayloadBytes) return yield* error("capacity", "payload-bytes");
3904
+ if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_events WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count events")) >= limits.maxEvents) return yield* error("capacity", "events");
3905
+ const accepted = {
3906
+ ...event,
3907
+ cutoff: yield* nextSequence(),
3908
+ cursor: 0,
3909
+ routingComplete: false,
3910
+ routingFailure: null
3911
+ };
3912
+ const json = yield* encode(AcceptedEvent, accepted, "encode accepted event");
3913
+ yield* failpoint.hit("subscription:accept:before");
3914
+ yield* query(sql`
3915
+ INSERT INTO effect_agent_subscription_events (tenant_id, source_address, event_id, source_name, source_version,
3916
+ matching_key, payload_digest, cutoff, cursor, routing_complete, next_attempt_at_millis, record_json)
3917
+ VALUES (${partition.tenantId}, ${partition.address}, ${accepted.eventId}, ${accepted.source.name}, ${accepted.source.version},
3918
+ ${accepted.matchingKey}, ${accepted.payloadDigest}, ${accepted.cutoff}, ${accepted.cursor}, 0, ${accepted.nextAttemptAtMillis}, ${json})
3919
+ `, "insert event");
3920
+ return {
3921
+ value: accepted,
3922
+ changed: true
3923
+ };
3924
+ }));
3925
+ if (result.changed) yield* failpoint.hit("subscription:accept:after");
3926
+ return result.value;
3927
+ });
3928
+ const event = Effect.fn("SqliteSubscriptionStore.event")((eventId) => readEvent(eventId, "get event"));
3929
+ const pendingEvents = Effect.fn("SqliteSubscriptionStore.pendingEvents")(function* (nowMillis, after, limit) {
3930
+ const rows = yield* query(sql`
3931
+ SELECT event_id FROM effect_agent_subscription_events WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3932
+ AND routing_complete=0 AND next_attempt_at_millis<=${nowMillis} AND event_id>${after} ORDER BY event_id LIMIT ${limit}
3933
+ `, "pending events");
3934
+ return yield* decodeRows(Schema.Struct({ event_id: Schema.String }), rows, "pending event keys").pipe(Effect.map((items) => items.map((item) => item.event_id)));
3935
+ });
3936
+ const candidates = Effect.fn("SqliteSubscriptionStore.candidates")(function* (input, limit) {
3937
+ const supplied = yield* validate(AcceptedEvent, input, "candidates-event");
3938
+ yield* requirePartition(supplied.partition, "candidates-partition");
3939
+ const stored = yield* readEvent(supplied.eventId, "candidates event");
3940
+ if (stored === null) return yield* error("not-found", "event");
3941
+ if (!sameAcceptedEventIdentity(stored, supplied)) return yield* error("conflict", "event");
3942
+ const rows = yield* query(sql`
3943
+ SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3944
+ AND source_name=${stored.source.name} AND source_version=${stored.source.version} AND matching_key=${stored.matchingKey}
3945
+ AND ordinal>${stored.cursor} AND ordinal<=${stored.cutoff} ORDER BY ordinal LIMIT ${limit}
3946
+ `, "subscription candidates");
3947
+ const decoded = yield* decodeRows(Schema.Struct({
3948
+ owner_id: Schema.String,
3949
+ subscription_id: Schema.String,
3950
+ ordinal: Schema.Natural
3951
+ }), rows, "subscription candidates");
3952
+ return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.candidateRecord")(function* (row) {
3953
+ const record = yield* readRegistration({
3954
+ partition,
3955
+ ownerId: row.owner_id,
3956
+ subscriptionId: row.subscription_id
3957
+ }, "subscription candidate");
3958
+ if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("subscription candidate projection");
3959
+ return record;
3960
+ }));
3961
+ });
3962
+ const insertDelivery = Effect.fn("SqliteSubscriptionStore.insertDelivery")(function* (delivery) {
3963
+ const json = yield* encode(SubscriptionDelivery, delivery, "encode selected delivery");
3964
+ yield* query(sql`
3965
+ INSERT INTO effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, event_id,
3966
+ delivery_key, state, next_attempt_at_millis, record_json)
3967
+ VALUES (${partition.tenantId}, ${partition.address}, ${delivery.key.subscription.ownerId}, ${delivery.key.subscription.subscriptionId},
3968
+ ${delivery.key.eventId}, ${subscriptionDeliveryKeyString(delivery.key)}, ${delivery.state}, ${delivery.retry.nextAttemptAtMillis}, ${json})
3969
+ `, "insert delivery");
3970
+ });
3971
+ const select = Effect.fn("SqliteSubscriptionStore.select")(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
3972
+ const supplied = yield* validate(AcceptedEvent, inputEvent, "select-event");
3973
+ const deliveries = yield* validate(Schema.Array(SubscriptionDelivery), inputDeliveries, "select-deliveries");
3974
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
3975
+ yield* requirePartition(supplied.partition, "select-partition");
3976
+ for (const candidate of deliveries) yield* requirePartition(candidate.key.subscription.partition, "select-delivery-partition");
3977
+ if (yield* transact(Effect.gen(function* () {
3978
+ const accepted = yield* readEvent(supplied.eventId, "select event");
3979
+ if (accepted === null) return yield* error("not-found", "event");
3980
+ if (!sameAcceptedEventIdentity(accepted, supplied) || accepted.cursor !== supplied.cursor) return yield* error("conflict", "event-cursor");
3981
+ if (accepted.routingComplete) return false;
3982
+ if (!Number.isSafeInteger(cursor) || cursor < accepted.cursor || cursor > accepted.cutoff) return yield* error("validation", "cursor");
3983
+ yield* failpoint.hit("subscription:select:before");
3984
+ const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
3985
+ const additions = [];
3986
+ for (const delivery of deliveries) {
3987
+ const record = yield* readRegistration(delivery.key.subscription, "select registration");
3988
+ if (record === null) return yield* error("not-found", "subscription");
3989
+ if (!subscriptionDeliveryCanSelect(delivery, record, accepted) || delivery.key.eventId !== accepted.eventId || delivery.source.name !== accepted.source.name || delivery.source.version !== accepted.source.version || record.ordinal <= accepted.cursor || record.ordinal > cursor) return yield* error("conflict", "selection");
3990
+ const existing = yield* readDelivery(delivery.key, "select existing delivery");
3991
+ if (existing !== null) {
3992
+ if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
3993
+ continue;
3994
+ }
3995
+ if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, false)) continue;
3996
+ additions.push({
3997
+ delivery,
3998
+ record
3999
+ });
4000
+ }
4001
+ if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count deliveries")) + additions.length > limits.maxDeliveries) return yield* error("capacity", "deliveries");
4002
+ for (const ownerId of new Set(additions.map(({ record }) => record.key.ownerId))) if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${ownerId}`, "count owner deliveries")) + additions.filter(({ record }) => record.key.ownerId === ownerId).length > limits.maxDeliveriesPerOwner) return yield* error("capacity", "owner-deliveries");
4003
+ for (const addition of additions) {
4004
+ yield* insertDelivery(addition.delivery);
4005
+ if (addition.record.configuration.mode === "once") yield* writeRegistration({
4006
+ ...addition.record,
4007
+ state: "consumed",
4008
+ recovery: null
4009
+ });
4010
+ }
4011
+ yield* writeEvent({
4012
+ ...accepted,
4013
+ cursor,
4014
+ routingComplete: complete,
4015
+ routingFailure: null
4016
+ });
4017
+ return true;
4018
+ }))) yield* failpoint.hit("subscription:select:after");
4019
+ });
4020
+ const catchUp = Effect.fn("SqliteSubscriptionStore.catchUp")(function* (inputEvent, inputDelivery, nowMillis, inputLimits) {
4021
+ const supplied = yield* validate(AcceptedEvent, inputEvent, "catch-up-event");
4022
+ const delivery = yield* validate(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
4023
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "catch-up-limits");
4024
+ yield* requirePartition(supplied.partition, "catch-up-partition");
4025
+ yield* requirePartition(delivery.key.subscription.partition, "catch-up-delivery-partition");
4026
+ if (yield* transact(Effect.gen(function* () {
4027
+ const accepted = yield* readEvent(supplied.eventId, "catch-up event");
4028
+ const record = yield* readRegistration(delivery.key.subscription, "catch-up subscription");
4029
+ if (accepted === null || record === null) return yield* error("not-found", accepted === null ? "event" : "subscription");
4030
+ if (!sameAcceptedEventIdentity(accepted, supplied) || !subscriptionDeliveryCanSelect(delivery, record, accepted) || delivery.key.eventId !== accepted.eventId || delivery.source.name !== accepted.source.name || delivery.source.version !== accepted.source.version || record.configuration.mode !== "once") return yield* error("conflict", "catch-up-identity");
4031
+ const existing = yield* readDelivery(delivery.key, "catch-up existing delivery");
4032
+ if (existing !== null) {
4033
+ if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
4034
+ return false;
4035
+ }
4036
+ yield* failpoint.hit("subscription:catch-up:before");
4037
+ const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
4038
+ if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, true)) return yield* error("conflict", "catch-up-eligibility");
4039
+ if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count deliveries")) >= limits.maxDeliveries) return yield* error("capacity", "deliveries");
4040
+ if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${record.key.ownerId}`, "count owner deliveries")) >= limits.maxDeliveriesPerOwner) return yield* error("capacity", "owner-deliveries");
4041
+ yield* insertDelivery(delivery);
4042
+ yield* writeRegistration({
4043
+ ...record,
4044
+ state: "consumed",
4045
+ recovery: null
4046
+ });
4047
+ return true;
4048
+ }))) yield* failpoint.hit("subscription:catch-up:after");
4049
+ });
4050
+ const deferEvent = Effect.fn("SqliteSubscriptionStore.deferEvent")(function* (eventId, nextAttemptAtMillis, code) {
4051
+ const routingFailure = code === void 0 ? "routing-failed" : yield* validate(SubscriptionName, code, "routing-failure");
4052
+ yield* transact(Effect.gen(function* () {
4053
+ const accepted = yield* readEvent(eventId, "defer event");
4054
+ if (accepted === null) return yield* error("not-found", "event");
4055
+ yield* failpoint.hit("subscription:defer-event:before");
4056
+ yield* writeEvent({
4057
+ ...accepted,
4058
+ nextAttemptAtMillis,
4059
+ routingFailure
4060
+ });
4061
+ }));
4062
+ yield* failpoint.hit("subscription:defer-event:after");
4063
+ });
4064
+ const delivery = Effect.fn("SqliteSubscriptionStore.delivery")(function* (input) {
4065
+ const key = yield* validate(SubscriptionDeliveryKey, input, "delivery-key");
4066
+ yield* requirePartition(key.subscription.partition, "delivery-partition");
4067
+ return yield* readDelivery(key, "get delivery");
4068
+ });
4069
+ const pendingDeliveries = Effect.fn("SqliteSubscriptionStore.pendingDeliveries")(function* (nowMillis, after, limit) {
4070
+ const rows = yield* query(sql`
4071
+ SELECT owner_id, subscription_id, event_id FROM effect_agent_subscription_deliveries
4072
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
4073
+ AND next_attempt_at_millis<=${nowMillis} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
4074
+ `, "pending deliveries");
4075
+ const rowSchema = Schema.Struct({
4076
+ owner_id: Schema.String,
4077
+ subscription_id: Schema.String,
4078
+ event_id: Schema.String
4079
+ });
4080
+ return yield* decodeRows(rowSchema, rows, "pending delivery keys").pipe(Effect.map((items) => items.map((item) => ({
4081
+ subscription: {
4082
+ partition,
4083
+ ownerId: item.owner_id,
4084
+ subscriptionId: item.subscription_id
4085
+ },
4086
+ eventId: item.event_id
4087
+ }))));
4088
+ });
4089
+ const listDeliveries = Effect.fn("SqliteSubscriptionStore.listDeliveries")(function* (input, after, limit) {
4090
+ const key = yield* requireKey(input, "list-deliveries-key");
4091
+ const rows = yield* query(sql`
4092
+ SELECT record_json FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
4093
+ AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
4094
+ `, "list deliveries");
4095
+ const decoded = yield* decodeRows(JsonRow, rows, "list deliveries");
4096
+ return yield* Effect.forEach(decoded, (row) => decode(SubscriptionDelivery, row.record_json, "list delivery"));
4097
+ });
4098
+ const changeDelivery = Effect.fn("SqliteSubscriptionStore.changeDelivery")(function* (inputKey, inputDeliveryId, inputChange) {
4099
+ const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
4100
+ const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
4101
+ const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
4102
+ yield* requirePartition(key.subscription.partition, "change-delivery-partition");
4103
+ const result = yield* transact(Effect.gen(function* () {
4104
+ const existing = yield* readDelivery(key, "change delivery");
4105
+ const record = yield* readRegistration(key.subscription, "change delivery subscription");
4106
+ if (existing === null || record === null) return yield* error("not-found", existing === null ? "delivery" : "subscription");
4107
+ yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
4108
+ const effectiveChange = change._tag === "Prepare" ? {
4109
+ ...change,
4110
+ nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis)
4111
+ } : change;
4112
+ const transition = applySubscriptionDeliveryChange(existing, record, deliveryId, effectiveChange);
4113
+ if (Result.isFailure(transition)) return yield* transition.failure;
4114
+ if (transition.success === existing) return {
4115
+ value: existing,
4116
+ changed: false
4117
+ };
4118
+ yield* writeDelivery(transition.success);
4119
+ return {
4120
+ value: transition.success,
4121
+ changed: true
4122
+ };
4123
+ }));
4124
+ if (result.changed) yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
4125
+ return result.value;
4126
+ });
4127
+ const recovering = Effect.fn("SqliteSubscriptionStore.recovering")(function* (nowMillis, after, limit) {
4128
+ const rows = yield* query(sql`
4129
+ SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions
4130
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active'
4131
+ AND recovery_at_millis IS NOT NULL AND recovery_at_millis<=${nowMillis} AND ordinal>${after}
4132
+ ORDER BY ordinal LIMIT ${limit}
4133
+ `, "recovering subscriptions");
4134
+ const rowSchema = Schema.Struct({
4135
+ owner_id: Schema.String,
4136
+ subscription_id: Schema.String,
4137
+ ordinal: Schema.Natural
4138
+ });
4139
+ return yield* decodeRows(rowSchema, rows, "recovering subscription keys").pipe(Effect.map((items) => items.map((item) => ({
4140
+ key: {
4141
+ partition,
4142
+ ownerId: item.owner_id,
4143
+ subscriptionId: item.subscription_id
4144
+ },
4145
+ ordinal: item.ordinal
4146
+ }))));
4147
+ });
4148
+ const deferRecovery = Effect.fn("SqliteSubscriptionStore.deferRecovery")(function* (input, recovery) {
4149
+ const key = yield* requireKey(input, "defer-recovery-key");
4150
+ yield* transact(Effect.gen(function* () {
4151
+ const record = yield* readRegistration(key, "defer recovery");
4152
+ if (record === null) return yield* error("not-found", "subscription");
4153
+ yield* failpoint.hit("subscription:defer-recovery:before");
4154
+ yield* writeRegistration({
4155
+ ...record,
4156
+ recovery: record.state === "active" ? recovery : null
4157
+ });
4158
+ }));
4159
+ yield* failpoint.hit("subscription:defer-recovery:after");
4160
+ });
4161
+ const readScanCursors = Effect.gen(function* () {
4162
+ const rows = yield* query(sql`
4163
+ SELECT event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
4164
+ FROM effect_agent_subscription_sequences
4165
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
4166
+ `, "read subscription scan cursors");
4167
+ const decoded = yield* decodeRows(ScanRow, rows, "read subscription scan cursors");
4168
+ if (decoded.length !== 1) return yield* corrupt("subscription scan cursors");
4169
+ return {
4170
+ events: decoded[0].event_scan_cursor,
4171
+ deliveries: decoded[0].delivery_scan_cursor,
4172
+ recovery: decoded[0].recovery_scan_cursor
4173
+ };
4174
+ });
4175
+ const advanceScanCursors = Effect.fn("SqliteSubscriptionStore.advanceScanCursors")(function* (input) {
4176
+ const cursors = yield* validate(SubscriptionScanCursors, input, "scan-cursors");
4177
+ yield* transact(Effect.gen(function* () {
4178
+ yield* failpoint.hit("subscription:advance-scan-cursors:before");
4179
+ yield* query(sql`
4180
+ UPDATE effect_agent_subscription_sequences
4181
+ SET event_scan_cursor=${cursors.events}, delivery_scan_cursor=${cursors.deliveries}, recovery_scan_cursor=${cursors.recovery}
4182
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
4183
+ `, "advance subscription scan cursors");
4184
+ }));
4185
+ yield* failpoint.hit("subscription:advance-scan-cursors:after");
4186
+ });
4187
+ const indexedDeadline = query(sql`
4188
+ SELECT MIN(deadline) AS deadline FROM (
4189
+ SELECT next_attempt_at_millis AS deadline FROM effect_agent_subscription_events
4190
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND routing_complete=0
4191
+ UNION ALL SELECT next_attempt_at_millis FROM effect_agent_subscription_deliveries
4192
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
4193
+ UNION ALL SELECT recovery_at_millis FROM effect_agent_subscriptions
4194
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active' AND recovery_at_millis IS NOT NULL
4195
+ )
4196
+ `, "next subscription deadline").pipe(Effect.flatMap((rows) => decodeRows(Schema.Struct({ deadline: Schema.NullOr(Schema.Number) }), rows, "next subscription deadline")), Effect.flatMap((rows) => rows.length === 1 ? Effect.succeed(rows[0].deadline) : Effect.fail(corrupt("next subscription deadline"))));
4197
+ const nextDeadline = Effect.gen(function* () {
4198
+ const cursors = yield* readScanCursors;
4199
+ if (cursors.events !== "" || cursors.deliveries !== "" || cursors.recovery !== 0) return 0;
4200
+ return yield* indexedDeadline;
4201
+ });
4202
+ const prearm = Effect.fn("DoSubscriptionStore.prearm")(function* (deadlineAtMillis) {
4203
+ yield* transactions.run((replace) => replaceAlarm(replace, deadlineAtMillis));
4204
+ yield* failpoint.hit("subscription:prearm:after");
4205
+ });
4206
+ const reconcile = Effect.gen(function* () {
4207
+ yield* transactions.run((replace) => Effect.gen(function* () {
4208
+ yield* replaceAlarm(replace, yield* readIndexedDeadline());
4209
+ }));
4210
+ yield* failpoint.hit("subscription:reconcile:after");
4211
+ });
4212
+ return Context.make(SubscriptionStore, {
4213
+ partition,
4214
+ register,
4215
+ get,
4216
+ list,
4217
+ cancel,
4218
+ accept,
4219
+ event,
4220
+ pendingEvents,
4221
+ candidates,
4222
+ select,
4223
+ catchUp,
4224
+ deferEvent,
4225
+ delivery,
4226
+ pendingDeliveries,
4227
+ listDeliveries,
4228
+ changeDelivery,
4229
+ recovering,
4230
+ deferRecovery,
4231
+ readScanCursors,
4232
+ advanceScanCursors,
4233
+ nextDeadline
4234
+ }).pipe(Context.add(DoSubscriptionAlarmControl, {
4235
+ prearm,
4236
+ reconcile
4237
+ }));
4238
+ });
4239
+ const doSubscriptionStoreLayer = (partition) => Layer.effectContext(makeSubscriptionStore(partition));
4240
+ //#endregion
3574
4241
  //#region src/port-protocol.ts
3575
4242
  /**
3576
4243
  * The cross-Durable-Object port protocol (plan §1.3, D-P6-3): Schema request/response/error
3577
- * envelopes for the CLOSED route-capable subset of the session ports. One Conversation's
3578
- * Durable Object executes another Conversation's request against its OWN local facets; the
4244
+ * envelopes for the CLOSED route-capable subset of the thread ports. One Thread's
4245
+ * Durable Object executes another Thread's request against its OWN local facets; the
3579
4246
  * envelopes here are the only values that cross the Object boundary, and they are
3580
4247
  * transport-agnostic — native Durable Object JS RPC is the shipped carrier, fetch-with-JSON
3581
4248
  * the documented fallback, and both move the same Schema-encoded JSON.
3582
4249
  *
3583
4250
  * The closed subset is exactly the set of operations the durable coordinator performs against
3584
- * a FOREIGN Conversation (parent/child establishment, status checks, abort propagation,
3585
- * child-settlement notification, and the child-conversation store operations used by
4251
+ * a FOREIGN Thread (parent/child establishment, status checks, abort propagation,
4252
+ * child-settlement notification, and the child-thread store operations used by
3586
4253
  * establishment, `verifySettledChild`, and result projection):
3587
4254
  *
3588
4255
  * - ledger: `admit`, `markReady`, `lookup`, `resolveAdmission`, `requestAbort`,
@@ -3595,18 +4262,18 @@ const scheduleStoreLayer = Layer.effectContext(makeServices);
3595
4262
  *
3596
4263
  * Failures cross the boundary as the `PortFailure` union and re-decode on the caller side to
3597
4264
  * the SAME tagged error types the local facet would have produced, so routed calls keep
3598
- * error-tag fidelity. `cause` chains inside `LedgerError`/`ConversationStoreError` travel as
4265
+ * error-tag fidelity. `cause` chains inside `LedgerError`/`ThreadStoreError` travel as
3599
4266
  * Schema defects and do not claim instance fidelity across Objects (plan §2.8).
3600
4267
  */
3601
4268
  /** Ceiling for protocol diagnostic strings; matches `AdmissionIndeterminate.reason`. */
3602
4269
  const MAX_PORT_DIAGNOSTIC_LENGTH = 4096;
3603
4270
  const BoundedDiagnostic = Schema.String.check(Schema.isMaxLength(MAX_PORT_DIAGNOSTIC_LENGTH));
3604
4271
  /** Truncate a diagnostic string to the protocol's bounded diagnostic length. */
3605
- const boundPortDiagnostic = (value) => value.length > 4096 ? `${value.slice(0, MAX_PORT_DIAGNOSTIC_LENGTH - 3)}...` : value;
4272
+ const boundPortDiagnostic = (value) => value.length > 4096 ? `${value.slice(0, 4093)}...` : value;
3606
4273
  /**
3607
4274
  * The envelope itself could not be honored: the receiving Object could not decode the
3608
4275
  * request, or a response could not be encoded/decoded. It never carries port semantics —
3609
- * callers fold it into the operation's base error (`LedgerError`/`ConversationStoreError`),
4276
+ * callers fold it into the operation's base error (`LedgerError`/`ThreadStoreError`),
3610
4277
  * except `resolveAdmission`, which folds it into `AdmissionIndeterminate` because a
3611
4278
  * non-answer is never proof of absence (SUB-031).
3612
4279
  */
@@ -3623,16 +4290,16 @@ var LedgerResolveAdmissionCall = class extends Schema.TaggedClass("@effect-agent
3623
4290
  var LedgerRequestAbortCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerRequestAbortCall")("LedgerRequestAbort", { request: AbortCommand }) {};
3624
4291
  /** Routed `SubmissionLedger.recordChildSettled` — the child→parent durable notification. */
3625
4292
  var LedgerRecordChildSettledCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerRecordChildSettledCall")("LedgerRecordChildSettled", { request: ChildSettledNotification }) {};
3626
- /** Routed `ConversationStore.materialize` against the owning Object. */
3627
- var StoreMaterializeCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreMaterializeCall")("StoreMaterialize", { request: ConversationMaterialization }) {};
3628
- /** Routed `ConversationStore.append` against the owning Object. */
4293
+ /** Routed `ThreadStore.materialize` against the owning Object. */
4294
+ var StoreMaterializeCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreMaterializeCall")("StoreMaterialize", { request: ThreadMaterialization }) {};
4295
+ /** Routed `ThreadStore.append` against the owning Object. */
3629
4296
  var StoreAppendCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreAppendCall")("StoreAppend", { request: FencedAppendRequest }) {};
3630
- /** Routed one-page `ConversationStore.read`; the page bound is the request's own `limit`. */
3631
- var StoreReadPageCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreReadPageCall")("StoreReadPage", { request: ConversationRead }) {};
3632
- /** Routed `ConversationStore.inspectTail` against the owning Object. */
3633
- var StoreInspectTailCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailCall")("StoreInspectTail", { request: ConversationTailRequest }) {};
3634
- /** Routed `ConversationStore.export` against the owning Object. */
3635
- var StoreExportCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportCall")("StoreExport", { request: ConversationExportRequest }) {};
4297
+ /** Routed one-page `ThreadStore.read`; the page bound is the request's own `limit`. */
4298
+ var StoreReadPageCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreReadPageCall")("StoreReadPage", { request: ThreadRead }) {};
4299
+ /** Routed `ThreadStore.inspectTail` against the owning Object. */
4300
+ var StoreInspectTailCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailCall")("StoreInspectTail", { request: ThreadTailRequest }) {};
4301
+ /** Routed `ThreadStore.export` against the owning Object. */
4302
+ var StoreExportCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportCall")("StoreExport", { request: ThreadExportRequest }) {};
3636
4303
  /** Every request that may cross a Durable Object boundary — the CLOSED route-capable subset. */
3637
4304
  const PortRequest = Schema.Union([
3638
4305
  LedgerAdmitCall,
@@ -3658,8 +4325,8 @@ var StoreMaterializeResult = class extends Schema.TaggedClass("@effect-agent/sto
3658
4325
  var StoreAppendResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreAppendResult")("StoreAppendResult", { result: AppendResult }) {};
3659
4326
  /** One page of canonical records, bounded by the request's `limit` (≤ 1,024). */
3660
4327
  var StoreReadPageResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreReadPageResult")("StoreReadPageResult", { records: Schema.Array(CanonicalRecordEnvelope).check(Schema.isMaxLength(1024)) }) {};
3661
- var StoreInspectTailResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailResult")("StoreInspectTailResult", { tail: ConversationTail }) {};
3662
- var StoreExportResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportResult")("StoreExportResult", { export: ConversationExport }) {};
4328
+ var StoreInspectTailResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailResult")("StoreInspectTailResult", { tail: ThreadTail }) {};
4329
+ var StoreExportResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportResult")("StoreExportResult", { export: ThreadExport }) {};
3663
4330
  /** Every successful routed result. Callers narrow by the tag their request implies. */
3664
4331
  const PortResult = Schema.Union([
3665
4332
  LedgerAdmitResult,
@@ -3677,15 +4344,15 @@ const PortResult = Schema.Union([
3677
4344
  /**
3678
4345
  * Every typed failure a route-capable operation can produce on its owning Object, plus the
3679
4346
  * protocol's own `PortProtocolError`. Members re-decode to the SAME tagged classes the
3680
- * session ports declare, so a routed caller observes identical error tags and fields.
4347
+ * thread ports declare, so a routed caller observes identical error tags and fields.
3681
4348
  */
3682
4349
  const PortFailure = Schema.Union([
3683
4350
  AdmissionConflict,
3684
4351
  SettlementConflict,
3685
4352
  JoinedToHost,
3686
4353
  LedgerError,
3687
- ConversationStoreError,
3688
- ConversationNotMaterialized,
4354
+ ThreadStoreError,
4355
+ ThreadNotMaterialized,
3689
4356
  AppendConflict,
3690
4357
  FenceRejected,
3691
4358
  PortProtocolError
@@ -3702,8 +4369,8 @@ const encodePortResponse = Schema.encodeEffect(PortResponse);
3702
4369
  const decodePortResponse = Schema.decodeUnknownEffect(PortResponse);
3703
4370
  //#endregion
3704
4371
  //#region src/routing.ts
3705
- const ConversationIdSchema = ConversationMaterialization.fields.conversationId;
3706
- const decodeConversationId = Schema.decodeUnknownEffect(ConversationIdSchema);
4372
+ const ThreadIdSchema = ThreadMaterialization.fields.threadId;
4373
+ const decodeThreadId = Schema.decodeUnknownEffect(ThreadIdSchema);
3707
4374
  /**
3708
4375
  * The ledger row bound routable Submission identities must respect (mirrors the local
3709
4376
  * facet's `MAX_IDENTIFIER_LENGTH`; the minting side already refuses longer identities typed
@@ -3714,7 +4381,7 @@ const MAX_ROUTABLE_SUBMISSION_ID_LENGTH = 1024;
3714
4381
  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}$/;
3715
4382
  /**
3716
4383
  * A transport could not deliver a port request to (or an answer from) the owning
3717
- * Conversation's Durable Object. `retryable` carries the platform's own stub signal when one
4384
+ * Thread's Durable Object. `retryable` carries the platform's own stub signal when one
3718
4385
  * exists. This error never crosses the wire — it is the CALLER-side evidence that the
3719
4386
  * authority was unreachable, which is exactly the case `AdmissionIndeterminate` was
3720
4387
  * specified for (SUB-031).
@@ -3757,24 +4424,24 @@ const portTransportFailure = (target, cause) => {
3757
4424
  };
3758
4425
  /**
3759
4426
  * Delivery of Schema-encoded port envelopes to the Durable Object that owns a FOREIGN
3760
- * Conversation (plan §1.3, D-P6-3). The shipped implementation (platform-cloudflare, WP3)
4427
+ * Thread (plan §1.3, D-P6-3). The shipped implementation (platform-cloudflare, WP3)
3761
4428
  * calls the owner's `portCall` over native Durable Object JS RPC via
3762
- * `namespace.idFromName(conversationId)`; the protocol is transport-agnostic and any carrier
4429
+ * `namespace.idFromName(threadId)`; the protocol is transport-agnostic and any carrier
3763
4430
  * that moves the encoded envelopes verbatim satisfies this service. Implementations MUST
3764
4431
  * surface every delivery problem as `PortTransportError` and must never fabricate an answer.
3765
4432
  */
3766
- var ConversationPortTransport = class extends Context.Service()("@effect-agent/storage-cloudflare/ConversationPortTransport") {};
4433
+ var ThreadPortTransport = class extends Context.Service()("@effect-agent/storage-cloudflare/ThreadPortTransport") {};
3767
4434
  const LOCAL = { _tag: "local" };
3768
4435
  /**
3769
- * Parse a DC-minted routable Submission identity — `{uuidv7}:{conversationId}`, split at the
3770
- * FIRST `:` because the Conversation tail may itself contain colons (D-P6-5). This adapter
4436
+ * Parse a DC-minted routable Submission identity — `{uuidv7}:{threadId}`, split at the
4437
+ * FIRST `:` because the Thread tail may itself contain colons (D-P6-5). This adapter
3771
4438
  * minted the format at admission and is the ONLY component that parses it; identities that do
3772
4439
  * not carry the minted shape (no separator, non-UUID head, empty tail) fall back to the local
3773
4440
  * facet, which is the only authority this Object can consult without inventing an owner.
3774
4441
  * Identities beyond the ledger's 1,024-character row bound fail typed: the minting side
3775
4442
  * refused them at admission, so they cannot name any stored row anywhere.
3776
4443
  */
3777
- const routableSubmissionTarget = (localConversationId) => Effect.fn("DoPortRouting.routableSubmissionTarget")(function* (operation, submissionId) {
4444
+ const routableSubmissionTarget = (localThreadId) => Effect.fn("DoPortRouting.routableSubmissionTarget")(function* (operation, submissionId) {
3778
4445
  if (submissionId.length > MAX_ROUTABLE_SUBMISSION_ID_LENGTH) return yield* LedgerError.make({
3779
4446
  operation,
3780
4447
  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.`
@@ -3783,16 +4450,16 @@ const routableSubmissionTarget = (localConversationId) => Effect.fn("DoPortRouti
3783
4450
  if (separator === -1) return LOCAL;
3784
4451
  if (!UUID_HEAD_PATTERN.test(submissionId.slice(0, separator))) return LOCAL;
3785
4452
  const tail = submissionId.slice(separator + 1);
3786
- if (tail === localConversationId) return LOCAL;
3787
- return yield* decodeConversationId(tail).pipe(Effect.map((conversationId) => ({
4453
+ if (tail === localThreadId) return LOCAL;
4454
+ return yield* decodeThreadId(tail).pipe(Effect.map((threadId) => ({
3788
4455
  _tag: "foreign",
3789
- conversationId
4456
+ threadId
3790
4457
  })), Effect.orElseSucceed(() => LOCAL));
3791
4458
  });
3792
4459
  const NoAdditionalPortFailure = Schema.Never;
3793
4460
  const AbortPortFailure = Schema.Union([SettlementConflict, JoinedToHost]);
3794
4461
  const AppendPortFailure = Schema.Union([
3795
- ConversationNotMaterialized,
4462
+ ThreadNotMaterialized,
3796
4463
  AppendConflict,
3797
4464
  FenceRejected
3798
4465
  ]);
@@ -3800,26 +4467,27 @@ const AppendPortFailure = Schema.Union([
3800
4467
  * The fail-fast refusal for any foreign operation OUTSIDE the closed route-capable subset
3801
4468
  * (plan §1.3): honesty over accidental distribution.
3802
4469
  */
3803
- const crossConversationLedgerError = (operation, target) => LedgerError.make({
4470
+ const crossThreadLedgerError = (operation, target) => LedgerError.make({
3804
4471
  operation,
3805
- 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.`
4472
+ message: `${operation} addressed to foreign Thread ${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 Thread's Durable Object.`
3806
4473
  });
3807
- const crossConversationStoreError = (operation, target) => ConversationStoreError.make({
4474
+ const crossThreadStoreError = (operation, target) => ThreadStoreError.make({
3808
4475
  operation,
3809
- 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.`
4476
+ message: `${operation} addressed to foreign Thread ${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 Thread's Durable Object.`
3810
4477
  });
3811
4478
  const makeTransportCall = (transport) => Effect.fn("DoPortRouting.transportCall")(function* (target, call) {
3812
4479
  const encoded = yield* encodePortRequest(call).pipe(Effect.mapError((error) => PortProtocolError.make({ message: boundPortDiagnostic(`The port request could not be encoded: ${error.message}`) })));
3813
- 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}`) })));
4480
+ const raw = yield* transport.call(target, encoded);
4481
+ return yield* decodePortResponse(raw).pipe(Effect.mapError((error) => PortProtocolError.make({ message: boundPortDiagnostic(`The port response could not be decoded: ${error.message}`) })));
3814
4482
  });
3815
4483
  const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServices")(function* (options) {
3816
4484
  const local = yield* SubmissionLedger;
3817
- const transport = yield* ConversationPortTransport;
4485
+ const transport = yield* ThreadPortTransport;
3818
4486
  const transportCall = makeTransportCall(transport);
3819
- const submissionTarget = routableSubmissionTarget(options.localConversationId);
4487
+ const submissionTarget = routableSubmissionTarget(options.localThreadId);
3820
4488
  const routeFailure = (operation, target) => (error) => LedgerError.make({
3821
4489
  operation,
3822
- message: boundPortDiagnostic(`Routed ${operation} to the Conversation Object owning ${target} failed: ${error.message}`),
4490
+ message: boundPortDiagnostic(`Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`),
3823
4491
  cause: error
3824
4492
  });
3825
4493
  /**
@@ -3838,14 +4506,14 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
3838
4506
  if (failure._tag === "LedgerError") return Effect.fail(failure);
3839
4507
  return Effect.fail(LedgerError.make({
3840
4508
  operation,
3841
- message: boundPortDiagnostic(`The Conversation Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
4509
+ message: boundPortDiagnostic(`The Thread Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
3842
4510
  cause: failure
3843
4511
  }));
3844
4512
  }
3845
4513
  const result = response.result;
3846
4514
  if (!isExpectedResult(result)) return Effect.fail(LedgerError.make({
3847
4515
  operation,
3848
- message: `The Conversation Object owning ${target} answered ${operation} with the mismatched result ${result._tag}.`
4516
+ message: `The Thread Object owning ${target} answered ${operation} with the mismatched result ${result._tag}.`
3849
4517
  }));
3850
4518
  return Effect.succeed(result);
3851
4519
  }), Effect.withSpan("DoPortRouting.foreignLedgerCall", { attributes: {
@@ -3864,13 +4532,13 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
3864
4532
  const resolveForeignAdmission = (target, request) => transportCall(target, LedgerResolveAdmissionCall.make({ request })).pipe(Effect.flatMap((response) => {
3865
4533
  if (response._tag === "PortFailed") {
3866
4534
  if (response.failure._tag === "LedgerError") return Effect.fail(response.failure);
3867
- 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}`) }));
4535
+ return Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Thread Object owning ${target} answered resolveAdmission with the out-of-contract failure ${response.failure._tag}: ${response.failure.message}`) }));
3868
4536
  }
3869
- 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}.`) }));
4537
+ if (response.result._tag !== "LedgerResolveAdmissionResult") return Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Thread Object owning ${target} answered resolveAdmission with the mismatched result ${response.result._tag}.`) }));
3870
4538
  return Effect.succeed(response.result.resolution);
3871
4539
  }), Effect.catchTags({
3872
- PortTransportError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Conversation Object owning ${target} is unreachable: ${error.message}`) })),
3873
- PortProtocolError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The answer of the Conversation Object owning ${target} could not be understood: ${error.message}`) }))
4540
+ PortTransportError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Thread Object owning ${target} is unreachable: ${error.message}`) })),
4541
+ PortProtocolError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The answer of the Thread Object owning ${target} could not be understood: ${error.message}`) }))
3874
4542
  }), Effect.withSpan("DoPortRouting.resolveForeignAdmission", { attributes: { target } }));
3875
4543
  const foreignLookupById = (operation, target, submissionId) => foreignLedgerCall(operation, target, LedgerLookupCall.make({ request: SubmissionLookupById.make({ submissionId }) }), LedgerLookupResult, NoAdditionalPortFailure).pipe(Effect.map((result) => result.submission === void 0 ? Option.none() : Option.some(result.submission)));
3876
4544
  /**
@@ -3889,7 +4557,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
3889
4557
  if (childSubmissionId === void 0 || attachments.has(childSubmissionId)) continue;
3890
4558
  const target = yield* submissionTarget(operation, childSubmissionId);
3891
4559
  if (target._tag !== "foreign") continue;
3892
- const child = yield* foreignLookupById(operation, target.conversationId, childSubmissionId);
4560
+ const child = yield* foreignLookupById(operation, target.threadId, childSubmissionId);
3893
4561
  if (Option.isNone(child)) continue;
3894
4562
  attachments.set(childSubmissionId, ChildAttachmentSnapshot.make({
3895
4563
  toolCallId: reservation.parentToolCallId,
@@ -3913,44 +4581,45 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
3913
4581
  });
3914
4582
  const routed = SubmissionLedger.of({
3915
4583
  capabilities: local.capabilities,
3916
- admit: (request) => request.conversationId === options.localConversationId ? local.admit(request) : foreignLedgerCall("ledger admit", request.conversationId, LedgerAdmitCall.make({ request }), LedgerAdmitResult, AdmissionConflict).pipe(Effect.map((reply) => reply.result)),
3917
- 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, NoAdditionalPortFailure).pipe(Effect.asVoid))),
3918
- 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, NoAdditionalPortFailure).pipe(Effect.map((result) => result.submission === void 0 ? Option.none() : Option.some(result.submission))),
3919
- resolveAdmission: (request) => request.conversationId === options.localConversationId ? local.resolveAdmission(request) : resolveForeignAdmission(request.conversationId, request),
3920
- 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, AbortPortFailure).pipe(Effect.map((reply) => reply.intent)))),
3921
- 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, NoAdditionalPortFailure).pipe(Effect.map((reply) => reply.outcome)))),
3922
- claim: (request) => request.conversationId === options.localConversationId ? local.claim(request) : Effect.fail(crossConversationLedgerError("ledger claim", request.conversationId)),
3923
- claimJoining: (request) => request.conversationId === options.localConversationId ? local.claimJoining(request) : Effect.fail(crossConversationLedgerError("ledger claim joining", request.conversationId)),
3924
- 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)))),
3925
- 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)))),
3926
- 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)))),
3927
- 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)))),
3928
- 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)))),
3929
- 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)))),
3930
- 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)))),
3931
- suspend: (request) => submissionTarget("ledger suspend", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.suspend(request) : Effect.fail(crossConversationLedgerError("ledger suspend", target.conversationId)))),
3932
- 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)))),
3933
- 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)))),
3934
- 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)))),
3935
- 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)))),
4584
+ admit: (request) => request.threadId === options.localThreadId ? local.admit(request) : foreignLedgerCall("ledger admit", request.threadId, LedgerAdmitCall.make({ request }), LedgerAdmitResult, AdmissionConflict).pipe(Effect.map((reply) => reply.result)),
4585
+ markReady: (request) => submissionTarget("ledger mark ready", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markReady(request) : foreignLedgerCall("ledger mark ready", target.threadId, LedgerMarkReadyCall.make({ request }), LedgerMarkReadyResult, NoAdditionalPortFailure).pipe(Effect.asVoid))),
4586
+ lookup: (request) => request._tag === "SubmissionLookupById" ? submissionTarget("ledger lookup", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.lookup(request) : foreignLookupById("ledger lookup", target.threadId, request.submissionId))) : request.threadId === options.localThreadId ? local.lookup(request) : foreignLedgerCall("ledger lookup", request.threadId, LedgerLookupCall.make({ request }), LedgerLookupResult, NoAdditionalPortFailure).pipe(Effect.map((result) => result.submission === void 0 ? Option.none() : Option.some(result.submission))),
4587
+ resolveAdmission: (request) => request.threadId === options.localThreadId ? local.resolveAdmission(request) : resolveForeignAdmission(request.threadId, request),
4588
+ requestAbort: (request) => submissionTarget("ledger request abort", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.requestAbort(request) : foreignLedgerCall("ledger request abort", target.threadId, LedgerRequestAbortCall.make({ request }), LedgerRequestAbortResult, AbortPortFailure).pipe(Effect.map((reply) => reply.intent)))),
4589
+ 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.threadId, LedgerRecordChildSettledCall.make({ request }), LedgerRecordChildSettledResult, NoAdditionalPortFailure).pipe(Effect.map((reply) => reply.outcome)))),
4590
+ claim: (request) => request.threadId === options.localThreadId ? local.claim(request) : Effect.fail(crossThreadLedgerError("ledger claim", request.threadId)),
4591
+ claimJoining: (request) => request.threadId === options.localThreadId ? local.claimJoining(request) : Effect.fail(crossThreadLedgerError("ledger claim joining", request.threadId)),
4592
+ renewOwnership: (request) => submissionTarget("ledger renew ownership", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.renewOwnership(request) : Effect.fail(crossThreadLedgerError("ledger renew ownership", target.threadId)))),
4593
+ releaseOwnership: (request) => submissionTarget("ledger release ownership", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.releaseOwnership(request) : Effect.fail(crossThreadLedgerError("ledger release ownership", target.threadId)))),
4594
+ markInputApplied: (request) => submissionTarget("ledger mark input applied", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markInputApplied(request) : Effect.fail(crossThreadLedgerError("ledger mark input applied", target.threadId)))),
4595
+ reserveSettlement: (request) => submissionTarget("ledger reserve settlement", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.reserveSettlement(request) : Effect.fail(crossThreadLedgerError("ledger reserve settlement", target.threadId)))),
4596
+ finalizeSettlement: (request) => submissionTarget("ledger finalize settlement", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.finalizeSettlement(request) : Effect.fail(crossThreadLedgerError("ledger finalize settlement", target.threadId)))),
4597
+ markJoined: (request) => submissionTarget("ledger mark joined", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markJoined(request) : Effect.fail(crossThreadLedgerError("ledger mark joined", target.threadId)))),
4598
+ revertJoining: (request) => submissionTarget("ledger revert joining", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.revertJoining(request) : Effect.fail(crossThreadLedgerError("ledger revert joining", target.threadId)))),
4599
+ suspend: (request) => submissionTarget("ledger suspend", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.suspend(request) : Effect.fail(crossThreadLedgerError("ledger suspend", target.threadId)))),
4600
+ recordApprovalDecision: (command) => submissionTarget("ledger record approval decision", command.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordApprovalDecision(command) : Effect.fail(crossThreadLedgerError("ledger record approval decision", target.threadId)))),
4601
+ markUnknown: (request) => submissionTarget("ledger mark unknown", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markUnknown(request) : Effect.fail(crossThreadLedgerError("ledger mark unknown", target.threadId)))),
4602
+ recordUnknownResolution: (command) => submissionTarget("ledger record unknown resolution", command.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordUnknownResolution(command) : Effect.fail(crossThreadLedgerError("ledger record unknown resolution", target.threadId)))),
4603
+ reserveChildBudget: (request) => submissionTarget("ledger reserve child budget", request.parentSubmissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.reserveChildBudget(request) : Effect.fail(crossThreadLedgerError("ledger reserve child budget", target.threadId)))),
3936
4604
  attachChildToReservation: local.attachChildToReservation,
3937
4605
  beginChildBudgetRelease: local.beginChildBudgetRelease,
3938
4606
  releaseChildBudget: local.releaseChildBudget,
3939
4607
  scanNonterminal: local.scanNonterminal,
3940
- 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))))
4608
+ 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(crossThreadLedgerError("ledger load recovery snapshot", target.threadId))))
3941
4609
  });
3942
4610
  return Context.make(SubmissionLedger, routed);
3943
4611
  });
3944
4612
  const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices")(function* (options) {
3945
- const local = yield* ConversationStore;
3946
- const transport = yield* ConversationPortTransport;
4613
+ const local = yield* ThreadStore;
4614
+ const checkpoints = local.checkpoints;
4615
+ const transport = yield* ThreadPortTransport;
3947
4616
  const transportCall = makeTransportCall(transport);
3948
- const routeFailure = (operation, target) => (error) => ConversationStoreError.make({
4617
+ const routeFailure = (operation, target) => (error) => ThreadStoreError.make({
3949
4618
  operation,
3950
- message: boundPortDiagnostic(`Routed ${operation} to the Conversation Object owning ${target} failed: ${error.message}`),
4619
+ message: boundPortDiagnostic(`Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`),
3951
4620
  cause: error
3952
4621
  });
3953
- /** The store twin of `foreignLedgerCall` with `ConversationStoreError` as the base error. */
4622
+ /** The store twin of `foreignLedgerCall` with `ThreadStoreError` as the base error. */
3954
4623
  const foreignStoreCall = (operation, target, call, resultSchema, failureSchema) => {
3955
4624
  const isExpectedResult = Schema.is(resultSchema);
3956
4625
  const isExpectedFailure = Schema.is(failureSchema);
@@ -3958,17 +4627,17 @@ const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices
3958
4627
  if (response._tag === "PortFailed") {
3959
4628
  const failure = response.failure;
3960
4629
  if (isExpectedFailure(failure)) return Effect.fail(failure);
3961
- if (failure._tag === "ConversationStoreError") return Effect.fail(failure);
3962
- return Effect.fail(ConversationStoreError.make({
4630
+ if (failure._tag === "ThreadStoreError") return Effect.fail(failure);
4631
+ return Effect.fail(ThreadStoreError.make({
3963
4632
  operation,
3964
- message: boundPortDiagnostic(`The Conversation Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
4633
+ message: boundPortDiagnostic(`The Thread Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
3965
4634
  cause: failure
3966
4635
  }));
3967
4636
  }
3968
4637
  const result = response.result;
3969
- if (!isExpectedResult(result)) return Effect.fail(ConversationStoreError.make({
4638
+ if (!isExpectedResult(result)) return Effect.fail(ThreadStoreError.make({
3970
4639
  operation,
3971
- message: `The Conversation Object owning ${target} answered ${operation} with the mismatched result ${result._tag}.`
4640
+ message: `The Thread Object owning ${target} answered ${operation} with the mismatched result ${result._tag}.`
3972
4641
  }));
3973
4642
  return Effect.succeed(result);
3974
4643
  }), Effect.withSpan("DoPortRouting.foreignStoreCall", { attributes: {
@@ -3976,39 +4645,41 @@ const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices
3976
4645
  target
3977
4646
  } }));
3978
4647
  };
3979
- const routed = ConversationStore.of({
3980
- materialize: (request) => request.conversationId === options.localConversationId ? local.materialize(request) : foreignStoreCall("conversation materialize", request.conversationId, StoreMaterializeCall.make({ request }), StoreMaterializeResult, FenceRejected).pipe(Effect.asVoid),
3981
- append: (request) => request.conversationId === options.localConversationId ? local.append(request) : foreignStoreCall("conversation append", request.conversationId, StoreAppendCall.make({ request }), StoreAppendResult, AppendPortFailure).pipe(Effect.map((reply) => reply.result)),
3982
- read: (request) => request.conversationId === options.localConversationId ? local.read(request) : Stream.unwrap(foreignStoreCall("conversation read", request.conversationId, StoreReadPageCall.make({ request }), StoreReadPageResult, ConversationNotMaterialized).pipe(Effect.map((reply) => Stream.fromIterable(reply.records)))),
3983
- inspectTail: (request) => request.conversationId === options.localConversationId ? local.inspectTail(request) : foreignStoreCall("conversation inspect tail", request.conversationId, StoreInspectTailCall.make({ request }), StoreInspectTailResult, ConversationNotMaterialized).pipe(Effect.map((reply) => reply.tail)),
3984
- export: (request) => request.conversationId === options.localConversationId ? local.export(request) : foreignStoreCall("conversation export", request.conversationId, StoreExportCall.make({ request }), StoreExportResult, ConversationNotMaterialized).pipe(Effect.map((reply) => reply.export)),
3985
- observe: (request) => request.conversationId === options.localConversationId ? local.observe(request) : Stream.unwrap(Effect.fail(crossConversationStoreError("conversation observe", request.conversationId))),
3986
- saveCheckpoint: (request) => request.checkpoint.conversationId === options.localConversationId ? local.saveCheckpoint(request) : Effect.fail(crossConversationStoreError("conversation save checkpoint", request.checkpoint.conversationId)),
3987
- loadCheckpoint: (request) => request.conversationId === options.localConversationId ? local.loadCheckpoint(request) : Effect.fail(crossConversationStoreError("conversation load checkpoint", request.conversationId))
3988
- });
3989
- return Context.make(ConversationStore, routed);
4648
+ const routed = ThreadStore.of({
4649
+ materialize: (request) => request.threadId === options.localThreadId ? local.materialize(request) : foreignStoreCall("thread materialize", request.threadId, StoreMaterializeCall.make({ request }), StoreMaterializeResult, FenceRejected).pipe(Effect.asVoid),
4650
+ append: (request) => request.threadId === options.localThreadId ? local.append(request) : foreignStoreCall("thread append", request.threadId, StoreAppendCall.make({ request }), StoreAppendResult, AppendPortFailure).pipe(Effect.map((reply) => reply.result)),
4651
+ read: (request) => request.threadId === options.localThreadId ? local.read(request) : Stream.unwrap(foreignStoreCall("thread read", request.threadId, StoreReadPageCall.make({ request }), StoreReadPageResult, ThreadNotMaterialized).pipe(Effect.map((reply) => Stream.fromIterable(reply.records)))),
4652
+ inspectTail: (request) => request.threadId === options.localThreadId ? local.inspectTail(request) : foreignStoreCall("thread inspect tail", request.threadId, StoreInspectTailCall.make({ request }), StoreInspectTailResult, ThreadNotMaterialized).pipe(Effect.map((reply) => reply.tail)),
4653
+ export: (request) => request.threadId === options.localThreadId ? local.export(request) : foreignStoreCall("thread export", request.threadId, StoreExportCall.make({ request }), StoreExportResult, ThreadNotMaterialized).pipe(Effect.map((reply) => reply.export)),
4654
+ observe: (request) => request.threadId === options.localThreadId ? local.observe(request) : Stream.unwrap(Effect.fail(crossThreadStoreError("thread observe", request.threadId))),
4655
+ ...checkpoints === void 0 ? {} : { checkpoints: {
4656
+ save: (request) => request.checkpoint.threadId === options.localThreadId ? checkpoints.save(request) : Effect.fail(crossThreadStoreError("thread save checkpoint", request.checkpoint.threadId)),
4657
+ load: (request) => request.threadId === options.localThreadId ? checkpoints.load(request) : Effect.fail(crossThreadStoreError("thread load checkpoint", request.threadId))
4658
+ } }
4659
+ });
4660
+ return Context.make(ThreadStore, routed);
3990
4661
  });
3991
4662
  /**
3992
4663
  * Routing decorator over the LOCAL `SubmissionLedger` facet (plan §1.3): a request addressing
3993
- * this Object's Conversation executes locally; a route-capable request addressing another
3994
- * Conversation is Schema-encoded onto the `ConversationPortTransport` and executed by the
4664
+ * this Object's Thread executes locally; a route-capable request addressing another
4665
+ * Thread is Schema-encoded onto the `ThreadPortTransport` and executed by the
3995
4666
  * owning Object's local facet; any other foreign request fails fast typed. Provide the WP1
3996
4667
  * local facet (`submissionLedgerLayer`/`ledgerLayer`) and a transport to close it.
3997
4668
  */
3998
4669
  const routedSubmissionLedgerLayer = (options) => Layer.effectContext(makeRoutedLedgerServices(options));
3999
4670
  /**
4000
- * Routing decorator over the LOCAL `ConversationStore` facet (plan §1.3): this-conversation
4671
+ * Routing decorator over the LOCAL `ThreadStore` facet (plan §1.3): this-thread
4001
4672
  * requests execute locally; foreign materialize/append/read/inspectTail/export travel the
4002
4673
  * transport; foreign observation and checkpoints fail fast typed.
4003
4674
  */
4004
- const routedConversationStoreLayer = (options) => Layer.effectContext(makeRoutedStoreServices(options));
4675
+ const routedThreadStoreLayer = (options) => Layer.effectContext(makeRoutedStoreServices(options));
4005
4676
  /** Fold one port operation's typed failures into the uniform response envelope. */
4006
4677
  const capture = (effect) => effect.pipe(Effect.map((result) => PortSucceeded.make({ result })), Effect.catch((failure) => Effect.succeed(PortFailed.make({ failure }))));
4007
4678
  /**
4008
4679
  * Execute one decoded port request against THIS Object's LOCAL facets — the owner-side half
4009
4680
  * of the routed ports (plan §1.3). Callers must provide the WP1 local facets, never the
4010
4681
  * routed decorators: the routing layer already established that this Object owns the
4011
- * addressed Conversation, and re-routing here could bounce a request between Objects.
4682
+ * addressed Thread, and re-routing here could bounce a request between Objects.
4012
4683
  * Failures never escape — every typed port failure becomes a `PortFailed` envelope that
4013
4684
  * re-decodes on the caller side.
4014
4685
  */
@@ -4039,24 +4710,24 @@ const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(functio
4039
4710
  return yield* capture(ledger.recordChildSettled(request.request).pipe(Effect.map((outcome) => LedgerRecordChildSettledResult.make({ outcome }))));
4040
4711
  }
4041
4712
  case "StoreMaterialize": {
4042
- const store = yield* ConversationStore;
4713
+ const store = yield* ThreadStore;
4043
4714
  return yield* capture(store.materialize(request.request).pipe(Effect.map(() => StoreMaterializeResult.make({}))));
4044
4715
  }
4045
4716
  case "StoreAppend": {
4046
- const store = yield* ConversationStore;
4717
+ const store = yield* ThreadStore;
4047
4718
  return yield* capture(store.append(request.request).pipe(Effect.map((result) => StoreAppendResult.make({ result }))));
4048
4719
  }
4049
4720
  case "StoreReadPage": {
4050
- const store = yield* ConversationStore;
4721
+ const store = yield* ThreadStore;
4051
4722
  return yield* capture(store.read(request.request).pipe(Stream.runCollect, Effect.map((records) => StoreReadPageResult.make({ records: [...records] }))));
4052
4723
  }
4053
4724
  case "StoreInspectTail": {
4054
- const store = yield* ConversationStore;
4725
+ const store = yield* ThreadStore;
4055
4726
  return yield* capture(store.inspectTail(request.request).pipe(Effect.map((tail) => StoreInspectTailResult.make({ tail }))));
4056
4727
  }
4057
4728
  case "StoreExport": {
4058
- const store = yield* ConversationStore;
4059
- return yield* capture(store.export(request.request).pipe(Effect.map((conversationExport) => StoreExportResult.make({ export: conversationExport }))));
4729
+ const store = yield* ThreadStore;
4730
+ return yield* capture(store.export(request.request).pipe(Effect.map((threadExport) => StoreExportResult.make({ export: threadExport }))));
4060
4731
  }
4061
4732
  }
4062
4733
  });
@@ -4080,9 +4751,10 @@ const encodedProtocolFailure = (message) => ({
4080
4751
  * transport never has to interpret exceptions as protocol answers.
4081
4752
  */
4082
4753
  const handleEncodedPortRequest = Effect.fn("DoPortRouting.handleEncodedPortRequest")(function* (encoded) {
4083
- 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}`))));
4754
+ const response = 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}`) }) }))));
4755
+ return yield* encodePortResponse(response).pipe(Effect.catch((error) => Effect.succeed(encodedProtocolFailure(`The port response could not be encoded: ${error.message}`))));
4084
4756
  });
4085
4757
  //#endregion
4086
- export { ConversationPortTransport, CurrentDoStorageVersion, DEFAULT_MAX_STORED_VALUE_BYTES, DoAppendConflict, DoCheckpointConflict, DoFenceRejected, DoLedgerError, DoScheduleAlarmControl, DoScheduleTransaction, 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, scheduleStoreLayer, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer };
4758
+ export { CurrentDoStorageVersion, DEFAULT_MAX_STORED_VALUE_BYTES, DoAppendConflict, DoCheckpointConflict, DoFenceRejected, DoLedgerError, DoScheduleAlarmControl, DoScheduleTransaction, DoStorageCompatibilityError, DoStorageConfig, DoStorageConfigValue, DoStorageCorruptionError, DoStorageError, DoStorageFailpoint, DoStorageFailpointError, DoStorageFailpointLocation, DoSubscriptionAlarmControl, DoSubscriptionTransaction, 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, ThreadPortTransport, boundPortDiagnostic, decodePortRequest, decodePortResponse, doSubscriptionStoreLayer, encodePortRequest, encodePortResponse, executePortRequest, handleEncodedPortRequest, layer, ledgerLayer, observationOffsetAt, portTransportFailure, routedSubmissionLedgerLayer, routedThreadStoreLayer, scheduleStoreLayer, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer, threadStoreLayer };
4087
4759
 
4088
4760
  //# sourceMappingURL=index.mjs.map