@effect-agent/storage-sqlite 0.1.0-beta.37 → 0.1.0-beta.39

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, AdmissionNotAdmitted, AdmissionRequest, AdmissionResult, AppendConflict, AppendResult, ApprovalConflict, ApprovalDecision, ApprovalDecisionCommand, ApprovalDecisionIntent, AttachChildToReservationRequest, BeginChildBudgetReleaseRequest, CanonicalBatch, CanonicalRecord, CanonicalRecordEnvelope, CanonicalSequence, CheckpointRejected, ChildAttachmentSnapshot, ChildBudgetReservationRequest, ChildBudgetReservationSnapshot, ChildReservationConflict, ChildReservationStatus, ChildSettledNotification, Claim, ClaimJoiningRequest, ClaimRequest, ConversationCheckpoint, ConversationExport, ConversationExportRequest, ConversationMaterialization, ConversationNotMaterialized, ConversationObservation, ConversationRead, ConversationStore, ConversationStoreError, ConversationTail, ConversationTailRequest, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, Digest, EMPTY_TAIL_DIGEST, FenceRejected, FencedAppendRequest, InputAppliedMarker, JoinSnapshot, JoinedToHost, JoiningClaim, LedgerCapabilities, LedgerError, LoadCheckpointRequest, MarkInputAppliedRequest, MarkJoinedRequest, MarkReadyRequest, MarkUnknownRequest, ObservationOffset, OwnershipLost, OwnershipRenewal, OwnershipSnapshot, ParentLinkage, PersistedJson, ProducerEpoch, QueueSequence, RecordEnvelope, RecoverySnapshot, RecoverySnapshotRequest, ReleaseChildBudgetRequest, ReleaseOwnershipRequest, RenewOwnershipRequest, ReservedChildBudget, ReservedSettlement, RevertJoiningRequest, SaveCheckpointRequest, ScheduleCapacityError, ScheduleChange, ScheduleConflict, ScheduleDueCursor, ScheduleFailpoint, ScheduleId, ScheduleInstant, ScheduleKey, ScheduleNotFound, ScheduleOwner, SchedulePageRequest, ScheduleRecord, ScheduleStorageError, ScheduleStore, Settlement, SettlementConflict, SettlementFinalization, SettlementOutcome, SettlementReservation, SettlementReservationSnapshot, SubmissionLedger, SubmissionLookup, 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 SqliteStorageFailpoint } from "./sqlite-storage-failpoint-DIwWk5dw.mjs";
2
+ import { AbortCommand, AbortIntent, AcceptedEvent, AdmissionAdmitted, AdmissionConflict, AdmissionNotAdmitted, AdmissionRequest, AdmissionResult, AppendConflict, AppendResult, ApprovalConflict, ApprovalDecision, ApprovalDecisionCommand, ApprovalDecisionIntent, AttachChildToReservationRequest, BeginChildBudgetReleaseRequest, CanonicalBatch, CanonicalRecord, CanonicalRecordEnvelope, CanonicalSequence, CheckpointRejected, ChildAttachmentSnapshot, ChildBudgetReservationRequest, ChildBudgetReservationSnapshot, ChildReservationConflict, ChildReservationStatus, ChildSettledNotification, Claim, ClaimJoiningRequest, ClaimRequest, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, DeliveryChange, Digest, EMPTY_TAIL_DIGEST, FenceRejected, FencedAppendRequest, InputAppliedMarker, JoinSnapshot, JoinedToHost, JoiningClaim, LedgerCapabilities, LedgerError, LoadCheckpointRequest, MarkInputAppliedRequest, MarkJoinedRequest, MarkReadyRequest, MarkUnknownRequest, ObservationOffset, OwnershipLost, OwnershipRenewal, OwnershipSnapshot, ParentLinkage, PersistedJson, ProducerEpoch, QueueSequence, RecordEnvelope, RecoverySnapshot, RecoverySnapshotRequest, ReleaseChildBudgetRequest, ReleaseOwnershipRequest, RenewOwnershipRequest, ReservedChildBudget, ReservedSettlement, RevertJoiningRequest, SaveCheckpointRequest, ScheduleCapacityError, ScheduleChange, ScheduleConflict, ScheduleDueCursor, ScheduleFailpoint, ScheduleId, ScheduleInstant, ScheduleKey, ScheduleNotFound, ScheduleOwner, SchedulePageRequest, ScheduleRecord, ScheduleStorageError, ScheduleStore, Settlement, SettlementConflict, SettlementFinalization, SettlementOutcome, SettlementReservation, SettlementReservationSnapshot, SourcePartition, SubmissionLedger, SubmissionLookup, SubmissionLookupByKey, SubmissionSnapshot, SubmissionState, SubscriptionDelivery, SubscriptionDeliveryKey, SubscriptionError, SubscriptionFailpoint, SubscriptionKey, SubscriptionLimits, SubscriptionName, SubscriptionRecord, SubscriptionScanCursors, SubscriptionStore, SuspendRequest, SuspensionReason, SuspensionSnapshot, ThreadCheckpoint, ThreadExport, ThreadExportRequest, ThreadMaterialization, ThreadNotMaterialized, ThreadObservation, ThreadRead, ThreadStore, ThreadStoreError, ThreadTail, ThreadTailRequest, UnknownResolution, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, applyScheduleChange, applySubscriptionDeliveryChange, defaultSchedulingLimits, digestCanonicalBatch, sameAcceptedEventIdentity, sameSourcePartition, scheduleDeadline, scheduleUsesCapacity, settlementFailureFromRecord, submissionAbortRecordId, subscriptionCanSelect, subscriptionDeliveryCanSelect, subscriptionDeliveryKeyString } from "@effect-agent/thread";
2
3
  import { Clock, Context, Crypto, DateTime, Duration, Effect, Exit, Layer, Option, Ref, Result, Schema, Stream } from "effect";
3
4
  import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node";
4
5
  import * as SqlClient from "effect/unstable/sql/SqlClient";
@@ -47,7 +48,7 @@ var SqliteAppendConflict = class extends Schema.TaggedError()("SqliteAppendConfl
47
48
  actualTailDigest: Schema.optionalKey(Schema.String)
48
49
  }) {};
49
50
  /**
50
- * A producer epoch does not match the Conversation's current writer registration. Appends
51
+ * A producer epoch does not match the Thread's current writer registration. Appends
51
52
  * require the exact registered epoch, so both older and newer unregistered epochs are fenced;
52
53
  * a newer epoch takes over by materializing first.
53
54
  */
@@ -76,7 +77,7 @@ const SqliteStorageFailpointLocation = Schema.Literals([
76
77
  "append:after-record-insert",
77
78
  "append:after-tail-update",
78
79
  "append:after",
79
- "export:after-conversation-read",
80
+ "export:after-thread-read",
80
81
  "save-checkpoint:before",
81
82
  "save-checkpoint:after",
82
83
  "ledger:admit:before",
@@ -130,13 +131,13 @@ var SqliteStorageFailpointError = class extends Schema.TaggedError()("SqliteStor
130
131
  };
131
132
  //#endregion
132
133
  //#region src/migrations.ts
133
- const CurrentSqliteStorageVersion = 5;
134
+ const CurrentSqliteStorageVersion = 7;
134
135
  const sqliteMigrations = SqliteMigrator.fromRecord({
135
- "1_current_persistent_conversation_foundation": Effect.gen(function* () {
136
+ "1_current_persistent_thread_foundation": Effect.gen(function* () {
136
137
  const sql = yield* SqlClient.SqlClient;
137
138
  yield* sql`
138
- CREATE TABLE effect_agent_conversations (
139
- conversation_id TEXT PRIMARY KEY NOT NULL,
139
+ CREATE TABLE effect_agent_threads (
140
+ thread_id TEXT PRIMARY KEY NOT NULL,
140
141
  created_at TEXT NOT NULL,
141
142
  tail_sequence INTEGER NOT NULL,
142
143
  tail_digest TEXT NOT NULL,
@@ -145,46 +146,46 @@ const sqliteMigrations = SqliteMigrator.fromRecord({
145
146
  `.withoutTransform;
146
147
  yield* sql`
147
148
  CREATE TABLE effect_agent_canonical_batches (
148
- conversation_id TEXT NOT NULL,
149
+ thread_id TEXT NOT NULL,
149
150
  batch_id TEXT NOT NULL,
150
151
  first_sequence INTEGER NOT NULL,
151
152
  last_sequence INTEGER NOT NULL,
152
153
  batch_digest TEXT NOT NULL,
153
154
  tail_digest TEXT NOT NULL,
154
155
  batch_json TEXT NOT NULL,
155
- PRIMARY KEY (conversation_id, batch_id),
156
- FOREIGN KEY (conversation_id)
157
- REFERENCES effect_agent_conversations(conversation_id)
156
+ PRIMARY KEY (thread_id, batch_id),
157
+ FOREIGN KEY (thread_id)
158
+ REFERENCES effect_agent_threads(thread_id)
158
159
  ON DELETE RESTRICT
159
160
  )
160
161
  `.withoutTransform;
161
162
  yield* sql`
162
163
  CREATE TABLE effect_agent_canonical_records (
163
- conversation_id TEXT NOT NULL,
164
+ thread_id TEXT NOT NULL,
164
165
  sequence INTEGER NOT NULL,
165
166
  record_id TEXT NOT NULL,
166
167
  batch_id TEXT NOT NULL,
167
168
  record_json TEXT NOT NULL,
168
- PRIMARY KEY (conversation_id, sequence),
169
- UNIQUE (conversation_id, record_id),
170
- FOREIGN KEY (conversation_id, batch_id)
171
- REFERENCES effect_agent_canonical_batches(conversation_id, batch_id)
169
+ PRIMARY KEY (thread_id, sequence),
170
+ UNIQUE (thread_id, record_id),
171
+ FOREIGN KEY (thread_id, batch_id)
172
+ REFERENCES effect_agent_canonical_batches(thread_id, batch_id)
172
173
  ON DELETE RESTRICT
173
174
  )
174
175
  `.withoutTransform;
175
176
  yield* sql`
176
177
  CREATE INDEX effect_agent_canonical_records_batch
177
- ON effect_agent_canonical_records (conversation_id, batch_id, sequence)
178
+ ON effect_agent_canonical_records (thread_id, batch_id, sequence)
178
179
  `.withoutTransform;
179
180
  yield* sql`
180
181
  CREATE TABLE effect_agent_checkpoints (
181
- conversation_id TEXT NOT NULL,
182
+ thread_id TEXT NOT NULL,
182
183
  through_sequence INTEGER NOT NULL,
183
184
  tail_digest TEXT NOT NULL,
184
185
  checkpoint_json TEXT NOT NULL,
185
- PRIMARY KEY (conversation_id, through_sequence),
186
- FOREIGN KEY (conversation_id)
187
- REFERENCES effect_agent_conversations(conversation_id)
186
+ PRIMARY KEY (thread_id, through_sequence),
187
+ FOREIGN KEY (thread_id)
188
+ REFERENCES effect_agent_threads(thread_id)
188
189
  ON DELETE RESTRICT
189
190
  )
190
191
  `.withoutTransform;
@@ -195,7 +196,7 @@ const sqliteMigrations = SqliteMigrator.fromRecord({
195
196
  yield* sql`
196
197
  CREATE TABLE effect_agent_submissions (
197
198
  submission_id TEXT PRIMARY KEY NOT NULL,
198
- conversation_id TEXT NOT NULL,
199
+ thread_id TEXT NOT NULL,
199
200
  queue_sequence INTEGER NOT NULL,
200
201
  principal TEXT NOT NULL,
201
202
  idempotency_key TEXT NOT NULL,
@@ -211,8 +212,8 @@ const sqliteMigrations = SqliteMigrator.fromRecord({
211
212
  ready_at TEXT,
212
213
  input_applied_record_id TEXT,
213
214
  input_applied_sequence INTEGER,
214
- UNIQUE (conversation_id, principal, idempotency_key),
215
- UNIQUE (conversation_id, queue_sequence)
215
+ UNIQUE (thread_id, principal, idempotency_key),
216
+ UNIQUE (thread_id, queue_sequence)
216
217
  )
217
218
  `.withoutTransform;
218
219
  yield* sql`
@@ -232,7 +233,7 @@ const sqliteMigrations = SqliteMigrator.fromRecord({
232
233
  CREATE TABLE effect_agent_attempts (
233
234
  attempt_id TEXT PRIMARY KEY NOT NULL,
234
235
  submission_id TEXT NOT NULL,
235
- conversation_id TEXT NOT NULL,
236
+ thread_id TEXT NOT NULL,
236
237
  owner_producer_id TEXT NOT NULL,
237
238
  producer_epoch INTEGER NOT NULL,
238
239
  claimed_at TEXT NOT NULL,
@@ -384,6 +385,80 @@ const sqliteMigrations = SqliteMigrator.fromRecord({
384
385
  WHERE deadline_at_millis IS NOT NULL
385
386
  `.withoutTransform;
386
387
  yield* sql`PRAGMA user_version = 5`.withoutTransform;
388
+ }),
389
+ "6_durable_subscriptions": Effect.gen(function* () {
390
+ const sql = yield* SqlClient.SqlClient;
391
+ yield* sql`
392
+ CREATE TABLE effect_agent_subscription_sequences (
393
+ tenant_id TEXT NOT NULL,
394
+ source_address TEXT NOT NULL,
395
+ sequence INTEGER NOT NULL,
396
+ event_scan_cursor TEXT NOT NULL,
397
+ delivery_scan_cursor TEXT NOT NULL,
398
+ recovery_scan_cursor INTEGER NOT NULL,
399
+ PRIMARY KEY (tenant_id, source_address)
400
+ )
401
+ `.withoutTransform;
402
+ yield* sql`
403
+ CREATE TABLE effect_agent_subscriptions (
404
+ tenant_id TEXT NOT NULL,
405
+ source_address TEXT NOT NULL,
406
+ owner_id TEXT NOT NULL,
407
+ subscription_id TEXT NOT NULL,
408
+ ordinal INTEGER NOT NULL,
409
+ source_name TEXT NOT NULL,
410
+ source_version TEXT NOT NULL,
411
+ matching_key TEXT NOT NULL,
412
+ state TEXT NOT NULL,
413
+ expires_at_millis INTEGER NOT NULL,
414
+ recovery_at_millis INTEGER,
415
+ record_json TEXT NOT NULL,
416
+ PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id),
417
+ UNIQUE (tenant_id, source_address, ordinal)
418
+ )
419
+ `.withoutTransform;
420
+ yield* sql`CREATE INDEX effect_agent_subscriptions_owner ON effect_agent_subscriptions (tenant_id, source_address, owner_id, ordinal)`.withoutTransform;
421
+ yield* sql`CREATE INDEX effect_agent_subscriptions_candidates ON effect_agent_subscriptions (tenant_id, source_address, source_name, source_version, matching_key, ordinal)`.withoutTransform;
422
+ 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;
423
+ yield* sql`
424
+ CREATE TABLE effect_agent_subscription_events (
425
+ tenant_id TEXT NOT NULL,
426
+ source_address TEXT NOT NULL,
427
+ event_id TEXT NOT NULL,
428
+ source_name TEXT NOT NULL,
429
+ source_version TEXT NOT NULL,
430
+ matching_key TEXT NOT NULL,
431
+ payload_digest TEXT NOT NULL,
432
+ cutoff INTEGER NOT NULL,
433
+ cursor INTEGER NOT NULL,
434
+ routing_complete INTEGER NOT NULL,
435
+ next_attempt_at_millis INTEGER NOT NULL,
436
+ record_json TEXT NOT NULL,
437
+ PRIMARY KEY (tenant_id, source_address, event_id)
438
+ )
439
+ `.withoutTransform;
440
+ 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;
441
+ yield* sql`
442
+ CREATE TABLE effect_agent_subscription_deliveries (
443
+ tenant_id TEXT NOT NULL,
444
+ source_address TEXT NOT NULL,
445
+ owner_id TEXT NOT NULL,
446
+ subscription_id TEXT NOT NULL,
447
+ event_id TEXT NOT NULL,
448
+ delivery_key TEXT NOT NULL,
449
+ state TEXT NOT NULL,
450
+ next_attempt_at_millis INTEGER NOT NULL,
451
+ record_json TEXT NOT NULL,
452
+ PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id, event_id),
453
+ UNIQUE (tenant_id, source_address, delivery_key)
454
+ )
455
+ `.withoutTransform;
456
+ 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;
457
+ 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;
458
+ yield* sql`PRAGMA user_version = 6`.withoutTransform;
459
+ }),
460
+ "7_thread_terminology_cutover": Effect.gen(function* () {
461
+ yield* (yield* SqlClient.SqlClient)`PRAGMA user_version = 7`.withoutTransform;
387
462
  })
388
463
  });
389
464
  //#endregion
@@ -404,7 +479,7 @@ var SqliteStorageConfigValue = class extends Schema.Class("@effect-agent/storage
404
479
  * Submission ownership lease duration in milliseconds (D5). The lease is a liveness hint
405
480
  * that makes an abandoned claim reclaimable; correctness never depends on it because every
406
481
  * canonical append is fenced by producer epoch. Convenience layers default this to
407
- * `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/session`.
482
+ * `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/thread`.
408
483
  */
409
484
  ownershipLeaseDuration: OwnershipLeaseMillis,
410
485
  /**
@@ -417,37 +492,19 @@ var SqliteStorageConfigValue = class extends Schema.Class("@effect-agent/storage
417
492
  /** Explicit SQLite storage configuration authority. */
418
493
  var SqliteStorageConfig = class extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageConfig") {};
419
494
  //#endregion
420
- //#region src/sqlite-storage-failpoint.ts
421
- const noFailpoint = () => Effect.void;
422
- /** Test-only control for replacing the active SQLite failpoint handler. */
423
- var SqliteStorageFailpointTestControl = class extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageFailpointTestControl") {};
424
- /** Explicit fault-injection authority used at SQLite operation boundaries. */
425
- var SqliteStorageFailpoint = class SqliteStorageFailpoint extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageFailpoint") {
426
- /** Production default: no fault injection. */
427
- static layer = Layer.succeed(this)({ hit: noFailpoint });
428
- /** Reusable test Layer with a control service backed by the same handler Ref. */
429
- static layerTest = Layer.effectContext(Effect.gen(function* () {
430
- const handler = yield* Ref.make(noFailpoint);
431
- return Context.make(SqliteStorageFailpoint, SqliteStorageFailpoint.of({ hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))) })).pipe(Context.add(SqliteStorageFailpointTestControl, SqliteStorageFailpointTestControl.of({
432
- clear: Ref.set(handler, noFailpoint),
433
- setHandler: (next) => Ref.set(handler, next)
434
- })));
435
- }));
436
- };
437
- //#endregion
438
495
  //#region src/sqlite-journal.ts
439
496
  const BoundedStoredText$1 = Schema.String.check(Schema.isMaxLength(16 * 1024 * 1024));
440
497
  const BoundedIdentifier$1 = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
441
498
  const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
442
- const MAX_RECORDS_PER_CONVERSATION = 65536;
499
+ const MAX_RECORDS_PER_THREAD = 65536;
443
500
  const MAX_STORED_TEXT_BYTES = 16 * 1024 * 1024;
444
501
  const MAX_IDENTIFIER_LENGTH = 1024;
445
502
  const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
446
503
  var SqliteVersionRow = class extends Schema.Class("SqliteVersionRow")({ user_version: NonNegativeInt }) {};
447
504
  var SqliteJournalModeRow = class extends Schema.Class("SqliteJournalModeRow")({ journal_mode: Schema.NonEmptyString.check(Schema.isMaxLength(32)) }) {};
448
505
  var SqliteNameRow = class extends Schema.Class("SqliteNameRow")({ name: BoundedIdentifier$1 }) {};
449
- var ConversationRow = class extends Schema.Class("ConversationRow")({
450
- conversation_id: BoundedIdentifier$1,
506
+ var ThreadRow = class extends Schema.Class("ThreadRow")({
507
+ thread_id: BoundedIdentifier$1,
451
508
  created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
452
509
  producer_epoch: ProducerEpoch,
453
510
  tail_digest: BoundedStoredText$1,
@@ -457,21 +514,21 @@ var BatchRow = class extends Schema.Class("BatchRow")({
457
514
  batch_digest: BoundedStoredText$1,
458
515
  batch_id: BoundedIdentifier$1,
459
516
  batch_json: BoundedStoredText$1,
460
- conversation_id: BoundedIdentifier$1,
517
+ thread_id: BoundedIdentifier$1,
461
518
  first_sequence: CanonicalSequence,
462
519
  last_sequence: CanonicalSequence,
463
520
  tail_digest: BoundedStoredText$1
464
521
  }) {};
465
522
  var RecordRow = class extends Schema.Class("RecordRow")({
466
523
  batch_id: BoundedIdentifier$1,
467
- conversation_id: BoundedIdentifier$1,
524
+ thread_id: BoundedIdentifier$1,
468
525
  record_id: BoundedIdentifier$1,
469
526
  record_json: BoundedStoredText$1,
470
527
  sequence: CanonicalSequence
471
528
  }) {};
472
529
  var CheckpointRow = class extends Schema.Class("CheckpointRow")({
473
530
  checkpoint_json: BoundedStoredText$1,
474
- conversation_id: BoundedIdentifier$1,
531
+ thread_id: BoundedIdentifier$1,
475
532
  tail_digest: BoundedStoredText$1,
476
533
  through_sequence: CanonicalSequence
477
534
  }) {};
@@ -483,7 +540,7 @@ var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-sqlite/
483
540
  batchDigest: BoundedStoredText$1,
484
541
  batchId: BoundedIdentifier$1,
485
542
  batchJson: BoundedStoredText$1,
486
- conversationId: BoundedIdentifier$1,
543
+ threadId: BoundedIdentifier$1,
487
544
  expectedTailDigest: BoundedStoredText$1,
488
545
  expectedTailSequence: CanonicalSequence,
489
546
  producerEpoch: ProducerEpoch,
@@ -497,20 +554,20 @@ var RawAppendResult = class extends Schema.Class("@effect-agent/storage-sqlite/R
497
554
  tailDigest: BoundedStoredText$1
498
555
  }) {};
499
556
  var RawReadRequest = class extends Schema.Class("@effect-agent/storage-sqlite/RawReadRequest")({
500
- conversationId: BoundedIdentifier$1,
557
+ threadId: BoundedIdentifier$1,
501
558
  fromSequenceExclusive: CanonicalSequence,
502
559
  limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
503
560
  }) {};
504
561
  var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-sqlite/RawCheckpoint")({
505
562
  checkpointJson: BoundedStoredText$1,
506
- conversationId: BoundedIdentifier$1,
563
+ threadId: BoundedIdentifier$1,
507
564
  tailDigest: BoundedStoredText$1,
508
565
  throughSequence: CanonicalSequence
509
566
  }) {};
510
- var RawConversationExport = class extends Schema.Class("@effect-agent/storage-sqlite/RawConversationExport")({
567
+ var RawThreadExport = class extends Schema.Class("@effect-agent/storage-sqlite/RawThreadExport")({
511
568
  batches: Schema.Array(BatchRow),
512
569
  checkpoints: Schema.Array(CheckpointRow),
513
- conversation: ConversationRow,
570
+ thread: ThreadRow,
514
571
  records: Schema.Array(RecordRow)
515
572
  }) {};
516
573
  const storageError = (operation) => (error) => SqliteStorageError.make({
@@ -519,13 +576,13 @@ const storageError = (operation) => (error) => SqliteStorageError.make({
519
576
  message: error.message
520
577
  });
521
578
  /** Decode raw SQLite rows against a Schema, reporting failures as typed corruption. */
522
- const decodeRows$1 = Effect.fn("SqliteJournal.decodeRows")((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
579
+ const decodeRows$2 = Effect.fn("SqliteJournal.decodeRows")((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
523
580
  table,
524
581
  rowKey,
525
582
  message: String(error)
526
583
  }))));
527
584
  /** Decode exactly one raw SQLite row against a Schema, reporting failures as typed corruption. */
528
- const decodeSingleRow = Effect.fn("SqliteJournal.decodeSingleRow")((schema, table, rowKey, rows) => decodeRows$1(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(SqliteStorageCorruptionError.make({
585
+ const decodeSingleRow = Effect.fn("SqliteJournal.decodeSingleRow")((schema, table, rowKey, rows) => decodeRows$2(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(SqliteStorageCorruptionError.make({
529
586
  table,
530
587
  rowKey,
531
588
  message: `Expected exactly one row but found ${decoded.length}.`
@@ -540,15 +597,15 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
540
597
  const journalMode = yield* decodeSingleRow(Schema.Array(SqliteJournalModeRow), "pragma_journal_mode", "singleton", journalModeRows);
541
598
  if (journalMode.journal_mode.toLowerCase() !== "wal") return yield* SqliteStorageCompatibilityError.make({
542
599
  actualVersion: 0,
543
- supportedVersion: 5,
600
+ supportedVersion: 7,
544
601
  message: `SQLite WAL mode is required; the database reported ${journalMode.journal_mode}.`
545
602
  });
546
603
  const versionRows = yield* sql`PRAGMA user_version`.pipe(Effect.mapError(storageError("read storage version")));
547
604
  const version = yield* decodeSingleRow(Schema.Array(SqliteVersionRow), "pragma_user_version", "singleton", versionRows);
548
- if (version.user_version !== 0 && version.user_version !== 5) return yield* SqliteStorageCompatibilityError.make({
605
+ if (version.user_version !== 0 && version.user_version !== 7) return yield* SqliteStorageCompatibilityError.make({
549
606
  actualVersion: version.user_version,
550
- supportedVersion: 5,
551
- message: `The SQLite file uses private-development storage version ${version.user_version}; this build supports exactly version 5. Reset the database file explicitly; automatic stored-data migrations are not provided during private development.`
607
+ supportedVersion: 7,
608
+ message: `The SQLite file uses private-development storage version ${version.user_version}; this build supports exactly version 7. Reset the database file explicitly; automatic stored-data migrations are not provided during private development.`
552
609
  });
553
610
  if (version.user_version === 0) {
554
611
  const existingRows = yield* sql`
@@ -558,9 +615,9 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
558
615
  AND name LIKE 'effect_agent_%'
559
616
  ORDER BY name
560
617
  `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
561
- if ((yield* decodeRows$1(Schema.Array(SqliteNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* SqliteStorageCompatibilityError.make({
618
+ if ((yield* decodeRows$2(Schema.Array(SqliteNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* SqliteStorageCompatibilityError.make({
562
619
  actualVersion: 0,
563
- supportedVersion: 5,
620
+ supportedVersion: 7,
564
621
  message: "The SQLite file contains unversioned Effect Agent tables. Reset it explicitly; refusing to mutate ambiguous stored data."
565
622
  });
566
623
  yield* SqliteMigrator.run({ loader: sqliteMigrations }).pipe(Effect.mapError((error) => SqliteStorageError.make({
@@ -574,7 +631,7 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
574
631
  FROM sqlite_master
575
632
  WHERE type = 'table'
576
633
  AND name IN (
577
- 'effect_agent_conversations',
634
+ 'effect_agent_threads',
578
635
  'effect_agent_canonical_batches',
579
636
  'effect_agent_canonical_records',
580
637
  'effect_agent_checkpoints',
@@ -589,9 +646,9 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
589
646
  )
590
647
  ORDER BY name
591
648
  `.pipe(Effect.mapError(storageError("verify storage tables")));
592
- if ((yield* decodeRows$1(Schema.Array(SqliteNameRow), "sqlite_master", "required_tables", requiredRows)).length !== 12) return yield* SqliteStorageCompatibilityError.make({
593
- actualVersion: 5,
594
- supportedVersion: 5,
649
+ if ((yield* decodeRows$2(Schema.Array(SqliteNameRow), "sqlite_master", "required_tables", requiredRows)).length !== 12) return yield* SqliteStorageCompatibilityError.make({
650
+ actualVersion: 7,
651
+ supportedVersion: 7,
595
652
  message: "The SQLite file claims the current format but is missing required tables. Reset the corrupt private-development data."
596
653
  });
597
654
  const classifyWriteFailure = (operation) => (error) => error.reason._tag === "LockTimeoutError" ? SqliteWriteContention.make({
@@ -641,44 +698,44 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
641
698
  yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], void 0));
642
699
  return yield* exit;
643
700
  })).pipe(Effect.withSpan("SqliteJournal.withReadTransaction", { attributes: { operation } })));
644
- const materialize = Effect.fn("SqliteJournal.materialize")(function* (conversationId, createdAt, emptyTailDigest, producerEpoch) {
645
- if (conversationId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(emptyTailDigest) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
646
- operation: "materialize conversation",
647
- message: "Conversation identity or initial digest exceeds the SQLite storage bounds."
701
+ const materialize = Effect.fn("SqliteJournal.materialize")(function* (threadId, createdAt, emptyTailDigest, producerEpoch) {
702
+ if (threadId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(emptyTailDigest) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
703
+ operation: "materialize thread",
704
+ message: "Thread identity or initial digest exceeds the SQLite storage bounds."
648
705
  });
649
706
  yield* withWriteTransaction("materialize transaction")(Effect.gen(function* () {
650
707
  const existingRows = yield* sql`
651
708
  SELECT
652
- conversation_id,
709
+ thread_id,
653
710
  created_at,
654
711
  tail_sequence,
655
712
  tail_digest,
656
713
  producer_epoch
657
- FROM effect_agent_conversations
658
- WHERE conversation_id = ${conversationId}
659
- `.pipe(Effect.mapError(storageError("read materialized conversation")));
660
- const existing = yield* decodeRows$1(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, existingRows);
714
+ FROM effect_agent_threads
715
+ WHERE thread_id = ${threadId}
716
+ `.pipe(Effect.mapError(storageError("read materialized thread")));
717
+ const existing = yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, existingRows);
661
718
  if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
662
- table: "effect_agent_conversations",
663
- rowKey: conversationId,
664
- message: "A conversation primary key returned more than one row."
719
+ table: "effect_agent_threads",
720
+ rowKey: threadId,
721
+ message: "A thread primary key returned more than one row."
665
722
  });
666
723
  if (existing.length === 0) {
667
724
  yield* sql`
668
- INSERT INTO effect_agent_conversations (
669
- conversation_id,
725
+ INSERT INTO effect_agent_threads (
726
+ thread_id,
670
727
  created_at,
671
728
  tail_sequence,
672
729
  tail_digest,
673
730
  producer_epoch
674
731
  ) VALUES (
675
- ${conversationId},
732
+ ${threadId},
676
733
  ${createdAt},
677
734
  0,
678
735
  ${emptyTailDigest},
679
736
  ${producerEpoch}
680
737
  )
681
- `.pipe(Effect.mapError(storageError("materialize conversation")));
738
+ `.pipe(Effect.mapError(storageError("materialize thread")));
682
739
  return;
683
740
  }
684
741
  if (producerEpoch < existing[0].producer_epoch) return yield* SqliteFenceRejected.make({
@@ -687,27 +744,27 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
687
744
  message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
688
745
  });
689
746
  if (producerEpoch > existing[0].producer_epoch) yield* sql`
690
- UPDATE effect_agent_conversations
747
+ UPDATE effect_agent_threads
691
748
  SET producer_epoch = ${producerEpoch}
692
- WHERE conversation_id = ${conversationId}
749
+ WHERE thread_id = ${threadId}
693
750
  `.pipe(Effect.mapError(storageError("advance materialization epoch")));
694
751
  }));
695
752
  });
696
- const getConversation = Effect.fn("SqliteJournal.getConversation")(function* (conversationId) {
753
+ const getThread = Effect.fn("SqliteJournal.getThread")(function* (threadId) {
697
754
  const rows = yield* sql`
698
755
  SELECT
699
- conversation_id,
756
+ thread_id,
700
757
  created_at,
701
758
  tail_sequence,
702
759
  tail_digest,
703
760
  producer_epoch
704
- FROM effect_agent_conversations
705
- WHERE conversation_id = ${conversationId}
706
- `.pipe(Effect.mapError(storageError("read conversation")));
707
- return yield* decodeRows$1(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, rows);
761
+ FROM effect_agent_threads
762
+ WHERE thread_id = ${threadId}
763
+ `.pipe(Effect.mapError(storageError("read thread")));
764
+ return yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
708
765
  });
709
766
  const append = Effect.fn("SqliteJournal.append")(function* (request) {
710
- if (request.conversationId.length > MAX_IDENTIFIER_LENGTH || request.batchId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(request.batchJson) > MAX_STORED_TEXT_BYTES || storedTextBytes(request.batchDigest) > MAX_STORED_TEXT_BYTES || storedTextBytes(request.tailDigest) > MAX_STORED_TEXT_BYTES || request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(record.recordJson) > MAX_STORED_TEXT_BYTES)) return yield* SqliteStorageError.make({
767
+ if (request.threadId.length > MAX_IDENTIFIER_LENGTH || request.batchId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(request.batchJson) > MAX_STORED_TEXT_BYTES || storedTextBytes(request.batchDigest) > MAX_STORED_TEXT_BYTES || storedTextBytes(request.tailDigest) > MAX_STORED_TEXT_BYTES || request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(record.recordJson) > MAX_STORED_TEXT_BYTES)) return yield* SqliteStorageError.make({
711
768
  operation: "append canonical batch",
712
769
  message: "Canonical identifiers or encoded JSON exceed the SQLite storage bounds."
713
770
  });
@@ -717,25 +774,25 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
717
774
  message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
718
775
  reason: "record-identity"
719
776
  });
720
- const conversationRows = yield* sql`
777
+ const threadRows = yield* sql`
721
778
  SELECT
722
- conversation_id,
779
+ thread_id,
723
780
  created_at,
724
781
  tail_sequence,
725
782
  tail_digest,
726
783
  producer_epoch
727
- FROM effect_agent_conversations
728
- WHERE conversation_id = ${request.conversationId}
784
+ FROM effect_agent_threads
785
+ WHERE thread_id = ${request.threadId}
729
786
  `.pipe(Effect.mapError(storageError("read append tail")));
730
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", request.conversationId, conversationRows);
731
- if (request.producerEpoch !== conversation.producer_epoch) return yield* SqliteFenceRejected.make({
787
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", request.threadId, threadRows);
788
+ if (request.producerEpoch !== thread.producer_epoch) return yield* SqliteFenceRejected.make({
732
789
  producerEpoch: request.producerEpoch,
733
- actualEpoch: conversation.producer_epoch,
734
- message: `Producer epoch ${request.producerEpoch} is not the current epoch ${conversation.producer_epoch}.`
790
+ actualEpoch: thread.producer_epoch,
791
+ message: `Producer epoch ${request.producerEpoch} is not the current epoch ${thread.producer_epoch}.`
735
792
  });
736
793
  const batchRows = yield* sql`
737
794
  SELECT
738
- conversation_id,
795
+ thread_id,
739
796
  batch_id,
740
797
  first_sequence,
741
798
  last_sequence,
@@ -743,13 +800,13 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
743
800
  tail_digest,
744
801
  batch_json
745
802
  FROM effect_agent_canonical_batches
746
- WHERE conversation_id = ${request.conversationId}
803
+ WHERE thread_id = ${request.threadId}
747
804
  AND batch_id = ${request.batchId}
748
805
  `.pipe(Effect.mapError(storageError("read idempotent batch")));
749
- const batches = yield* decodeRows$1(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.conversationId}/${request.batchId}`, batchRows);
806
+ const batches = yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.threadId}/${request.batchId}`, batchRows);
750
807
  if (batches.length > 1) return yield* SqliteStorageCorruptionError.make({
751
808
  table: "effect_agent_canonical_batches",
752
- rowKey: `${request.conversationId}/${request.batchId}`,
809
+ rowKey: `${request.threadId}/${request.batchId}`,
753
810
  message: "A canonical batch primary key returned more than one row."
754
811
  });
755
812
  if (batches.length === 1) {
@@ -765,34 +822,34 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
765
822
  tailDigest: existing.tail_digest
766
823
  });
767
824
  }
768
- if (request.expectedTailSequence !== conversation.tail_sequence || request.expectedTailDigest !== conversation.tail_digest) return yield* SqliteAppendConflict.make({
769
- message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${conversation.tail_sequence}/${conversation.tail_digest}.`,
825
+ if (request.expectedTailSequence !== thread.tail_sequence || request.expectedTailDigest !== thread.tail_digest) return yield* SqliteAppendConflict.make({
826
+ message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${thread.tail_sequence}/${thread.tail_digest}.`,
770
827
  reason: "tail",
771
- actualTailSequence: conversation.tail_sequence,
772
- actualTailDigest: conversation.tail_digest
828
+ actualTailSequence: thread.tail_sequence,
829
+ actualTailDigest: thread.tail_digest
773
830
  });
774
- if (conversation.tail_sequence + request.records.length > MAX_RECORDS_PER_CONVERSATION) return yield* SqliteStorageError.make({
831
+ if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) return yield* SqliteStorageError.make({
775
832
  operation: "append canonical batch",
776
- message: `Conversation record limit ${MAX_RECORDS_PER_CONVERSATION} would be exceeded.`
833
+ message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`
777
834
  });
778
835
  const existingRecordRows = yield* sql`
779
836
  SELECT
780
- conversation_id,
837
+ thread_id,
781
838
  sequence,
782
839
  record_id,
783
840
  batch_id,
784
841
  record_json
785
842
  FROM effect_agent_canonical_records
786
- WHERE conversation_id = ${request.conversationId}
843
+ WHERE thread_id = ${request.threadId}
787
844
  AND record_id IN ${sql.in(recordIds)}
788
845
  ORDER BY sequence
789
846
  `.pipe(Effect.mapError(storageError("check canonical record identities")));
790
- const existingRecords = yield* decodeRows$1(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}/record_ids`, existingRecordRows);
847
+ const existingRecords = yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/record_ids`, existingRecordRows);
791
848
  if (existingRecords.length > 0) return yield* SqliteAppendConflict.make({
792
849
  message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
793
850
  reason: "record-identity"
794
851
  });
795
- const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(conversation.tail_sequence + 1).pipe(Effect.mapError((error) => SqliteStorageError.make({
852
+ const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(thread.tail_sequence + 1).pipe(Effect.mapError((error) => SqliteStorageError.make({
796
853
  cause: error,
797
854
  operation: "append canonical batch",
798
855
  message: error.message
@@ -804,7 +861,7 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
804
861
  })));
805
862
  yield* sql`
806
863
  INSERT INTO effect_agent_canonical_batches (
807
- conversation_id,
864
+ thread_id,
808
865
  batch_id,
809
866
  first_sequence,
810
867
  last_sequence,
@@ -812,7 +869,7 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
812
869
  tail_digest,
813
870
  batch_json
814
871
  ) VALUES (
815
- ${request.conversationId},
872
+ ${request.threadId},
816
873
  ${request.batchId},
817
874
  ${firstSequence},
818
875
  ${lastSequence},
@@ -825,13 +882,13 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
825
882
  yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
826
883
  yield* sql`
827
884
  INSERT INTO effect_agent_canonical_records (
828
- conversation_id,
885
+ thread_id,
829
886
  sequence,
830
887
  record_id,
831
888
  batch_id,
832
889
  record_json
833
890
  ) VALUES (
834
- ${request.conversationId},
891
+ ${request.threadId},
835
892
  ${firstSequence + index},
836
893
  ${record.recordId},
837
894
  ${request.batchId},
@@ -841,13 +898,13 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
841
898
  yield* failpoint("append:after-record-insert");
842
899
  }), { discard: true });
843
900
  yield* sql`
844
- UPDATE effect_agent_conversations
901
+ UPDATE effect_agent_threads
845
902
  SET
846
903
  tail_sequence = ${lastSequence},
847
904
  tail_digest = ${request.tailDigest},
848
905
  producer_epoch = ${request.producerEpoch}
849
- WHERE conversation_id = ${request.conversationId}
850
- `.pipe(Effect.mapError(storageError("advance conversation tail")));
906
+ WHERE thread_id = ${request.threadId}
907
+ `.pipe(Effect.mapError(storageError("advance thread tail")));
851
908
  yield* failpoint("append:after-tail-update");
852
909
  return RawAppendResult.make({
853
910
  firstSequence,
@@ -860,36 +917,36 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
860
917
  const read = Effect.fn("SqliteJournal.read")(function* (request) {
861
918
  const rows = yield* sql`
862
919
  SELECT
863
- conversation_id,
920
+ thread_id,
864
921
  sequence,
865
922
  record_id,
866
923
  batch_id,
867
924
  record_json
868
925
  FROM effect_agent_canonical_records
869
- WHERE conversation_id = ${request.conversationId}
926
+ WHERE thread_id = ${request.threadId}
870
927
  AND sequence > ${request.fromSequenceExclusive}
871
928
  ORDER BY sequence
872
929
  LIMIT ${request.limit}
873
930
  `.pipe(Effect.mapError(storageError("read canonical records")));
874
- return yield* decodeRows$1(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}>${request.fromSequenceExclusive}`, rows);
931
+ return yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}>${request.fromSequenceExclusive}`, rows);
875
932
  });
876
- const exportConversation = Effect.fn("SqliteJournal.exportConversation")(function* (conversationId) {
933
+ const exportThread = Effect.fn("SqliteJournal.exportThread")(function* (threadId) {
877
934
  return yield* withReadTransaction("export transaction")(Effect.gen(function* () {
878
- const conversationRows = yield* sql`
935
+ const threadRows = yield* sql`
879
936
  SELECT
880
- conversation_id,
937
+ thread_id,
881
938
  created_at,
882
939
  tail_sequence,
883
940
  tail_digest,
884
941
  producer_epoch
885
- FROM effect_agent_conversations
886
- WHERE conversation_id = ${conversationId}
887
- `.pipe(Effect.mapError(storageError("export conversation")));
888
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, conversationRows);
889
- yield* failpoint("export:after-conversation-read");
942
+ FROM effect_agent_threads
943
+ WHERE thread_id = ${threadId}
944
+ `.pipe(Effect.mapError(storageError("export thread")));
945
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", threadId, threadRows);
946
+ yield* failpoint("export:after-thread-read");
890
947
  const batchRows = yield* sql`
891
948
  SELECT
892
- conversation_id,
949
+ thread_id,
893
950
  batch_id,
894
951
  first_sequence,
895
952
  last_sequence,
@@ -897,70 +954,70 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
897
954
  tail_digest,
898
955
  batch_json
899
956
  FROM effect_agent_canonical_batches
900
- WHERE conversation_id = ${conversationId}
957
+ WHERE thread_id = ${threadId}
901
958
  ORDER BY first_sequence
902
959
  `.pipe(Effect.mapError(storageError("export canonical batches")));
903
960
  const recordRows = yield* sql`
904
961
  SELECT
905
- conversation_id,
962
+ thread_id,
906
963
  sequence,
907
964
  record_id,
908
965
  batch_id,
909
966
  record_json
910
967
  FROM effect_agent_canonical_records
911
- WHERE conversation_id = ${conversationId}
968
+ WHERE thread_id = ${threadId}
912
969
  ORDER BY sequence
913
970
  `.pipe(Effect.mapError(storageError("export canonical records")));
914
971
  const checkpointRows = yield* sql`
915
972
  SELECT
916
- conversation_id,
973
+ thread_id,
917
974
  through_sequence,
918
975
  tail_digest,
919
976
  checkpoint_json
920
977
  FROM effect_agent_checkpoints
921
- WHERE conversation_id = ${conversationId}
978
+ WHERE thread_id = ${threadId}
922
979
  ORDER BY through_sequence
923
980
  `.pipe(Effect.mapError(storageError("export checkpoints")));
924
- return RawConversationExport.make({
925
- conversation,
926
- batches: yield* decodeRows$1(Schema.Array(BatchRow), "effect_agent_canonical_batches", conversationId, batchRows),
927
- records: yield* decodeRows$1(Schema.Array(RecordRow), "effect_agent_canonical_records", conversationId, recordRows),
928
- checkpoints: yield* decodeRows$1(Schema.Array(CheckpointRow), "effect_agent_checkpoints", conversationId, checkpointRows)
981
+ return RawThreadExport.make({
982
+ thread,
983
+ batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", threadId, batchRows),
984
+ records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", threadId, recordRows),
985
+ checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", threadId, checkpointRows)
929
986
  });
930
987
  }));
931
988
  });
932
989
  const saveCheckpoint = Effect.fn("SqliteJournal.saveCheckpoint")(function* (checkpoint) {
933
- if (checkpoint.conversationId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(checkpoint.checkpointJson) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
990
+ if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(checkpoint.checkpointJson) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
934
991
  operation: "save checkpoint",
935
992
  message: "Checkpoint identity or encoded JSON exceeds the SQLite storage bounds."
936
993
  });
937
994
  yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
938
- const conversationRows = yield* sql`
995
+ const threadRows = yield* sql`
939
996
  SELECT
940
- conversation_id,
997
+ thread_id,
941
998
  created_at,
942
999
  tail_sequence,
943
1000
  tail_digest,
944
1001
  producer_epoch
945
- FROM effect_agent_conversations
946
- WHERE conversation_id = ${checkpoint.conversationId}
1002
+ FROM effect_agent_threads
1003
+ WHERE thread_id = ${checkpoint.threadId}
947
1004
  `.pipe(Effect.mapError(storageError("read checkpoint tail")));
948
- const conversation = yield* decodeSingleRow(Schema.Array(ConversationRow), "effect_agent_conversations", checkpoint.conversationId, conversationRows);
949
- if (checkpoint.throughSequence > conversation.tail_sequence) return yield* SqliteCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${conversation.tail_sequence}.` });
1005
+ const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", checkpoint.threadId, threadRows);
1006
+ if (checkpoint.throughSequence > thread.tail_sequence) return yield* SqliteCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${thread.tail_sequence}.` });
950
1007
  const checkpointRows = yield* sql`
951
1008
  SELECT
952
- conversation_id,
1009
+ thread_id,
953
1010
  through_sequence,
954
1011
  tail_digest,
955
1012
  checkpoint_json
956
1013
  FROM effect_agent_checkpoints
957
- WHERE conversation_id = ${checkpoint.conversationId}
1014
+ WHERE thread_id = ${checkpoint.threadId}
958
1015
  AND through_sequence = ${checkpoint.throughSequence}
959
1016
  `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
960
- const existing = yield* decodeRows$1(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.conversationId}/${checkpoint.throughSequence}`, checkpointRows);
1017
+ const existing = yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.threadId}/${checkpoint.throughSequence}`, checkpointRows);
961
1018
  if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
962
1019
  table: "effect_agent_checkpoints",
963
- rowKey: `${checkpoint.conversationId}/${checkpoint.throughSequence}`,
1020
+ rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
964
1021
  message: "A checkpoint primary key returned more than one row."
965
1022
  });
966
1023
  if (existing.length === 1) {
@@ -969,12 +1026,12 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
969
1026
  }
970
1027
  yield* sql`
971
1028
  INSERT INTO effect_agent_checkpoints (
972
- conversation_id,
1029
+ thread_id,
973
1030
  through_sequence,
974
1031
  tail_digest,
975
1032
  checkpoint_json
976
1033
  ) VALUES (
977
- ${checkpoint.conversationId},
1034
+ ${checkpoint.threadId},
978
1035
  ${checkpoint.throughSequence},
979
1036
  ${checkpoint.tailDigest},
980
1037
  ${checkpoint.checkpointJson}
@@ -982,33 +1039,33 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
982
1039
  `.pipe(Effect.mapError(storageError("insert checkpoint")));
983
1040
  }));
984
1041
  });
985
- const loadCheckpoint = Effect.fn("SqliteJournal.loadCheckpoint")(function* (conversationId, atOrBeforeSequence) {
1042
+ const loadCheckpoint = Effect.fn("SqliteJournal.loadCheckpoint")(function* (threadId, atOrBeforeSequence) {
986
1043
  const rows = yield* sql`
987
1044
  SELECT
988
- conversation_id,
1045
+ thread_id,
989
1046
  through_sequence,
990
1047
  tail_digest,
991
1048
  checkpoint_json
992
1049
  FROM effect_agent_checkpoints
993
- WHERE conversation_id = ${conversationId}
1050
+ WHERE thread_id = ${threadId}
994
1051
  AND through_sequence <= ${atOrBeforeSequence}
995
1052
  ORDER BY through_sequence DESC
996
1053
  LIMIT 1
997
1054
  `.pipe(Effect.mapError(storageError("load checkpoint")));
998
- return yield* decodeRows$1(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${conversationId}<=${atOrBeforeSequence}`, rows);
1055
+ return yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${threadId}<=${atOrBeforeSequence}`, rows);
999
1056
  });
1000
1057
  return {
1001
1058
  append,
1002
- exportConversation,
1003
- getConversation,
1004
- getTailDigestAt: Effect.fn("SqliteJournal.getTailDigestAt")(function* (conversationId, sequence) {
1059
+ exportThread,
1060
+ getThread,
1061
+ getTailDigestAt: Effect.fn("SqliteJournal.getTailDigestAt")(function* (threadId, sequence) {
1005
1062
  if (sequence === 0) {
1006
- const conversations = yield* getConversation(conversationId);
1007
- return conversations.length === 0 ? [] : [conversations[0].tail_sequence === 0 ? conversations[0].tail_digest : void 0].filter((value) => value !== void 0);
1063
+ const threads = yield* getThread(threadId);
1064
+ return threads.length === 0 ? [] : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : void 0].filter((value) => value !== void 0);
1008
1065
  }
1009
1066
  const rows = yield* sql`
1010
1067
  SELECT
1011
- conversation_id,
1068
+ thread_id,
1012
1069
  batch_id,
1013
1070
  first_sequence,
1014
1071
  last_sequence,
@@ -1016,10 +1073,10 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
1016
1073
  tail_digest,
1017
1074
  batch_json
1018
1075
  FROM effect_agent_canonical_batches
1019
- WHERE conversation_id = ${conversationId}
1076
+ WHERE thread_id = ${threadId}
1020
1077
  AND last_sequence = ${sequence}
1021
1078
  `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
1022
- return (yield* decodeRows$1(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${conversationId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
1079
+ return (yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${threadId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
1023
1080
  }),
1024
1081
  loadCheckpoint,
1025
1082
  materialize,
@@ -1027,19 +1084,19 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
1027
1084
  saveCheckpoint,
1028
1085
  scanStoredPayloads: Effect.fn("SqliteJournal.scanStoredPayloads")(function* () {
1029
1086
  return yield* withReadTransaction("startup scan transaction")(Effect.gen(function* () {
1030
- const conversations = yield* sql`
1087
+ const threads = yield* sql`
1031
1088
  SELECT
1032
- conversation_id,
1089
+ thread_id,
1033
1090
  created_at,
1034
1091
  tail_sequence,
1035
1092
  tail_digest,
1036
1093
  producer_epoch
1037
- FROM effect_agent_conversations
1038
- ORDER BY conversation_id
1039
- `.pipe(Effect.mapError(storageError("scan conversations")));
1094
+ FROM effect_agent_threads
1095
+ ORDER BY thread_id
1096
+ `.pipe(Effect.mapError(storageError("scan threads")));
1040
1097
  const batches = yield* sql`
1041
1098
  SELECT
1042
- conversation_id,
1099
+ thread_id,
1043
1100
  batch_id,
1044
1101
  first_sequence,
1045
1102
  last_sequence,
@@ -1047,32 +1104,32 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
1047
1104
  tail_digest,
1048
1105
  batch_json
1049
1106
  FROM effect_agent_canonical_batches
1050
- ORDER BY conversation_id, first_sequence
1107
+ ORDER BY thread_id, first_sequence
1051
1108
  `.pipe(Effect.mapError(storageError("scan canonical batches")));
1052
1109
  const records = yield* sql`
1053
1110
  SELECT
1054
- conversation_id,
1111
+ thread_id,
1055
1112
  sequence,
1056
1113
  record_id,
1057
1114
  batch_id,
1058
1115
  record_json
1059
1116
  FROM effect_agent_canonical_records
1060
- ORDER BY conversation_id, sequence
1117
+ ORDER BY thread_id, sequence
1061
1118
  `.pipe(Effect.mapError(storageError("scan canonical records")));
1062
1119
  const checkpoints = yield* sql`
1063
1120
  SELECT
1064
- conversation_id,
1121
+ thread_id,
1065
1122
  through_sequence,
1066
1123
  tail_digest,
1067
1124
  checkpoint_json
1068
1125
  FROM effect_agent_checkpoints
1069
- ORDER BY conversation_id, through_sequence
1126
+ ORDER BY thread_id, through_sequence
1070
1127
  `.pipe(Effect.mapError(storageError("scan checkpoints")));
1071
1128
  return {
1072
- conversations: yield* decodeRows$1(Schema.Array(ConversationRow), "effect_agent_conversations", "startup_scan", conversations),
1073
- batches: yield* decodeRows$1(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
1074
- records: yield* decodeRows$1(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
1075
- checkpoints: yield* decodeRows$1(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
1129
+ threads: yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", "startup_scan", threads),
1130
+ batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
1131
+ records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
1132
+ checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
1076
1133
  };
1077
1134
  }));
1078
1135
  }),
@@ -1080,7 +1137,7 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
1080
1137
  };
1081
1138
  });
1082
1139
  //#endregion
1083
- //#region src/sqlite-conversation-store.ts
1140
+ //#region src/sqlite-thread-store.ts
1084
1141
  const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
1085
1142
  const SQLITE_OFFSET_PREFIX = "effect-agent-sqlite@1:";
1086
1143
  const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
@@ -1088,77 +1145,77 @@ const isDigest = Schema.is(Digest);
1088
1145
  const isSqliteFenceRejected = Schema.is(SqliteFenceRejected);
1089
1146
  const isSqliteAppendConflict = Schema.is(SqliteAppendConflict);
1090
1147
  const isSqliteCheckpointConflict = Schema.is(SqliteCheckpointConflict);
1091
- const storeError = (operation, error) => ConversationStoreError.make({
1148
+ const storeError = (operation, error) => ThreadStoreError.make({
1092
1149
  cause: error,
1093
1150
  operation,
1094
1151
  message: error.message
1095
1152
  });
1096
- const schemaStoreError = (operation, error) => ConversationStoreError.make({
1153
+ const schemaStoreError = (operation, error) => ThreadStoreError.make({
1097
1154
  cause: error,
1098
1155
  operation,
1099
1156
  message: error.message
1100
1157
  });
1101
- const makeOffset = Effect.fn("SqliteConversationStore.makeOffset")(function* (conversationId, sequence) {
1102
- return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.flatMap((validatedSequence) => Schema.decodeUnknownEffect(ObservationOffset)(`${SQLITE_OFFSET_PREFIX}${encodeURIComponent(conversationId)}:${validatedSequence}`)), Effect.mapError((error) => schemaStoreError("encode observation offset", error)));
1158
+ const makeOffset = Effect.fn("SqliteThreadStore.makeOffset")(function* (threadId, sequence) {
1159
+ return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.flatMap((validatedSequence) => Schema.decodeUnknownEffect(ObservationOffset)(`${SQLITE_OFFSET_PREFIX}${encodeURIComponent(threadId)}:${validatedSequence}`)), Effect.mapError((error) => schemaStoreError("encode observation offset", error)));
1103
1160
  });
1104
- const parseOffset = Effect.fn("SqliteConversationStore.parseOffset")(function* (conversationId, offset) {
1161
+ const parseOffset = Effect.fn("SqliteThreadStore.parseOffset")(function* (threadId, offset) {
1105
1162
  if (offset === void 0) return ZERO_CANONICAL_SEQUENCE;
1106
1163
  const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
1107
- const conversationPrefix = `${SQLITE_OFFSET_PREFIX}${encodeURIComponent(conversationId)}:`;
1108
- if (!text.startsWith(conversationPrefix)) return yield* ConversationStoreError.make({
1164
+ const threadPrefix = `${SQLITE_OFFSET_PREFIX}${encodeURIComponent(threadId)}:`;
1165
+ if (!text.startsWith(threadPrefix)) return yield* ThreadStoreError.make({
1109
1166
  operation: "decode observation offset",
1110
- message: "The observation offset belongs to a different adapter, storage version, or Conversation."
1167
+ message: "The observation offset belongs to a different adapter, storage version, or Thread."
1111
1168
  });
1112
- const sequenceText = text.slice(conversationPrefix.length);
1113
- if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield* ConversationStoreError.make({
1169
+ const sequenceText = text.slice(threadPrefix.length);
1170
+ if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield* ThreadStoreError.make({
1114
1171
  operation: "decode observation offset",
1115
1172
  message: "The observation offset is malformed."
1116
1173
  });
1117
1174
  return yield* Schema.decodeUnknownEffect(CanonicalSequence)(Number(sequenceText)).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
1118
1175
  });
1119
- const mapFence = (conversationId, error) => FenceRejected.make({
1120
- conversationId,
1176
+ const mapFence = (threadId, error) => FenceRejected.make({
1177
+ threadId,
1121
1178
  actualEpoch: error.actualEpoch,
1122
1179
  attemptedEpoch: error.producerEpoch
1123
1180
  });
1124
- const encodeCanonicalRecord = Effect.fn("SqliteConversationStore.encodeCanonicalRecord")(function* (record) {
1181
+ const encodeCanonicalRecord = Effect.fn("SqliteThreadStore.encodeCanonicalRecord")(function* (record) {
1125
1182
  return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(record).pipe(Effect.mapError((error) => schemaStoreError("encode canonical record", error)));
1126
1183
  });
1127
- const encodeCanonicalBatch = Effect.fn("SqliteConversationStore.encodeCanonicalBatch")(function* (batch) {
1184
+ const encodeCanonicalBatch = Effect.fn("SqliteThreadStore.encodeCanonicalBatch")(function* (batch) {
1128
1185
  return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalBatch))(batch).pipe(Effect.mapError((error) => schemaStoreError("encode canonical batch", error)));
1129
1186
  });
1130
- const encodeCheckpoint = Effect.fn("SqliteConversationStore.encodeCheckpoint")(function* (checkpoint) {
1131
- return yield* Schema.encodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpoint).pipe(Effect.mapError((error) => schemaStoreError("encode checkpoint", error)));
1187
+ const encodeCheckpoint = Effect.fn("SqliteThreadStore.encodeCheckpoint")(function* (checkpoint) {
1188
+ return yield* Schema.encodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint).pipe(Effect.mapError((error) => schemaStoreError("encode checkpoint", error)));
1132
1189
  });
1133
- const decodeEnvelope = Effect.fn("SqliteConversationStore.decodeEnvelope")(function* (row) {
1134
- const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) => ConversationStoreError.make({
1190
+ const decodeEnvelope = Effect.fn("SqliteThreadStore.decodeEnvelope")(function* (row) {
1191
+ const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) => ThreadStoreError.make({
1135
1192
  operation: "decode canonical record",
1136
1193
  message: error.message
1137
1194
  })));
1138
- const conversationId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.conversationId)(row.conversation_id).pipe(Effect.mapError((error) => schemaStoreError("decode conversation identity", error)));
1139
- const offset = yield* makeOffset(conversationId, row.sequence);
1195
+ const threadId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.threadId)(row.thread_id).pipe(Effect.mapError((error) => schemaStoreError("decode thread identity", error)));
1196
+ const offset = yield* makeOffset(threadId, row.sequence);
1140
1197
  const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(row.batch_id).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
1141
1198
  return CanonicalRecordEnvelope.make({
1142
- conversationId,
1199
+ threadId,
1143
1200
  batchId,
1144
1201
  sequence: row.sequence,
1145
1202
  offset,
1146
1203
  record
1147
1204
  });
1148
1205
  });
1149
- const decodeCheckpoint = Effect.fn("SqliteConversationStore.decodeCheckpoint")(function* (checkpointJson) {
1150
- return yield* Schema.decodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpointJson).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint", error)));
1206
+ const decodeCheckpoint = Effect.fn("SqliteThreadStore.decodeCheckpoint")(function* (checkpointJson) {
1207
+ return yield* Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpointJson).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint", error)));
1151
1208
  });
1152
- const requireConversation = Effect.fn("SqliteConversationStore.requireConversation")(function* (journal, conversationId) {
1153
- const rows = yield* journal.getConversation(conversationId).pipe(Effect.mapError((error) => storeError("read conversation", error)));
1154
- if (rows.length === 0) return yield* ConversationNotMaterialized.make({ conversationId });
1209
+ const requireThread = Effect.fn("SqliteThreadStore.requireThread")(function* (journal, threadId) {
1210
+ const rows = yield* journal.getThread(threadId).pipe(Effect.mapError((error) => storeError("read thread", error)));
1211
+ if (rows.length === 0) return yield* ThreadNotMaterialized.make({ threadId });
1155
1212
  return rows[0];
1156
1213
  });
1157
- const tailDigestAt = Effect.fn("SqliteConversationStore.tailDigestAt")(function* (journal, conversationId, sequence) {
1214
+ const tailDigestAt = Effect.fn("SqliteThreadStore.tailDigestAt")(function* (journal, threadId, sequence) {
1158
1215
  if (sequence === 0) return EMPTY_TAIL_DIGEST;
1159
- const digests = yield* journal.getTailDigestAt(conversationId, sequence).pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
1216
+ const digests = yield* journal.getTailDigestAt(threadId, sequence).pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
1160
1217
  if (digests.length !== 1) return yield* CheckpointRejected.make({
1161
- conversationId,
1218
+ threadId,
1162
1219
  reason: "digest-mismatch"
1163
1220
  });
1164
1221
  return yield* Schema.decodeUnknownEffect(Digest)(digests[0]).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)));
@@ -1177,14 +1234,14 @@ const groupByKey = (rows, key) => {
1177
1234
  * and re-digested against the canonical chain. Routine opens skip this scan: per-operation
1178
1235
  * Schema decoding plus the digest chain already fail clearly on corrupt rows.
1179
1236
  */
1180
- const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPayloads")(function* (journal, crypto) {
1237
+ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads")(function* (journal, crypto) {
1181
1238
  const stored = yield* journal.scanStoredPayloads();
1182
1239
  const batches = yield* Effect.forEach(stored.batches, (batch) => Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(Effect.map((decoded) => ({
1183
1240
  decoded,
1184
1241
  row: batch
1185
1242
  })), Effect.mapError((error) => SqliteStorageCorruptionError.make({
1186
1243
  table: "effect_agent_canonical_batches",
1187
- rowKey: `${batch.conversation_id}/${batch.batch_id}`,
1244
+ rowKey: `${batch.thread_id}/${batch.batch_id}`,
1188
1245
  message: error.message
1189
1246
  }))));
1190
1247
  const records = yield* Effect.forEach(stored.records, (record) => Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(Effect.map((decoded) => ({
@@ -1192,31 +1249,31 @@ const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPa
1192
1249
  row: record
1193
1250
  })), Effect.mapError((error) => SqliteStorageCorruptionError.make({
1194
1251
  table: "effect_agent_canonical_records",
1195
- rowKey: `${record.conversation_id}/${record.sequence}`,
1252
+ rowKey: `${record.thread_id}/${record.sequence}`,
1196
1253
  message: error.message
1197
1254
  }))));
1198
- const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpoint.checkpoint_json).pipe(Effect.map((decoded) => ({
1255
+ const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(Effect.map((decoded) => ({
1199
1256
  decoded,
1200
1257
  row: checkpoint
1201
1258
  })), Effect.mapError((error) => SqliteStorageCorruptionError.make({
1202
1259
  table: "effect_agent_checkpoints",
1203
- rowKey: `${checkpoint.conversation_id}/${checkpoint.through_sequence}`,
1260
+ rowKey: `${checkpoint.thread_id}/${checkpoint.through_sequence}`,
1204
1261
  message: error.message
1205
1262
  }))));
1206
- const batchesByConversation = groupByKey(batches, ({ row }) => row.conversation_id);
1207
- const recordsByConversation = groupByKey(records, ({ row }) => row.conversation_id);
1208
- const checkpointsByConversation = groupByKey(checkpoints, ({ row }) => row.conversation_id);
1209
- const materializedIds = new Set(stored.conversations.map((conversation) => conversation.conversation_id));
1210
- for (const conversation of stored.conversations) {
1211
- const conversationBatches = batchesByConversation.get(conversation.conversation_id) ?? [];
1212
- const conversationRecords = recordsByConversation.get(conversation.conversation_id) ?? [];
1213
- const conversationCheckpoints = checkpointsByConversation.get(conversation.conversation_id) ?? [];
1214
- const recordsByBatch = groupByKey(conversationRecords, ({ row }) => row.batch_id);
1263
+ const batchesByThread = groupByKey(batches, ({ row }) => row.thread_id);
1264
+ const recordsByThread = groupByKey(records, ({ row }) => row.thread_id);
1265
+ const checkpointsByThread = groupByKey(checkpoints, ({ row }) => row.thread_id);
1266
+ const materializedIds = new Set(stored.threads.map((thread) => thread.thread_id));
1267
+ for (const thread of stored.threads) {
1268
+ const threadBatches = batchesByThread.get(thread.thread_id) ?? [];
1269
+ const threadRecords = recordsByThread.get(thread.thread_id) ?? [];
1270
+ const threadCheckpoints = checkpointsByThread.get(thread.thread_id) ?? [];
1271
+ const recordsByBatch = groupByKey(threadRecords, ({ row }) => row.batch_id);
1215
1272
  let previousDigest = EMPTY_TAIL_DIGEST;
1216
1273
  let expectedSequence = 1;
1217
1274
  const tailDigests = /* @__PURE__ */ new Map([[0, EMPTY_TAIL_DIGEST]]);
1218
- for (const { decoded: canonicalBatch, row: batchRow } of conversationBatches) {
1219
- const key = `${batchRow.conversation_id}/${batchRow.batch_id}`;
1275
+ for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
1276
+ const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
1220
1277
  if (canonicalBatch.batchId !== batchRow.batch_id || batchRow.first_sequence !== expectedSequence || batchRow.last_sequence !== batchRow.first_sequence + canonicalBatch.records.length - 1) return yield* SqliteStorageCorruptionError.make({
1221
1278
  table: "effect_agent_canonical_batches",
1222
1279
  rowKey: key,
@@ -1261,41 +1318,41 @@ const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPa
1261
1318
  expectedSequence = batchRow.last_sequence + 1;
1262
1319
  tailDigests.set(batchRow.last_sequence, digest);
1263
1320
  }
1264
- if (conversationRecords.length !== conversation.tail_sequence || conversation.tail_sequence !== expectedSequence - 1 || conversation.tail_digest !== previousDigest) return yield* SqliteStorageCorruptionError.make({
1265
- table: "effect_agent_conversations",
1266
- rowKey: conversation.conversation_id,
1267
- message: "Conversation tail does not match its canonical batch chain."
1321
+ if (threadRecords.length !== thread.tail_sequence || thread.tail_sequence !== expectedSequence - 1 || thread.tail_digest !== previousDigest) return yield* SqliteStorageCorruptionError.make({
1322
+ table: "effect_agent_threads",
1323
+ rowKey: thread.thread_id,
1324
+ message: "Thread tail does not match its canonical batch chain."
1268
1325
  });
1269
- for (const checkpoint of conversationCheckpoints) if (checkpoint.decoded.conversationId !== conversation.conversation_id || checkpoint.decoded.throughSequence !== checkpoint.row.through_sequence || checkpoint.decoded.tailDigest !== checkpoint.row.tail_digest || tailDigests.get(checkpoint.row.through_sequence) !== checkpoint.row.tail_digest) return yield* SqliteStorageCorruptionError.make({
1326
+ for (const checkpoint of threadCheckpoints) if (checkpoint.decoded.threadId !== thread.thread_id || checkpoint.decoded.throughSequence !== checkpoint.row.through_sequence || checkpoint.decoded.tailDigest !== checkpoint.row.tail_digest || tailDigests.get(checkpoint.row.through_sequence) !== checkpoint.row.tail_digest) return yield* SqliteStorageCorruptionError.make({
1270
1327
  table: "effect_agent_checkpoints",
1271
- rowKey: `${conversation.conversation_id}/${checkpoint.row.through_sequence}`,
1328
+ rowKey: `${thread.thread_id}/${checkpoint.row.through_sequence}`,
1272
1329
  message: "Checkpoint identity or digest is not bound to a canonical batch tail."
1273
1330
  });
1274
1331
  }
1275
- if (batches.some(({ row }) => !materializedIds.has(row.conversation_id)) || records.some(({ row }) => !materializedIds.has(row.conversation_id)) || checkpoints.some(({ row }) => !materializedIds.has(row.conversation_id))) return yield* SqliteStorageCorruptionError.make({
1276
- table: "effect_agent_conversations",
1332
+ if (batches.some(({ row }) => !materializedIds.has(row.thread_id)) || records.some(({ row }) => !materializedIds.has(row.thread_id)) || checkpoints.some(({ row }) => !materializedIds.has(row.thread_id))) return yield* SqliteStorageCorruptionError.make({
1333
+ table: "effect_agent_threads",
1277
1334
  rowKey: "startup_scan",
1278
- message: "Canonical rows exist without a materialized Conversation."
1335
+ message: "Canonical rows exist without a materialized Thread."
1279
1336
  });
1280
1337
  });
1281
- const makeServices$1 = Effect.fn("SqliteConversationStore.makeServices")(function* () {
1338
+ const makeServices$1 = Effect.fn("SqliteThreadStore.makeServices")(function* () {
1282
1339
  const config = yield* SqliteStorageConfig;
1283
1340
  const failpoint = yield* SqliteStorageFailpoint;
1284
1341
  const crypto = yield* Crypto.Crypto;
1285
1342
  const journal = yield* initializeSqliteJournal();
1286
1343
  if (config.verifyOnOpen) yield* decodeStartupPayloads(journal, crypto);
1287
1344
  const provideCrypto = (effect) => Effect.provideService(effect, Crypto.Crypto, crypto);
1288
- const hitFailpoint = Effect.fn("SqliteConversationStore.hitFailpoint")((location) => failpoint.hit(location).pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))));
1289
- const materialize = Effect.fn("SqliteConversationStore.materialize")(function* (request) {
1290
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationMaterialization))(request).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
1345
+ const hitFailpoint = Effect.fn("SqliteThreadStore.hitFailpoint")((location) => failpoint.hit(location).pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))));
1346
+ const materialize = Effect.fn("SqliteThreadStore.materialize")(function* (request) {
1347
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadMaterialization))(request).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
1291
1348
  const now = yield* Clock.currentTimeMillis;
1292
1349
  yield* hitFailpoint("materialize:before");
1293
- yield* journal.materialize(validated.conversationId, new Date(now).toISOString(), EMPTY_TAIL_DIGEST, validated.producerEpoch).pipe(Effect.mapError((error) => error._tag === "SqliteFenceRejected" ? mapFence(validated.conversationId, error) : storeError("materialize conversation", error)));
1350
+ yield* journal.materialize(validated.threadId, new Date(now).toISOString(), EMPTY_TAIL_DIGEST, validated.producerEpoch).pipe(Effect.mapError((error) => error._tag === "SqliteFenceRejected" ? mapFence(validated.threadId, error) : storeError("materialize thread", error)));
1294
1351
  yield* hitFailpoint("materialize:after");
1295
1352
  });
1296
- const append = Effect.fn("SqliteConversationStore.append")(function* (request) {
1353
+ const append = Effect.fn("SqliteThreadStore.append")(function* (request) {
1297
1354
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
1298
- yield* requireConversation(journal, validated.conversationId);
1355
+ yield* requireThread(journal, validated.threadId);
1299
1356
  const tailDigest = yield* provideCrypto(digestCanonicalBatch(validated.expectedTailDigest, validated.batch)).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
1300
1357
  const batchJson = yield* encodeCanonicalBatch(validated.batch);
1301
1358
  const rawRecords = yield* Effect.forEach(validated.batch.records, (record) => encodeCanonicalRecord(record).pipe(Effect.map((recordJson) => ({
@@ -1303,7 +1360,7 @@ const makeServices$1 = Effect.fn("SqliteConversationStore.makeServices")(functio
1303
1360
  recordJson
1304
1361
  }))));
1305
1362
  const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
1306
- conversationId: validated.conversationId,
1363
+ threadId: validated.threadId,
1307
1364
  batchId: validated.batch.batchId,
1308
1365
  batchDigest: tailDigest,
1309
1366
  batchJson,
@@ -1315,15 +1372,15 @@ const makeServices$1 = Effect.fn("SqliteConversationStore.makeServices")(functio
1315
1372
  }).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
1316
1373
  yield* hitFailpoint("append:before");
1317
1374
  const result = yield* journal.append(rawRequest).pipe(Effect.mapError((error) => {
1318
- if (isSqliteFenceRejected(error)) return mapFence(validated.conversationId, error);
1375
+ if (isSqliteFenceRejected(error)) return mapFence(validated.threadId, error);
1319
1376
  if (isSqliteAppendConflict(error)) return error.actualTailSequence !== void 0 && isDigest(error.actualTailDigest) ? AppendConflict.make({
1320
- conversationId: validated.conversationId,
1377
+ threadId: validated.threadId,
1321
1378
  batchId: validated.batch.batchId,
1322
1379
  reason: error.reason,
1323
1380
  actualTailSequence: error.actualTailSequence,
1324
1381
  actualTailDigest: error.actualTailDigest
1325
1382
  }) : AppendConflict.make({
1326
- conversationId: validated.conversationId,
1383
+ threadId: validated.threadId,
1327
1384
  batchId: validated.batch.batchId,
1328
1385
  reason: error.reason
1329
1386
  });
@@ -1332,30 +1389,30 @@ const makeServices$1 = Effect.fn("SqliteConversationStore.makeServices")(functio
1332
1389
  yield* hitFailpoint("append:after");
1333
1390
  return result;
1334
1391
  });
1335
- const loadRecords = Effect.fn("SqliteConversationStore.loadRecords")(function* (request) {
1392
+ const loadRecords = Effect.fn("SqliteThreadStore.loadRecords")(function* (request) {
1336
1393
  const rows = yield* journal.read(request).pipe(Effect.mapError((error) => storeError("read canonical records", error)));
1337
1394
  return yield* Effect.forEach(rows, decodeEnvelope);
1338
1395
  });
1339
- const readEffect = Effect.fn("SqliteConversationStore.read")(function* (request) {
1340
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationRead))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation read", error)));
1341
- yield* requireConversation(journal, validated.conversationId);
1396
+ const readEffect = Effect.fn("SqliteThreadStore.read")(function* (request) {
1397
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadRead))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread read", error)));
1398
+ yield* requireThread(journal, validated.threadId);
1342
1399
  const records = yield* loadRecords(RawReadRequest.make({
1343
- conversationId: validated.conversationId,
1400
+ threadId: validated.threadId,
1344
1401
  fromSequenceExclusive: validated.afterSequence ?? ZERO_CANONICAL_SEQUENCE,
1345
1402
  limit: validated.limit
1346
1403
  }));
1347
1404
  return Stream.fromIterable(records);
1348
1405
  });
1349
1406
  const read = (request) => Stream.unwrap(readEffect(request));
1350
- const observeEffect = Effect.fn("SqliteConversationStore.observe")(function* (request) {
1351
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationObservation))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation observation", error)));
1352
- yield* requireConversation(journal, validated.conversationId);
1353
- const initialSequence = yield* parseOffset(validated.conversationId, validated.afterOffset);
1407
+ const observeEffect = Effect.fn("SqliteThreadStore.observe")(function* (request) {
1408
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadObservation))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread observation", error)));
1409
+ yield* requireThread(journal, validated.threadId);
1410
+ const initialSequence = yield* parseOffset(validated.threadId, validated.afterOffset);
1354
1411
  const cursor = yield* Ref.make(initialSequence);
1355
- const poll = Effect.fn("SqliteConversationStore.observePoll")(function* () {
1412
+ const poll = Effect.fn("SqliteThreadStore.observePoll")(function* () {
1356
1413
  const fromSequenceExclusive = yield* Ref.get(cursor);
1357
1414
  const records = yield* loadRecords(RawReadRequest.make({
1358
- conversationId: validated.conversationId,
1415
+ threadId: validated.threadId,
1359
1416
  fromSequenceExclusive,
1360
1417
  limit: 1024
1361
1418
  }));
@@ -1369,96 +1426,98 @@ const makeServices$1 = Effect.fn("SqliteConversationStore.makeServices")(functio
1369
1426
  return Stream.fromIterableEffectRepeat(poll());
1370
1427
  });
1371
1428
  const observe = (request) => Stream.unwrap(observeEffect(request));
1372
- const exportConversation = Effect.fn("SqliteConversationStore.export")(function* (request) {
1373
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationExportRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate conversation export", error)));
1374
- yield* requireConversation(journal, validated.conversationId);
1375
- const exported = yield* journal.exportConversation(validated.conversationId).pipe(Effect.mapError((error) => storeError("export conversation", error)));
1429
+ const exportThread = Effect.fn("SqliteThreadStore.export")(function* (request) {
1430
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadExportRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread export", error)));
1431
+ yield* requireThread(journal, validated.threadId);
1432
+ const exported = yield* journal.exportThread(validated.threadId).pipe(Effect.mapError((error) => storeError("export thread", error)));
1376
1433
  const records = yield* Effect.forEach(exported.records, decodeEnvelope);
1377
- if (records.length > 65536) return yield* ConversationStoreError.make({
1378
- operation: "decode conversation export",
1379
- message: "The conversation exceeds the current export record limit."
1434
+ if (records.length > 65536) return yield* ThreadStoreError.make({
1435
+ operation: "decode thread export",
1436
+ message: "The thread exceeds the current export record limit."
1380
1437
  });
1381
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.conversation.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
1382
- return ConversationExport.make({
1383
- format: "effect-agent/conversation@1",
1384
- conversationId: validated.conversationId,
1385
- tailSequence: exported.conversation.tail_sequence,
1438
+ const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
1439
+ return ThreadExport.make({
1440
+ format: "effect-agent/thread@1",
1441
+ threadId: validated.threadId,
1442
+ tailSequence: exported.thread.tail_sequence,
1386
1443
  tailDigest,
1387
1444
  records
1388
1445
  });
1389
1446
  });
1390
- const inspectTail = Effect.fn("SqliteConversationStore.inspectTail")(function* (request) {
1391
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationTailRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
1392
- const conversation = yield* requireConversation(journal, validated.conversationId);
1393
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(conversation.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode tail digest", error)));
1394
- return ConversationTail.make({
1395
- conversationId: validated.conversationId,
1396
- tailSequence: conversation.tail_sequence,
1447
+ const inspectTail = Effect.fn("SqliteThreadStore.inspectTail")(function* (request) {
1448
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadTailRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
1449
+ const thread = yield* requireThread(journal, validated.threadId);
1450
+ const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode tail digest", error)));
1451
+ return ThreadTail.make({
1452
+ threadId: validated.threadId,
1453
+ tailSequence: thread.tail_sequence,
1397
1454
  tailDigest,
1398
- producerEpoch: conversation.producer_epoch
1455
+ producerEpoch: thread.producer_epoch
1399
1456
  });
1400
1457
  });
1401
- const saveCheckpoint = Effect.fn("SqliteConversationStore.saveCheckpoint")(function* (request) {
1458
+ const saveCheckpoint = Effect.fn("SqliteThreadStore.saveCheckpoint")(function* (request) {
1402
1459
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
1403
- const conversation = yield* requireConversation(journal, validated.checkpoint.conversationId);
1404
- if (validated.checkpoint.throughSequence > conversation.tail_sequence) return yield* CheckpointRejected.make({
1405
- conversationId: validated.checkpoint.conversationId,
1460
+ const thread = yield* requireThread(journal, validated.checkpoint.threadId);
1461
+ if (validated.checkpoint.throughSequence > thread.tail_sequence) return yield* CheckpointRejected.make({
1462
+ threadId: validated.checkpoint.threadId,
1406
1463
  reason: "ahead-of-tail"
1407
1464
  });
1408
- if ((yield* tailDigestAt(journal, validated.checkpoint.conversationId, validated.checkpoint.throughSequence)) !== validated.checkpoint.tailDigest) return yield* CheckpointRejected.make({
1409
- conversationId: validated.checkpoint.conversationId,
1465
+ if ((yield* tailDigestAt(journal, validated.checkpoint.threadId, validated.checkpoint.throughSequence)) !== validated.checkpoint.tailDigest) return yield* CheckpointRejected.make({
1466
+ threadId: validated.checkpoint.threadId,
1410
1467
  reason: "digest-mismatch"
1411
1468
  });
1412
1469
  const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
1413
1470
  const raw = RawCheckpoint.make({
1414
- conversationId: validated.checkpoint.conversationId,
1471
+ threadId: validated.checkpoint.threadId,
1415
1472
  throughSequence: validated.checkpoint.throughSequence,
1416
1473
  tailDigest: validated.checkpoint.tailDigest,
1417
1474
  checkpointJson
1418
1475
  });
1419
1476
  yield* hitFailpoint("save-checkpoint:before");
1420
1477
  yield* journal.saveCheckpoint(raw).pipe(Effect.mapError((error) => isSqliteCheckpointConflict(error) ? CheckpointRejected.make({
1421
- conversationId: validated.checkpoint.conversationId,
1478
+ threadId: validated.checkpoint.threadId,
1422
1479
  reason: "digest-mismatch"
1423
1480
  }) : storeError("save checkpoint", error)));
1424
1481
  yield* hitFailpoint("save-checkpoint:after");
1425
1482
  });
1426
- const loadCheckpoint = Effect.fn("SqliteConversationStore.loadCheckpoint")(function* (request) {
1483
+ const loadCheckpoint = Effect.fn("SqliteThreadStore.loadCheckpoint")(function* (request) {
1427
1484
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
1428
- const conversation = yield* requireConversation(journal, validated.conversationId);
1429
- const rows = yield* journal.loadCheckpoint(validated.conversationId, validated.atOrBeforeSequence ?? conversation.tail_sequence).pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
1485
+ const thread = yield* requireThread(journal, validated.threadId);
1486
+ const rows = yield* journal.loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence).pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
1430
1487
  if (rows.length === 0) return Option.none();
1431
- if (rows.length !== 1) return yield* ConversationStoreError.make({
1488
+ if (rows.length !== 1) return yield* ThreadStoreError.make({
1432
1489
  operation: "load checkpoint",
1433
1490
  message: `Expected at most one checkpoint row but found ${rows.length}.`
1434
1491
  });
1435
1492
  const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
1436
- if ((yield* tailDigestAt(journal, checkpoint.conversationId, checkpoint.throughSequence)) !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
1437
- conversationId: checkpoint.conversationId,
1493
+ if ((yield* tailDigestAt(journal, checkpoint.threadId, checkpoint.throughSequence)) !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
1494
+ threadId: checkpoint.threadId,
1438
1495
  reason: "digest-mismatch"
1439
1496
  });
1440
1497
  return Option.some(checkpoint);
1441
1498
  });
1442
- const conversationStore = ConversationStore.of({
1499
+ const threadStore = ThreadStore.of({
1443
1500
  append,
1444
- export: exportConversation,
1501
+ export: exportThread,
1445
1502
  inspectTail,
1446
- loadCheckpoint,
1447
1503
  materialize,
1448
1504
  observe,
1449
1505
  read,
1450
- saveCheckpoint
1506
+ checkpoints: {
1507
+ save: saveCheckpoint,
1508
+ load: loadCheckpoint
1509
+ }
1451
1510
  });
1452
- return Context.make(ConversationStore, conversationStore);
1511
+ return Context.make(ThreadStore, threadStore);
1453
1512
  });
1454
1513
  /**
1455
- * SQLite Conversation Store implementation with configuration, failpoint, SQL, and Crypto
1514
+ * SQLite Thread Store implementation with configuration, failpoint, SQL, and Crypto
1456
1515
  * authority kept visible in its input channel.
1457
1516
  */
1458
- const conversationStoreLayer = Layer.effectContext(makeServices$1());
1517
+ const threadStoreLayer = Layer.effectContext(makeServices$1());
1459
1518
  /**
1460
1519
  * Validated SQLite storage configuration Layer with the documented defaults applied. Shared
1461
- * by the ConversationStore and SubmissionLedger convenience layers so their defaults cannot
1520
+ * by the ThreadStore and SubmissionLedger convenience layers so their defaults cannot
1462
1521
  * drift.
1463
1522
  */
1464
1523
  const storageConfigLayer = (options) => Layer.effect(SqliteStorageConfig)(Schema.decodeUnknownEffect(SqliteStorageConfigValue)({
@@ -1474,10 +1533,10 @@ const storageConfigLayer = (options) => Layer.effect(SqliteStorageConfig)(Schema
1474
1533
  /** The failpoint Layer selected by convenience options: explicit handler or the no-op default. */
1475
1534
  const storageFailpointLayer = (options) => options.failpoint === void 0 ? SqliteStorageFailpoint.layer : Layer.succeed(SqliteStorageFailpoint)({ hit: options.failpoint });
1476
1535
  /**
1477
- * A composition-root convenience Layer for canonical Conversations. Durable accepted work is
1536
+ * A composition-root convenience Layer for canonical Threads. Durable accepted work is
1478
1537
  * served by the separate SubmissionLedger port.
1479
1538
  */
1480
- const layer = (options) => Layer.unwrap(Effect.map(SqliteStorageConfig, (config) => conversationStoreLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(SqliteStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ filename: options.filename }), NodeCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
1539
+ const layer = (options) => Layer.unwrap(Effect.map(SqliteStorageConfig, (config) => threadStoreLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(SqliteStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ filename: options.filename }), NodeCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
1481
1540
  /** Create an adapter-owned resumable observation offset for a known canonical sequence. */
1482
1541
  const observationOffsetAt = makeOffset;
1483
1542
  //#endregion
@@ -1494,7 +1553,7 @@ const STILL_WAITING = "still-waiting";
1494
1553
  const WOKEN = "woken";
1495
1554
  var SubmissionRow = class extends Schema.Class("SubmissionRow")({
1496
1555
  submission_id: BoundedIdentifier,
1497
- conversation_id: BoundedIdentifier,
1556
+ thread_id: BoundedIdentifier,
1498
1557
  queue_sequence: QueueSequence,
1499
1558
  principal: BoundedIdentifier,
1500
1559
  idempotency_key: BoundedIdentifier,
@@ -1576,7 +1635,7 @@ var MaxQueueSequenceRow = class extends Schema.Class("MaxQueueSequenceRow")({ ma
1576
1635
  var CanonicalRecordIdRow = class extends Schema.Class("CanonicalRecordIdRow")({ record_id: BoundedIdentifier }) {};
1577
1636
  const SUBMISSION_COLUMNS = `
1578
1637
  submission_id,
1579
- conversation_id,
1638
+ thread_id,
1580
1639
  queue_sequence,
1581
1640
  principal,
1582
1641
  idempotency_key,
@@ -1613,7 +1672,7 @@ const CHILD_RESERVATION_COLUMNS = `
1613
1672
  release_began_at,
1614
1673
  released_at
1615
1674
  `;
1616
- /** The branded ToolCallId schema, reached through the session port so no core import is needed. */
1675
+ /** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
1617
1676
  const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
1618
1677
  const ToolCallIdList = Schema.Array(ToolCallIdSchema);
1619
1678
  const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
@@ -1691,7 +1750,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1691
1750
  iso: new Date(millis).toISOString()
1692
1751
  }));
1693
1752
  const timestampMillis = (operation, rowKey) => (timestamp) => decodeUtcInstant(timestamp).pipe(Effect.map(DateTime.toEpochMillis), Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submission_ownership", rowKey, error.message)));
1694
- const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows$1(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1753
+ const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1695
1754
  const readSubmission = Effect.fn("SqliteSubmissionLedger.readSubmission")(function* (operation, submissionId) {
1696
1755
  const rows = yield* sql`
1697
1756
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
@@ -1722,23 +1781,23 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1722
1781
  FROM effect_agent_submission_ownership
1723
1782
  WHERE submission_id = ${submissionId}
1724
1783
  `.pipe(Effect.mapError(sqlFailure(operation)));
1725
- const decoded = yield* decodeRows$1(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1784
+ const decoded = yield* decodeRows$2(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1726
1785
  if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submission_ownership", submissionId, "An ownership primary key returned more than one row.");
1727
1786
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1728
1787
  });
1729
- const conversationEpoch = Effect.fn("SqliteSubmissionLedger.conversationEpoch")(function* (operation, conversationId) {
1730
- const conversations = yield* journal.getConversation(conversationId).pipe(Effect.mapError(internalFailure(operation)));
1731
- return conversations.length === 0 ? EPOCH_ZERO : conversations[0].producer_epoch;
1788
+ const threadEpoch = Effect.fn("SqliteSubmissionLedger.threadEpoch")(function* (operation, threadId) {
1789
+ const threads = yield* journal.getThread(threadId).pipe(Effect.mapError(internalFailure(operation)));
1790
+ return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
1732
1791
  });
1733
1792
  /**
1734
1793
  * Verify inside the surrounding write transaction that the presented token still owns the
1735
1794
  * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
1736
- * Conversation's current producer epoch (DUR-006).
1795
+ * Thread's current producer epoch (DUR-006).
1737
1796
  */
1738
1797
  const requireOwnership = Effect.fn("SqliteSubmissionLedger.requireOwnership")(function* (operation, submission, ownershipToken) {
1739
1798
  const ownership = yield* readOwnership(operation, submission.submission_id);
1740
1799
  if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
1741
- const actualEpoch = yield* conversationEpoch(operation, submission.conversation_id);
1800
+ const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
1742
1801
  const submissionId = yield* Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId)(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
1743
1802
  return yield* OwnershipLost.make({
1744
1803
  submissionId,
@@ -1753,7 +1812,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1753
1812
  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.");
1754
1813
  return yield* decodeSubmissionSnapshotUnknown({
1755
1814
  submissionId: row.submission_id,
1756
- conversationId: row.conversation_id,
1815
+ threadId: row.thread_id,
1757
1816
  queueSequence: row.queue_sequence,
1758
1817
  principal: row.principal,
1759
1818
  idempotencyKey: row.idempotency_key,
@@ -1787,7 +1846,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1787
1846
  FROM effect_agent_settlement_reservations
1788
1847
  WHERE submission_id = ${submissionId}
1789
1848
  `.pipe(Effect.mapError(sqlFailure(operation)));
1790
- const decoded = yield* decodeRows$1(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1849
+ const decoded = yield* decodeRows$2(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1791
1850
  if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", submissionId, "A settlement reservation primary key returned more than one row.");
1792
1851
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1793
1852
  });
@@ -1802,11 +1861,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1802
1861
  FROM effect_agent_abort_intents
1803
1862
  WHERE submission_id = ${submissionId}
1804
1863
  `.pipe(Effect.mapError(sqlFailure(operation)));
1805
- const decoded = yield* decodeRows$1(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1864
+ const decoded = yield* decodeRows$2(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1806
1865
  if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_abort_intents", submissionId, "An abort intent primary key returned more than one row.");
1807
1866
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1808
1867
  });
1809
- const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$1(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1868
+ const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1810
1869
  const readChildReservation = Effect.fn("SqliteSubmissionLedger.readChildReservation")(function* (operation, reservationId) {
1811
1870
  const rows = yield* sql`
1812
1871
  SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
@@ -1859,7 +1918,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1859
1918
  WHERE submission_id = ${submissionId}
1860
1919
  ORDER BY tool_call_id ASC
1861
1920
  `.pipe(Effect.mapError(sqlFailure(operation)));
1862
- return yield* decodeRows$1(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1921
+ return yield* decodeRows$2(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1863
1922
  });
1864
1923
  const approvalIntentFromRow = Effect.fn("SqliteSubmissionLedger.approvalIntentFromRow")(function* (operation, row) {
1865
1924
  return yield* decodeApprovalDecisionIntent({
@@ -1884,7 +1943,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1884
1943
  WHERE submission_id = ${submissionId}
1885
1944
  ORDER BY tool_call_id ASC
1886
1945
  `.pipe(Effect.mapError(sqlFailure(operation)));
1887
- return yield* decodeRows$1(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1946
+ return yield* decodeRows$2(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1888
1947
  });
1889
1948
  const unknownResolutionIntentFromRow = Effect.fn("SqliteSubmissionLedger.unknownResolutionIntentFromRow")(function* (operation, row) {
1890
1949
  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)));
@@ -1907,18 +1966,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1907
1966
  * derived from the shared canonical-records table using the deterministic abort record
1908
1967
  * identity, never from a cached ledger marker.
1909
1968
  */
1910
- const canonicalAbortRecordId = Effect.fn("SqliteSubmissionLedger.canonicalAbortRecordId")(function* (operation, conversationId, submissionId) {
1969
+ const canonicalAbortRecordId = Effect.fn("SqliteSubmissionLedger.canonicalAbortRecordId")(function* (operation, threadId, submissionId) {
1911
1970
  const recordId = submissionAbortRecordId(submissionId);
1912
1971
  const rows = yield* sql`
1913
1972
  SELECT record_id
1914
1973
  FROM effect_agent_canonical_records
1915
- WHERE conversation_id = ${conversationId}
1974
+ WHERE thread_id = ${threadId}
1916
1975
  AND record_id = ${recordId}
1917
1976
  `.pipe(Effect.mapError(sqlFailure(operation)));
1918
- return (yield* decodeRows$1(Schema.Array(CanonicalRecordIdRow), "effect_agent_canonical_records", `${conversationId}/${recordId}`, rows).pipe(Effect.mapError(internalFailure(operation)))).length === 0 ? void 0 : recordId;
1977
+ return (yield* decodeRows$2(Schema.Array(CanonicalRecordIdRow), "effect_agent_canonical_records", `${threadId}/${recordId}`, rows).pipe(Effect.mapError(internalFailure(operation)))).length === 0 ? void 0 : recordId;
1919
1978
  });
1920
1979
  const abortIntentFromRow = Effect.fn("SqliteSubmissionLedger.abortIntentFromRow")(function* (operation, submission, submissionId, row) {
1921
- const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.conversation_id, submissionId);
1980
+ const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, submissionId);
1922
1981
  return yield* decodeAbortIntent({
1923
1982
  submissionId: row.submission_id,
1924
1983
  author: row.author,
@@ -1937,11 +1996,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1937
1996
  const mintedReceiptId = yield* mintIdentifier("receipt", operation);
1938
1997
  yield* hitFailpoint("ledger:admit:before", operation);
1939
1998
  const result = yield* inWriteTransaction(operation, Effect.gen(function* () {
1940
- const keyRowKey = `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`;
1999
+ const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
1941
2000
  const existingRows = yield* sql`
1942
2001
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1943
2002
  FROM effect_agent_submissions
1944
- WHERE conversation_id = ${validated.conversationId}
2003
+ WHERE thread_id = ${validated.threadId}
1945
2004
  AND principal = ${validated.principal}
1946
2005
  AND idempotency_key = ${validated.idempotencyKey}
1947
2006
  `.pipe(Effect.mapError(sqlFailure(operation)));
@@ -1950,7 +2009,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1950
2009
  if (existing.length === 1) {
1951
2010
  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;
1952
2011
  if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) return yield* AdmissionConflict.make({
1953
- conversationId: validated.conversationId,
2012
+ threadId: validated.threadId,
1954
2013
  principal: validated.principal,
1955
2014
  idempotencyKey: validated.idempotencyKey,
1956
2015
  existingInputDigest: existing[0].input_digest,
@@ -1967,15 +2026,15 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1967
2026
  const maxRows = yield* sql`
1968
2027
  SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
1969
2028
  FROM effect_agent_submissions
1970
- WHERE conversation_id = ${validated.conversationId}
2029
+ WHERE thread_id = ${validated.threadId}
1971
2030
  `.pipe(Effect.mapError(sqlFailure(operation)));
1972
- const decodedMax = yield* decodeRows$1(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.conversationId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
2031
+ const decodedMax = yield* decodeRows$2(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.threadId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
1973
2032
  const queueSequence = yield* decodeQueueSequence((decodedMax[0]?.max_queue_sequence ?? 0) + 1).pipe(Effect.mapError(internalFailure(operation)));
1974
2033
  const now = yield* currentInstant;
1975
2034
  yield* sql`
1976
2035
  INSERT INTO effect_agent_submissions (
1977
2036
  submission_id,
1978
- conversation_id,
2037
+ thread_id,
1979
2038
  queue_sequence,
1980
2039
  principal,
1981
2040
  idempotency_key,
@@ -1991,7 +2050,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1991
2050
  parent_tool_call_id
1992
2051
  ) VALUES (
1993
2052
  ${mintedSubmissionId},
1994
- ${validated.conversationId},
2053
+ ${validated.threadId},
1995
2054
  ${queueSequence},
1996
2055
  ${validated.principal},
1997
2056
  ${validated.idempotencyKey},
@@ -2044,12 +2103,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2044
2103
  const rows = yield* sql`
2045
2104
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2046
2105
  FROM effect_agent_submissions
2047
- WHERE conversation_id = ${validated.conversationId}
2106
+ WHERE thread_id = ${validated.threadId}
2048
2107
  AND principal = ${validated.principal}
2049
2108
  AND idempotency_key = ${validated.idempotencyKey}
2050
2109
  `.pipe(Effect.mapError(sqlFailure(operation)));
2051
- const decoded = yield* decodeSubmissionRows(operation, `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2052
- 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.");
2110
+ const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2111
+ 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.");
2053
2112
  if (decoded.length === 0) return Option.none();
2054
2113
  return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
2055
2114
  });
@@ -2059,12 +2118,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2059
2118
  const rows = yield* sql`
2060
2119
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2061
2120
  FROM effect_agent_submissions
2062
- WHERE conversation_id = ${validated.conversationId}
2121
+ WHERE thread_id = ${validated.threadId}
2063
2122
  AND principal = ${validated.principal}
2064
2123
  AND idempotency_key = ${validated.idempotencyKey}
2065
2124
  `.pipe(Effect.mapError(sqlFailure(operation)));
2066
- const decoded = yield* decodeSubmissionRows(operation, `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2067
- 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.");
2125
+ const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
2126
+ 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.");
2068
2127
  if (decoded.length === 0) return AdmissionNotAdmitted.make();
2069
2128
  return AdmissionAdmitted.make({ submission: yield* decodeSubmissionSnapshot(operation, decoded[0]) });
2070
2129
  });
@@ -2078,12 +2137,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2078
2137
  const headRows = yield* sql`
2079
2138
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2080
2139
  FROM effect_agent_submissions
2081
- WHERE conversation_id = ${validated.conversationId}
2140
+ WHERE thread_id = ${validated.threadId}
2082
2141
  AND state <> 'settled'
2083
2142
  ORDER BY queue_sequence ASC
2084
2143
  LIMIT 1
2085
2144
  `.pipe(Effect.mapError(sqlFailure(operation)));
2086
- const heads = yield* decodeSubmissionRows(operation, validated.conversationId, headRows);
2145
+ const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
2087
2146
  if (heads.length === 0) return Option.none();
2088
2147
  const head = heads[0];
2089
2148
  if (head.state === "joining" || head.state === "joined" || head.state === "suspended" || head.state === "unknown" && Option.isNone(yield* readAbortIntent(operation, head.submission_id))) return Option.none();
@@ -2092,19 +2151,19 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2092
2151
  if (Option.isSome(ownership)) {
2093
2152
  if ((yield* timestampMillis(operation, head.submission_id)(ownership.value.lease_expires_at)) > now.millis) return Option.none();
2094
2153
  }
2095
- const conversations = yield* journal.getConversation(head.conversation_id).pipe(Effect.mapError(internalFailure(operation)));
2154
+ const threads = yield* journal.getThread(head.thread_id).pipe(Effect.mapError(internalFailure(operation)));
2096
2155
  let producerEpoch;
2097
- if (conversations.length === 0) {
2156
+ if (threads.length === 0) {
2098
2157
  producerEpoch = 1;
2099
2158
  yield* sql`
2100
- INSERT INTO effect_agent_conversations (
2101
- conversation_id,
2159
+ INSERT INTO effect_agent_threads (
2160
+ thread_id,
2102
2161
  created_at,
2103
2162
  tail_sequence,
2104
2163
  tail_digest,
2105
2164
  producer_epoch
2106
2165
  ) VALUES (
2107
- ${head.conversation_id},
2166
+ ${head.thread_id},
2108
2167
  ${now.iso},
2109
2168
  0,
2110
2169
  ${EMPTY_TAIL_DIGEST},
@@ -2112,11 +2171,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2112
2171
  )
2113
2172
  `.pipe(Effect.mapError(sqlFailure(operation)));
2114
2173
  } else {
2115
- producerEpoch = conversations[0].producer_epoch + 1;
2174
+ producerEpoch = threads[0].producer_epoch + 1;
2116
2175
  yield* sql`
2117
- UPDATE effect_agent_conversations
2176
+ UPDATE effect_agent_threads
2118
2177
  SET producer_epoch = ${producerEpoch}
2119
- WHERE conversation_id = ${head.conversation_id}
2178
+ WHERE thread_id = ${head.thread_id}
2120
2179
  `.pipe(Effect.mapError(sqlFailure(operation)));
2121
2180
  }
2122
2181
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
@@ -2147,14 +2206,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2147
2206
  INSERT INTO effect_agent_attempts (
2148
2207
  attempt_id,
2149
2208
  submission_id,
2150
- conversation_id,
2209
+ thread_id,
2151
2210
  owner_producer_id,
2152
2211
  producer_epoch,
2153
2212
  claimed_at
2154
2213
  ) VALUES (
2155
2214
  ${attemptId},
2156
2215
  ${head.submission_id},
2157
- ${head.conversation_id},
2216
+ ${head.thread_id},
2158
2217
  ${validated.producerId},
2159
2218
  ${producerEpoch},
2160
2219
  ${now.iso}
@@ -2414,7 +2473,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2414
2473
  ${now.iso}
2415
2474
  )
2416
2475
  `.pipe(Effect.mapError(sqlFailure(operation)));
2417
- const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.conversation_id, validated.submissionId);
2476
+ const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, validated.submissionId);
2418
2477
  return yield* decodeAbortIntent({
2419
2478
  submissionId: validated.submissionId,
2420
2479
  author: validated.author,
@@ -2432,19 +2491,19 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2432
2491
  yield* hitFailpoint("ledger:claim-joining:before", operation);
2433
2492
  const claims = yield* inWriteTransaction(operation, Effect.gen(function* () {
2434
2493
  const host = yield* requireSubmission(operation, validated.hostSubmissionId);
2435
- if (host.conversation_id !== validated.conversationId) return yield* LedgerError.make({
2494
+ if (host.thread_id !== validated.threadId) return yield* LedgerError.make({
2436
2495
  operation,
2437
- message: `Host submission ${validated.hostSubmissionId} does not belong to conversation ${validated.conversationId}.`
2496
+ message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`
2438
2497
  });
2439
2498
  yield* requireOwnership(operation, host, validated.ownershipToken);
2440
2499
  const laterRows = yield* sql`
2441
2500
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2442
2501
  FROM effect_agent_submissions
2443
- WHERE conversation_id = ${validated.conversationId}
2502
+ WHERE thread_id = ${validated.threadId}
2444
2503
  AND queue_sequence > ${host.queue_sequence}
2445
2504
  ORDER BY queue_sequence ASC
2446
2505
  `.pipe(Effect.mapError(sqlFailure(operation)));
2447
- const later = yield* decodeSubmissionRows(operation, validated.conversationId, laterRows);
2506
+ const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
2448
2507
  const claimed = [];
2449
2508
  for (const row of later) {
2450
2509
  if (claimed.length >= validated.maxCount) break;
@@ -2947,27 +3006,27 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2947
3006
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2948
3007
  FROM effect_agent_submissions
2949
3008
  WHERE state <> 'settled'
2950
- ORDER BY conversation_id ASC, queue_sequence ASC
3009
+ ORDER BY thread_id ASC, queue_sequence ASC
2951
3010
  LIMIT ${SCAN_PAGE_SIZE}
2952
3011
  ` : sql`
2953
3012
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2954
3013
  FROM effect_agent_submissions
2955
3014
  WHERE state <> 'settled'
2956
3015
  AND (
2957
- conversation_id > ${cursor.conversationId}
3016
+ thread_id > ${cursor.threadId}
2958
3017
  OR (
2959
- conversation_id = ${cursor.conversationId}
3018
+ thread_id = ${cursor.threadId}
2960
3019
  AND queue_sequence > ${cursor.queueSequence}
2961
3020
  )
2962
3021
  )
2963
- ORDER BY conversation_id ASC, queue_sequence ASC
3022
+ ORDER BY thread_id ASC, queue_sequence ASC
2964
3023
  LIMIT ${SCAN_PAGE_SIZE}
2965
3024
  `).pipe(Effect.mapError(sqlFailure(operation)));
2966
3025
  const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
2967
3026
  const snapshots = yield* Effect.forEach(decoded, (row) => decodeSubmissionSnapshot(operation, row));
2968
3027
  const last = decoded[decoded.length - 1];
2969
3028
  return [snapshots, last === void 0 || decoded.length < SCAN_PAGE_SIZE ? Option.none() : Option.some({
2970
- conversationId: last.conversation_id,
3029
+ threadId: last.thread_id,
2971
3030
  queueSequence: last.queue_sequence
2972
3031
  })];
2973
3032
  });
@@ -3112,7 +3171,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
3112
3171
  const submissionLedgerLayer = Layer.effectContext(makeServices());
3113
3172
  /**
3114
3173
  * A composition-root convenience Layer for the durable Submission Ledger. Point it at the
3115
- * same database file as the ConversationStore so claims fence the same producer epochs.
3174
+ * same database file as the ThreadStore so claims fence the same producer epochs.
3116
3175
  */
3117
3176
  const ledgerLayer = (options) => Layer.unwrap(Effect.map(SqliteStorageConfig, (config) => submissionLedgerLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(SqliteStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ filename: options.filename }), NodeCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
3118
3177
  //#endregion
@@ -3134,27 +3193,27 @@ const ScheduleDueRow = Schema.Struct({
3134
3193
  });
3135
3194
  var ScheduleCountRow = class extends Schema.Class("@effect-agent/storage-sqlite/ScheduleCountRow")({ schedule_count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) }) {};
3136
3195
  var ScheduleDeadlineRow = class extends Schema.Class("@effect-agent/storage-sqlite/ScheduleDeadlineRow")({ deadline_at_millis: StoredDeadline }) {};
3137
- const unavailable = (operation) => ScheduleStorageError.make({
3196
+ const unavailable$1 = (operation) => ScheduleStorageError.make({
3138
3197
  operation,
3139
3198
  reason: "unavailable"
3140
3199
  });
3141
- const corrupt = (operation) => ScheduleStorageError.make({
3200
+ const corrupt$1 = (operation) => ScheduleStorageError.make({
3142
3201
  operation,
3143
3202
  reason: "corrupt"
3144
3203
  });
3145
- const decodeRows = Effect.fn("SqliteScheduleStore.decodeRows")(function* (schema, rows, operation) {
3146
- return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt(operation)));
3204
+ const decodeRows$1 = Effect.fn("SqliteScheduleStore.decodeRows")(function* (schema, rows, operation) {
3205
+ return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt$1(operation)));
3147
3206
  });
3148
3207
  const decodeRecord = Effect.fn("SqliteScheduleStore.decodeRecord")(function* (row) {
3149
- const record = yield* Schema.decodeEffect(Schema.fromJsonString(ScheduleRecord))(row.record_json).pipe(Effect.mapError(() => corrupt("decode schedule")));
3150
- 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 identity");
3208
+ const record = yield* Schema.decodeEffect(Schema.fromJsonString(ScheduleRecord))(row.record_json).pipe(Effect.mapError(() => corrupt$1("decode schedule")));
3209
+ if (record.owner.tenantId !== row.tenant_id || record.owner.ownerId !== row.owner_id || record.scheduleId !== row.schedule_id || scheduleDeadline(record) !== row.deadline_at_millis) return yield* corrupt$1("decode schedule identity");
3151
3210
  return record;
3152
3211
  });
3153
3212
  const encodeRecord = Effect.fn("SqliteScheduleStore.encodeRecord")(function* (record) {
3154
- return yield* Schema.encodeEffect(Schema.fromJsonString(ScheduleRecord))(record).pipe(Effect.mapError(() => corrupt("encode schedule")));
3213
+ return yield* Schema.encodeEffect(Schema.fromJsonString(ScheduleRecord))(record).pipe(Effect.mapError(() => corrupt$1("encode schedule")));
3155
3214
  });
3156
3215
  const decodeInput = Effect.fn("SqliteScheduleStore.decodeInput")(function* (operation, schema, value) {
3157
- return yield* Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt(operation)));
3216
+ return yield* Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt$1(operation)));
3158
3217
  });
3159
3218
  const makeScheduleStore = Effect.gen(function* () {
3160
3219
  const sql = yield* SqlClient.SqlClient;
@@ -3167,13 +3226,13 @@ const makeScheduleStore = Effect.gen(function* () {
3167
3226
  WHERE tenant_id = ${key.owner.tenantId}
3168
3227
  AND owner_id = ${key.owner.ownerId}
3169
3228
  AND schedule_id = ${key.scheduleId}
3170
- `.pipe(Effect.mapError(() => unavailable(operation)));
3171
- return yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
3229
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3230
+ return yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
3172
3231
  });
3173
3232
  const readOne = Effect.fn("SqliteScheduleStore.readOne")(function* (key, operation) {
3174
3233
  const rows = yield* readRows(key, operation);
3175
3234
  if (rows.length === 0) return null;
3176
- if (rows.length !== 1) return yield* corrupt(operation);
3235
+ if (rows.length !== 1) return yield* corrupt$1(operation);
3177
3236
  return yield* decodeRecord(rows[0]);
3178
3237
  });
3179
3238
  const insert = Effect.fn("SqliteScheduleStore.insert")(function* (record, ownerLimit) {
@@ -3202,9 +3261,9 @@ const makeScheduleStore = Effect.gen(function* () {
3202
3261
  AND owner_id = ${canonical.owner.ownerId}
3203
3262
  AND (json_extract(record_json, '$.pending') IS NOT NULL OR
3204
3263
  (json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
3205
- `.pipe(Effect.mapError(() => unavailable(operation)));
3206
- const counts = yield* decodeRows(Schema.Array(ScheduleCountRow), rawCounts, operation);
3207
- if (counts.length !== 1) return yield* corrupt(operation);
3264
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3265
+ const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
3266
+ if (counts.length !== 1) return yield* corrupt$1(operation);
3208
3267
  if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
3209
3268
  yield* scheduleFailpoint.hit("schedule:insert:before");
3210
3269
  yield* sql`
@@ -3217,12 +3276,12 @@ const makeScheduleStore = Effect.gen(function* () {
3217
3276
  ${scheduleDeadline(canonical)},
3218
3277
  ${recordJson}
3219
3278
  )
3220
- `.pipe(Effect.mapError(() => unavailable(operation)));
3279
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3221
3280
  return {
3222
3281
  record: canonical,
3223
3282
  inserted: true
3224
3283
  };
3225
- })).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable(operation))));
3284
+ })).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable$1(operation))));
3226
3285
  if (result.inserted) yield* scheduleFailpoint.hit("schedule:insert:after");
3227
3286
  return result.record;
3228
3287
  });
@@ -3240,7 +3299,7 @@ const makeScheduleStore = Effect.gen(function* () {
3240
3299
  AND owner_id = ${decodedRequest.owner.ownerId}
3241
3300
  ORDER BY schedule_id
3242
3301
  LIMIT ${decodedRequest.limit + 1}
3243
- `.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
3302
+ `.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
3244
3303
  SELECT tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
3245
3304
  FROM effect_agent_schedules
3246
3305
  WHERE tenant_id = ${decodedRequest.owner.tenantId}
@@ -3248,8 +3307,8 @@ const makeScheduleStore = Effect.gen(function* () {
3248
3307
  AND schedule_id > ${decodedRequest.after}
3249
3308
  ORDER BY schedule_id
3250
3309
  LIMIT ${decodedRequest.limit + 1}
3251
- `.pipe(Effect.mapError(() => unavailable(operation)));
3252
- const decoded = yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
3310
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3311
+ const decoded = yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
3253
3312
  const records = yield* Effect.forEach(decoded, decodeRecord);
3254
3313
  const hasNext = records.length > decodedRequest.limit;
3255
3314
  const items = hasNext ? records.slice(0, decodedRequest.limit) : records;
@@ -3274,9 +3333,9 @@ const makeScheduleStore = Effect.gen(function* () {
3274
3333
  WHERE tenant_id = ${key.owner.tenantId} AND owner_id = ${key.owner.ownerId}
3275
3334
  AND (json_extract(record_json, '$.pending') IS NOT NULL OR
3276
3335
  (json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
3277
- `.pipe(Effect.mapError(() => unavailable(operation)));
3278
- const counts = yield* decodeRows(Schema.Array(ScheduleCountRow), rawCounts, operation);
3279
- if (counts.length !== 1) return yield* corrupt(operation);
3336
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3337
+ const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
3338
+ if (counts.length !== 1) return yield* corrupt$1(operation);
3280
3339
  if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
3281
3340
  }
3282
3341
  if (next === current) return {
@@ -3291,19 +3350,19 @@ const makeScheduleStore = Effect.gen(function* () {
3291
3350
  WHERE tenant_id = ${decodedKey.owner.tenantId}
3292
3351
  AND owner_id = ${decodedKey.owner.ownerId}
3293
3352
  AND schedule_id = ${decodedKey.scheduleId}
3294
- `.pipe(Effect.mapError(() => unavailable(operation)));
3353
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3295
3354
  return {
3296
3355
  record: next,
3297
3356
  changed: true
3298
3357
  };
3299
- })).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable(operation))));
3358
+ })).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable$1(operation))));
3300
3359
  if (result.changed) yield* scheduleFailpoint.hit(`schedule:${decodedChange._tag.toLowerCase()}:after`);
3301
3360
  return result.record;
3302
3361
  });
3303
3362
  const due = Effect.fn("SqliteScheduleStore.due")(function* (nowMillis, limit, owner, after) {
3304
3363
  const operation = "query due schedules";
3305
3364
  const decodedOwner = owner === void 0 ? void 0 : yield* decodeInput(operation, ScheduleOwner, owner);
3306
- const cursor = after === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(Effect.mapError(() => corrupt(operation)));
3365
+ const cursor = after === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(Effect.mapError(() => corrupt$1(operation)));
3307
3366
  const continuation = cursor === void 0 ? sql`1 = 1` : sql`
3308
3367
  (deadline_at_millis, tenant_id, owner_id, schedule_id) >
3309
3368
  (${cursor.deadlineAtMillis}, ${cursor.owner.tenantId}, ${cursor.owner.ownerId}, ${cursor.scheduleId})`;
@@ -3313,7 +3372,7 @@ const makeScheduleStore = Effect.gen(function* () {
3313
3372
  WHERE deadline_at_millis <= ${nowMillis} AND ${continuation}
3314
3373
  ORDER BY deadline_at_millis, tenant_id, owner_id, schedule_id
3315
3374
  LIMIT ${limit}
3316
- `.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
3375
+ `.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
3317
3376
  SELECT tenant_id, owner_id, schedule_id, deadline_at_millis
3318
3377
  FROM effect_agent_schedules
3319
3378
  WHERE tenant_id = ${decodedOwner.tenantId}
@@ -3321,8 +3380,8 @@ const makeScheduleStore = Effect.gen(function* () {
3321
3380
  AND deadline_at_millis <= ${nowMillis} AND ${continuation}
3322
3381
  ORDER BY deadline_at_millis, schedule_id
3323
3382
  LIMIT ${limit}
3324
- `.pipe(Effect.mapError(() => unavailable(operation)));
3325
- return (yield* decodeRows(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
3383
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3384
+ return (yield* decodeRows$1(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
3326
3385
  owner: {
3327
3386
  tenantId: row.tenant_id,
3328
3387
  ownerId: row.owner_id
@@ -3338,15 +3397,15 @@ const makeScheduleStore = Effect.gen(function* () {
3338
3397
  SELECT MIN(deadline_at_millis) AS deadline_at_millis
3339
3398
  FROM effect_agent_schedules
3340
3399
  WHERE deadline_at_millis IS NOT NULL
3341
- `.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
3400
+ `.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
3342
3401
  SELECT MIN(deadline_at_millis) AS deadline_at_millis
3343
3402
  FROM effect_agent_schedules
3344
3403
  WHERE tenant_id = ${decodedOwner.tenantId}
3345
3404
  AND owner_id = ${decodedOwner.ownerId}
3346
3405
  AND deadline_at_millis IS NOT NULL
3347
- `.pipe(Effect.mapError(() => unavailable(operation)));
3348
- const decoded = yield* decodeRows(Schema.Array(ScheduleDeadlineRow), rows, operation);
3349
- if (decoded.length !== 1) return yield* corrupt(operation);
3406
+ `.pipe(Effect.mapError(() => unavailable$1(operation)));
3407
+ const decoded = yield* decodeRows$1(Schema.Array(ScheduleDeadlineRow), rows, operation);
3408
+ if (decoded.length !== 1) return yield* corrupt$1(operation);
3350
3409
  return decoded[0].deadline_at_millis;
3351
3410
  });
3352
3411
  return ScheduleStore.of({
@@ -3361,6 +3420,590 @@ const makeScheduleStore = Effect.gen(function* () {
3361
3420
  /** SQLite implementation of the atomic ScheduleStore port. */
3362
3421
  const scheduleStoreLayer = Layer.effect(ScheduleStore)(makeScheduleStore);
3363
3422
  //#endregion
3364
- export { CurrentSqliteStorageVersion, SqliteAppendConflict, SqliteCheckpointConflict, SqliteFenceRejected, SqliteLedgerError, SqliteStorageCompatibilityError, SqliteStorageConfig, SqliteStorageConfigValue, SqliteStorageCorruptionError, SqliteStorageError, SqliteStorageFailpoint, SqliteStorageFailpointError, SqliteStorageFailpointLocation, SqliteStorageFailpointTestControl, SqliteWriteContention, conversationStoreLayer, layer, ledgerLayer, observationOffsetAt, scheduleStoreLayer, sqliteMigrations, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer };
3423
+ //#region src/sqlite-subscription-store.ts
3424
+ const StoredJson = Schema.String.check(Schema.isMaxLength(16 * 1024 * 1024));
3425
+ const CountRow = Schema.Struct({ count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) });
3426
+ const SequenceRow = Schema.Struct({ sequence: Schema.Natural });
3427
+ const ScanRow = Schema.Struct({
3428
+ event_scan_cursor: Schema.String,
3429
+ delivery_scan_cursor: Schema.String,
3430
+ recovery_scan_cursor: Schema.Natural
3431
+ });
3432
+ const JsonRow = Schema.Struct({ record_json: StoredJson });
3433
+ const RegistrationRow = Schema.Struct({
3434
+ owner_id: Schema.String,
3435
+ subscription_id: Schema.String,
3436
+ ordinal: Schema.Natural,
3437
+ source_name: Schema.String,
3438
+ source_version: Schema.String,
3439
+ matching_key: Schema.String,
3440
+ state: SubscriptionRecord.fields.state,
3441
+ expires_at_millis: Schema.Number,
3442
+ recovery_at_millis: Schema.NullOr(Schema.Number),
3443
+ record_json: StoredJson
3444
+ });
3445
+ const EventRow = Schema.Struct({
3446
+ event_id: Schema.String,
3447
+ source_name: Schema.String,
3448
+ source_version: Schema.String,
3449
+ matching_key: Schema.String,
3450
+ payload_digest: Digest,
3451
+ cutoff: Schema.Natural,
3452
+ cursor: Schema.Natural,
3453
+ routing_complete: Schema.Number,
3454
+ next_attempt_at_millis: Schema.Number,
3455
+ record_json: StoredJson
3456
+ });
3457
+ const DeliveryRow = Schema.Struct({
3458
+ owner_id: Schema.String,
3459
+ subscription_id: Schema.String,
3460
+ event_id: Schema.String,
3461
+ delivery_key: Schema.String,
3462
+ state: SubscriptionDelivery.fields.state,
3463
+ next_attempt_at_millis: Schema.Number,
3464
+ record_json: StoredJson
3465
+ });
3466
+ const error = (reason, code) => SubscriptionError.make({
3467
+ reason,
3468
+ code
3469
+ });
3470
+ const unavailable = (operation) => error("storage", operation);
3471
+ const corrupt = (operation) => error("corrupt", operation);
3472
+ const bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
3473
+ const validate = (schema, value, code) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => error("validation", code)));
3474
+ const encode = (schema, value, code) => Schema.encodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
3475
+ const decode = (schema, value, code) => Schema.decodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
3476
+ const decodeRows = (schema, rows, code) => Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(Effect.mapError(() => corrupt(code)));
3477
+ 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;
3478
+ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function* (owned) {
3479
+ const partition = yield* validate(SourcePartition, owned, "partition");
3480
+ const sql = yield* SqlClient.SqlClient;
3481
+ const failpoint = yield* SubscriptionFailpoint;
3482
+ yield* initializeSqliteJournal();
3483
+ yield* sql`
3484
+ INSERT INTO effect_agent_subscription_sequences (
3485
+ tenant_id, source_address, sequence, event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
3486
+ ) VALUES (${partition.tenantId}, ${partition.address}, 0, '', '', 0) ON CONFLICT DO NOTHING
3487
+ `.pipe(Effect.mapError(() => unavailable("initialize subscription partition")));
3488
+ const transact = (effect) => sql.withTransaction(effect).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable("transaction"))));
3489
+ const query = (effect, code) => effect.pipe(Effect.mapError(() => unavailable(code)));
3490
+ const requirePartition = (candidate, code) => sameSourcePartition(candidate, partition) ? Effect.void : Effect.fail(error("validation", code));
3491
+ const requireKey = Effect.fn("SqliteSubscriptionStore.requireKey")(function* (input, code) {
3492
+ const key = yield* validate(SubscriptionKey, input, code);
3493
+ yield* requirePartition(key.partition, code);
3494
+ return key;
3495
+ });
3496
+ const readRegistration = Effect.fn("SqliteSubscriptionStore.readRegistration")(function* (key, code) {
3497
+ const rows = yield* query(sql`
3498
+ SELECT owner_id, subscription_id, ordinal, source_name, source_version, matching_key, state,
3499
+ expires_at_millis, recovery_at_millis, record_json FROM effect_agent_subscriptions
3500
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3501
+ AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId}
3502
+ `, code);
3503
+ const decodedRows = yield* decodeRows(RegistrationRow, rows, code);
3504
+ if (decodedRows.length > 1) return yield* corrupt(code);
3505
+ const row = decodedRows[0];
3506
+ if (row === void 0) return null;
3507
+ const record = yield* decode(SubscriptionRecord, row.record_json, code);
3508
+ 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`);
3509
+ return record;
3510
+ });
3511
+ const readEvent = Effect.fn("SqliteSubscriptionStore.readEvent")(function* (eventId, code) {
3512
+ const rows = yield* query(sql`
3513
+ SELECT event_id, source_name, source_version, matching_key, payload_digest, cutoff, cursor,
3514
+ routing_complete, next_attempt_at_millis, record_json FROM effect_agent_subscription_events
3515
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${eventId}
3516
+ `, code);
3517
+ const decodedRows = yield* decodeRows(EventRow, rows, code);
3518
+ if (decodedRows.length > 1) return yield* corrupt(code);
3519
+ const row = decodedRows[0];
3520
+ if (row === void 0) return null;
3521
+ const event = yield* decode(AcceptedEvent, row.record_json, code);
3522
+ 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`);
3523
+ return event;
3524
+ });
3525
+ const readDelivery = Effect.fn("SqliteSubscriptionStore.readDelivery")(function* (key, code) {
3526
+ const rows = yield* query(sql`
3527
+ SELECT owner_id, subscription_id, event_id, delivery_key, state, next_attempt_at_millis, record_json
3528
+ FROM effect_agent_subscription_deliveries
3529
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3530
+ AND owner_id=${key.subscription.ownerId} AND subscription_id=${key.subscription.subscriptionId}
3531
+ AND event_id=${key.eventId}
3532
+ `, code);
3533
+ const decodedRows = yield* decodeRows(DeliveryRow, rows, code);
3534
+ if (decodedRows.length > 1) return yield* corrupt(code);
3535
+ const row = decodedRows[0];
3536
+ if (row === void 0) return null;
3537
+ const delivery = yield* decode(SubscriptionDelivery, row.record_json, code);
3538
+ 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`);
3539
+ return delivery;
3540
+ });
3541
+ const count = Effect.fn("SqliteSubscriptionStore.count")(function* (statement, code) {
3542
+ const rows = yield* query(statement, code);
3543
+ const decoded = yield* decodeRows(CountRow, rows, code);
3544
+ if (decoded.length !== 1) return yield* corrupt(code);
3545
+ return decoded[0].count;
3546
+ });
3547
+ const nextSequence = Effect.fn("SqliteSubscriptionStore.nextSequence")(function* () {
3548
+ const rows = yield* query(sql`
3549
+ UPDATE effect_agent_subscription_sequences SET sequence=sequence+1
3550
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3551
+ RETURNING sequence
3552
+ `, "advance subscription sequence");
3553
+ const decoded = yield* decodeRows(SequenceRow, rows, "advance subscription sequence");
3554
+ if (decoded.length !== 1) return yield* corrupt("subscription sequence");
3555
+ return decoded[0].sequence;
3556
+ });
3557
+ const writeRegistration = Effect.fn("SqliteSubscriptionStore.writeRegistration")(function* (record) {
3558
+ const json = yield* encode(SubscriptionRecord, record, "encode subscription");
3559
+ yield* query(sql`
3560
+ UPDATE effect_agent_subscriptions SET state=${record.state}, expires_at_millis=${record.configuration.expiresAtMillis},
3561
+ recovery_at_millis=${record.recovery?.nextAttemptAtMillis ?? null}, record_json=${json}
3562
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3563
+ AND owner_id=${record.key.ownerId} AND subscription_id=${record.key.subscriptionId}
3564
+ `, "write subscription");
3565
+ });
3566
+ const writeEvent = Effect.fn("SqliteSubscriptionStore.writeEvent")(function* (event) {
3567
+ const json = yield* encode(AcceptedEvent, event, "encode event");
3568
+ yield* query(sql`
3569
+ UPDATE effect_agent_subscription_events SET cursor=${event.cursor}, routing_complete=${event.routingComplete ? 1 : 0},
3570
+ next_attempt_at_millis=${event.nextAttemptAtMillis}, record_json=${json}
3571
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${event.eventId}
3572
+ `, "write event");
3573
+ });
3574
+ const writeDelivery = Effect.fn("SqliteSubscriptionStore.writeDelivery")(function* (delivery) {
3575
+ const json = yield* encode(SubscriptionDelivery, delivery, "encode delivery");
3576
+ yield* query(sql`
3577
+ UPDATE effect_agent_subscription_deliveries SET state=${delivery.state},
3578
+ next_attempt_at_millis=${delivery.retry.nextAttemptAtMillis}, record_json=${json}
3579
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3580
+ AND owner_id=${delivery.key.subscription.ownerId} AND subscription_id=${delivery.key.subscription.subscriptionId}
3581
+ AND event_id=${delivery.key.eventId}
3582
+ `, "write delivery");
3583
+ });
3584
+ const register = Effect.fn("SqliteSubscriptionStore.register")(function* (input, inputLimits) {
3585
+ const record = yield* validate(SubscriptionRecord, input, "register-record");
3586
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
3587
+ yield* requirePartition(record.key.partition, "register-partition");
3588
+ const result = yield* transact(Effect.gen(function* () {
3589
+ const existing = yield* readRegistration(record.key, "register-existing");
3590
+ if (existing !== null) {
3591
+ if (existing.creationFingerprint !== record.creationFingerprint) return yield* error("conflict", "registration-identity");
3592
+ return {
3593
+ value: existing,
3594
+ changed: false
3595
+ };
3596
+ }
3597
+ if (bytes(record.configuration.context) > limits.maxContextBytes) return yield* error("capacity", "context-bytes");
3598
+ if (bytes(record.configuration.parameters) > limits.maxPayloadBytes) return yield* error("capacity", "parameters-bytes");
3599
+ if (record.configuration.expiresAtMillis - record.createdAtMillis > limits.maxLifetimeMillis) return yield* error("capacity", "lifetime");
3600
+ 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");
3601
+ 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");
3602
+ const assigned = {
3603
+ ...record,
3604
+ ordinal: yield* nextSequence()
3605
+ };
3606
+ const json = yield* encode(SubscriptionRecord, assigned, "encode registration");
3607
+ yield* failpoint.hit("subscription:register:before");
3608
+ yield* query(sql`
3609
+ INSERT INTO effect_agent_subscriptions (tenant_id, source_address, owner_id, subscription_id, ordinal,
3610
+ source_name, source_version, matching_key, state, expires_at_millis, recovery_at_millis, record_json)
3611
+ VALUES (${partition.tenantId}, ${partition.address}, ${assigned.key.ownerId}, ${assigned.key.subscriptionId}, ${assigned.ordinal},
3612
+ ${assigned.configuration.source.name}, ${assigned.configuration.source.version}, ${assigned.configuration.matchingKey}, ${assigned.state},
3613
+ ${assigned.configuration.expiresAtMillis}, ${assigned.recovery?.nextAttemptAtMillis ?? null}, ${json})
3614
+ `, "insert registration");
3615
+ return {
3616
+ value: assigned,
3617
+ changed: true
3618
+ };
3619
+ }));
3620
+ if (result.changed) yield* failpoint.hit("subscription:register:after");
3621
+ return result.value;
3622
+ });
3623
+ const get = Effect.fn("SqliteSubscriptionStore.get")(function* (input) {
3624
+ return yield* readRegistration(yield* requireKey(input, "get-key"), "get subscription");
3625
+ });
3626
+ const list = Effect.fn("SqliteSubscriptionStore.list")(function* (ownerId, after, limit) {
3627
+ const rows = yield* query(sql`
3628
+ SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3629
+ AND owner_id=${ownerId} AND ordinal>${after} ORDER BY ordinal LIMIT ${limit}
3630
+ `, "list subscriptions");
3631
+ const decoded = yield* decodeRows(Schema.Struct({
3632
+ owner_id: Schema.String,
3633
+ subscription_id: Schema.String,
3634
+ ordinal: Schema.Natural
3635
+ }), rows, "list subscriptions");
3636
+ return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.listRecord")(function* (row) {
3637
+ const record = yield* readRegistration({
3638
+ partition,
3639
+ ownerId: row.owner_id,
3640
+ subscriptionId: row.subscription_id
3641
+ }, "list subscription");
3642
+ if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("list subscription projection");
3643
+ return record;
3644
+ }));
3645
+ });
3646
+ const cancel = Effect.fn("SqliteSubscriptionStore.cancel")(function* (input) {
3647
+ const key = yield* requireKey(input, "cancel-key");
3648
+ const result = yield* transact(Effect.gen(function* () {
3649
+ const current = yield* readRegistration(key, "cancel subscription");
3650
+ if (current === null) return yield* error("not-found", "subscription");
3651
+ if (current.state === "cancelled") return {
3652
+ value: current,
3653
+ changed: false
3654
+ };
3655
+ const updated = {
3656
+ ...current,
3657
+ state: "cancelled",
3658
+ recovery: null
3659
+ };
3660
+ yield* failpoint.hit("subscription:cancel:before");
3661
+ yield* writeRegistration(updated);
3662
+ return {
3663
+ value: updated,
3664
+ changed: true
3665
+ };
3666
+ }));
3667
+ if (result.changed) yield* failpoint.hit("subscription:cancel:after");
3668
+ return result.value;
3669
+ });
3670
+ const accept = Effect.fn("SqliteSubscriptionStore.accept")(function* (input, inputLimits) {
3671
+ const event = yield* validate(AcceptedEvent, input, "accept-event");
3672
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "accept-limits");
3673
+ yield* requirePartition(event.partition, "accept-partition");
3674
+ const result = yield* transact(Effect.gen(function* () {
3675
+ const existing = yield* readEvent(event.eventId, "accept event");
3676
+ if (existing !== null) {
3677
+ if (!sameAcceptedEventIdentity(existing, event)) return yield* error("conflict", "event-identity");
3678
+ return {
3679
+ value: existing,
3680
+ changed: false
3681
+ };
3682
+ }
3683
+ if (bytes(event.payload) > limits.maxPayloadBytes) return yield* error("capacity", "payload-bytes");
3684
+ 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");
3685
+ const accepted = {
3686
+ ...event,
3687
+ cutoff: yield* nextSequence(),
3688
+ cursor: 0,
3689
+ routingComplete: false,
3690
+ routingFailure: null
3691
+ };
3692
+ const json = yield* encode(AcceptedEvent, accepted, "encode accepted event");
3693
+ yield* failpoint.hit("subscription:accept:before");
3694
+ yield* query(sql`
3695
+ INSERT INTO effect_agent_subscription_events (tenant_id, source_address, event_id, source_name, source_version,
3696
+ matching_key, payload_digest, cutoff, cursor, routing_complete, next_attempt_at_millis, record_json)
3697
+ VALUES (${partition.tenantId}, ${partition.address}, ${accepted.eventId}, ${accepted.source.name}, ${accepted.source.version},
3698
+ ${accepted.matchingKey}, ${accepted.payloadDigest}, ${accepted.cutoff}, ${accepted.cursor}, 0, ${accepted.nextAttemptAtMillis}, ${json})
3699
+ `, "insert event");
3700
+ return {
3701
+ value: accepted,
3702
+ changed: true
3703
+ };
3704
+ }));
3705
+ if (result.changed) yield* failpoint.hit("subscription:accept:after");
3706
+ return result.value;
3707
+ });
3708
+ const event = Effect.fn("SqliteSubscriptionStore.event")((eventId) => readEvent(eventId, "get event"));
3709
+ const pendingEvents = Effect.fn("SqliteSubscriptionStore.pendingEvents")(function* (nowMillis, after, limit) {
3710
+ const rows = yield* query(sql`
3711
+ SELECT event_id FROM effect_agent_subscription_events WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3712
+ AND routing_complete=0 AND next_attempt_at_millis<=${nowMillis} AND event_id>${after} ORDER BY event_id LIMIT ${limit}
3713
+ `, "pending events");
3714
+ return yield* decodeRows(Schema.Struct({ event_id: Schema.String }), rows, "pending event keys").pipe(Effect.map((items) => items.map((item) => item.event_id)));
3715
+ });
3716
+ const candidates = Effect.fn("SqliteSubscriptionStore.candidates")(function* (input, limit) {
3717
+ const supplied = yield* validate(AcceptedEvent, input, "candidates-event");
3718
+ yield* requirePartition(supplied.partition, "candidates-partition");
3719
+ const stored = yield* readEvent(supplied.eventId, "candidates event");
3720
+ if (stored === null) return yield* error("not-found", "event");
3721
+ if (!sameAcceptedEventIdentity(stored, supplied)) return yield* error("conflict", "event");
3722
+ const rows = yield* query(sql`
3723
+ SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3724
+ AND source_name=${stored.source.name} AND source_version=${stored.source.version} AND matching_key=${stored.matchingKey}
3725
+ AND ordinal>${stored.cursor} AND ordinal<=${stored.cutoff} ORDER BY ordinal LIMIT ${limit}
3726
+ `, "subscription candidates");
3727
+ const decoded = yield* decodeRows(Schema.Struct({
3728
+ owner_id: Schema.String,
3729
+ subscription_id: Schema.String,
3730
+ ordinal: Schema.Natural
3731
+ }), rows, "subscription candidates");
3732
+ return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.candidateRecord")(function* (row) {
3733
+ const record = yield* readRegistration({
3734
+ partition,
3735
+ ownerId: row.owner_id,
3736
+ subscriptionId: row.subscription_id
3737
+ }, "subscription candidate");
3738
+ if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("subscription candidate projection");
3739
+ return record;
3740
+ }));
3741
+ });
3742
+ const insertDelivery = Effect.fn("SqliteSubscriptionStore.insertDelivery")(function* (delivery) {
3743
+ const json = yield* encode(SubscriptionDelivery, delivery, "encode selected delivery");
3744
+ yield* query(sql`
3745
+ INSERT INTO effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, event_id,
3746
+ delivery_key, state, next_attempt_at_millis, record_json)
3747
+ VALUES (${partition.tenantId}, ${partition.address}, ${delivery.key.subscription.ownerId}, ${delivery.key.subscription.subscriptionId},
3748
+ ${delivery.key.eventId}, ${subscriptionDeliveryKeyString(delivery.key)}, ${delivery.state}, ${delivery.retry.nextAttemptAtMillis}, ${json})
3749
+ `, "insert delivery");
3750
+ });
3751
+ const select = Effect.fn("SqliteSubscriptionStore.select")(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
3752
+ const supplied = yield* validate(AcceptedEvent, inputEvent, "select-event");
3753
+ const deliveries = yield* validate(Schema.Array(SubscriptionDelivery), inputDeliveries, "select-deliveries");
3754
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
3755
+ yield* requirePartition(supplied.partition, "select-partition");
3756
+ for (const candidate of deliveries) yield* requirePartition(candidate.key.subscription.partition, "select-delivery-partition");
3757
+ if (yield* transact(Effect.gen(function* () {
3758
+ const accepted = yield* readEvent(supplied.eventId, "select event");
3759
+ if (accepted === null) return yield* error("not-found", "event");
3760
+ if (!sameAcceptedEventIdentity(accepted, supplied) || accepted.cursor !== supplied.cursor) return yield* error("conflict", "event-cursor");
3761
+ if (accepted.routingComplete) return false;
3762
+ if (!Number.isSafeInteger(cursor) || cursor < accepted.cursor || cursor > accepted.cutoff) return yield* error("validation", "cursor");
3763
+ yield* failpoint.hit("subscription:select:before");
3764
+ const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
3765
+ const additions = [];
3766
+ for (const delivery of deliveries) {
3767
+ const record = yield* readRegistration(delivery.key.subscription, "select registration");
3768
+ if (record === null) return yield* error("not-found", "subscription");
3769
+ 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");
3770
+ const existing = yield* readDelivery(delivery.key, "select existing delivery");
3771
+ if (existing !== null) {
3772
+ if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
3773
+ continue;
3774
+ }
3775
+ if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, false)) continue;
3776
+ additions.push({
3777
+ delivery,
3778
+ record
3779
+ });
3780
+ }
3781
+ 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");
3782
+ 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");
3783
+ for (const addition of additions) {
3784
+ yield* insertDelivery(addition.delivery);
3785
+ if (addition.record.configuration.mode === "once") yield* writeRegistration({
3786
+ ...addition.record,
3787
+ state: "consumed",
3788
+ recovery: null
3789
+ });
3790
+ }
3791
+ yield* writeEvent({
3792
+ ...accepted,
3793
+ cursor,
3794
+ routingComplete: complete,
3795
+ routingFailure: null
3796
+ });
3797
+ return true;
3798
+ }))) yield* failpoint.hit("subscription:select:after");
3799
+ });
3800
+ const catchUp = Effect.fn("SqliteSubscriptionStore.catchUp")(function* (inputEvent, inputDelivery, nowMillis, inputLimits) {
3801
+ const supplied = yield* validate(AcceptedEvent, inputEvent, "catch-up-event");
3802
+ const delivery = yield* validate(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
3803
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "catch-up-limits");
3804
+ yield* requirePartition(supplied.partition, "catch-up-partition");
3805
+ yield* requirePartition(delivery.key.subscription.partition, "catch-up-delivery-partition");
3806
+ if (yield* transact(Effect.gen(function* () {
3807
+ const accepted = yield* readEvent(supplied.eventId, "catch-up event");
3808
+ const record = yield* readRegistration(delivery.key.subscription, "catch-up subscription");
3809
+ if (accepted === null || record === null) return yield* error("not-found", accepted === null ? "event" : "subscription");
3810
+ 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");
3811
+ const existing = yield* readDelivery(delivery.key, "catch-up existing delivery");
3812
+ if (existing !== null) {
3813
+ if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
3814
+ return false;
3815
+ }
3816
+ yield* failpoint.hit("subscription:catch-up:before");
3817
+ if (!subscriptionCanSelect(record, accepted, Math.max(nowMillis, yield* Clock.currentTimeMillis), true)) return yield* error("conflict", "catch-up-eligibility");
3818
+ 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");
3819
+ 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");
3820
+ yield* insertDelivery(delivery);
3821
+ yield* writeRegistration({
3822
+ ...record,
3823
+ state: "consumed",
3824
+ recovery: null
3825
+ });
3826
+ return true;
3827
+ }))) yield* failpoint.hit("subscription:catch-up:after");
3828
+ });
3829
+ const deferEvent = Effect.fn("SqliteSubscriptionStore.deferEvent")(function* (eventId, nextAttemptAtMillis, code) {
3830
+ const routingFailure = code === void 0 ? "routing-failed" : yield* validate(SubscriptionName, code, "routing-failure");
3831
+ yield* transact(Effect.gen(function* () {
3832
+ const accepted = yield* readEvent(eventId, "defer event");
3833
+ if (accepted === null) return yield* error("not-found", "event");
3834
+ yield* failpoint.hit("subscription:defer-event:before");
3835
+ yield* writeEvent({
3836
+ ...accepted,
3837
+ nextAttemptAtMillis,
3838
+ routingFailure
3839
+ });
3840
+ }));
3841
+ yield* failpoint.hit("subscription:defer-event:after");
3842
+ });
3843
+ const delivery = Effect.fn("SqliteSubscriptionStore.delivery")(function* (input) {
3844
+ const key = yield* validate(SubscriptionDeliveryKey, input, "delivery-key");
3845
+ yield* requirePartition(key.subscription.partition, "delivery-partition");
3846
+ return yield* readDelivery(key, "get delivery");
3847
+ });
3848
+ const pendingDeliveries = Effect.fn("SqliteSubscriptionStore.pendingDeliveries")(function* (nowMillis, after, limit) {
3849
+ const rows = yield* query(sql`
3850
+ SELECT owner_id, subscription_id, event_id FROM effect_agent_subscription_deliveries
3851
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
3852
+ AND next_attempt_at_millis<=${nowMillis} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
3853
+ `, "pending deliveries");
3854
+ const rowSchema = Schema.Struct({
3855
+ owner_id: Schema.String,
3856
+ subscription_id: Schema.String,
3857
+ event_id: Schema.String
3858
+ });
3859
+ return yield* decodeRows(rowSchema, rows, "pending delivery keys").pipe(Effect.map((items) => items.map((item) => ({
3860
+ subscription: {
3861
+ partition,
3862
+ ownerId: item.owner_id,
3863
+ subscriptionId: item.subscription_id
3864
+ },
3865
+ eventId: item.event_id
3866
+ }))));
3867
+ });
3868
+ const listDeliveries = Effect.fn("SqliteSubscriptionStore.listDeliveries")(function* (input, after, limit) {
3869
+ const key = yield* requireKey(input, "list-deliveries-key");
3870
+ const rows = yield* query(sql`
3871
+ SELECT record_json FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3872
+ AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
3873
+ `, "list deliveries");
3874
+ const decoded = yield* decodeRows(JsonRow, rows, "list deliveries");
3875
+ return yield* Effect.forEach(decoded, (row) => decode(SubscriptionDelivery, row.record_json, "list delivery"));
3876
+ });
3877
+ const changeDelivery = Effect.fn("SqliteSubscriptionStore.changeDelivery")(function* (inputKey, inputDeliveryId, inputChange) {
3878
+ const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
3879
+ const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
3880
+ const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
3881
+ yield* requirePartition(key.subscription.partition, "change-delivery-partition");
3882
+ const result = yield* transact(Effect.gen(function* () {
3883
+ const existing = yield* readDelivery(key, "change delivery");
3884
+ const record = yield* readRegistration(key.subscription, "change delivery subscription");
3885
+ if (existing === null || record === null) return yield* error("not-found", existing === null ? "delivery" : "subscription");
3886
+ yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
3887
+ const effectiveChange = change._tag === "Prepare" ? {
3888
+ ...change,
3889
+ nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis)
3890
+ } : change;
3891
+ const transition = applySubscriptionDeliveryChange(existing, record, deliveryId, effectiveChange);
3892
+ if (Result.isFailure(transition)) return yield* transition.failure;
3893
+ if (transition.success === existing) return {
3894
+ value: existing,
3895
+ changed: false
3896
+ };
3897
+ yield* writeDelivery(transition.success);
3898
+ return {
3899
+ value: transition.success,
3900
+ changed: true
3901
+ };
3902
+ }));
3903
+ if (result.changed) yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
3904
+ return result.value;
3905
+ });
3906
+ const recovering = Effect.fn("SqliteSubscriptionStore.recovering")(function* (nowMillis, after, limit) {
3907
+ const rows = yield* query(sql`
3908
+ SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions
3909
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active'
3910
+ AND recovery_at_millis IS NOT NULL AND recovery_at_millis<=${nowMillis} AND ordinal>${after}
3911
+ ORDER BY ordinal LIMIT ${limit}
3912
+ `, "recovering subscriptions");
3913
+ const rowSchema = Schema.Struct({
3914
+ owner_id: Schema.String,
3915
+ subscription_id: Schema.String,
3916
+ ordinal: Schema.Natural
3917
+ });
3918
+ return yield* decodeRows(rowSchema, rows, "recovering subscription keys").pipe(Effect.map((items) => items.map((item) => ({
3919
+ key: {
3920
+ partition,
3921
+ ownerId: item.owner_id,
3922
+ subscriptionId: item.subscription_id
3923
+ },
3924
+ ordinal: item.ordinal
3925
+ }))));
3926
+ });
3927
+ const deferRecovery = Effect.fn("SqliteSubscriptionStore.deferRecovery")(function* (input, recovery) {
3928
+ const key = yield* requireKey(input, "defer-recovery-key");
3929
+ yield* transact(Effect.gen(function* () {
3930
+ const record = yield* readRegistration(key, "defer recovery");
3931
+ if (record === null) return yield* error("not-found", "subscription");
3932
+ yield* failpoint.hit("subscription:defer-recovery:before");
3933
+ yield* writeRegistration({
3934
+ ...record,
3935
+ recovery: record.state === "active" ? recovery : null
3936
+ });
3937
+ }));
3938
+ yield* failpoint.hit("subscription:defer-recovery:after");
3939
+ });
3940
+ const readScanCursors = Effect.gen(function* () {
3941
+ const rows = yield* query(sql`
3942
+ SELECT event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
3943
+ FROM effect_agent_subscription_sequences
3944
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3945
+ `, "read subscription scan cursors");
3946
+ const decoded = yield* decodeRows(ScanRow, rows, "read subscription scan cursors");
3947
+ if (decoded.length !== 1) return yield* corrupt("subscription scan cursors");
3948
+ return {
3949
+ events: decoded[0].event_scan_cursor,
3950
+ deliveries: decoded[0].delivery_scan_cursor,
3951
+ recovery: decoded[0].recovery_scan_cursor
3952
+ };
3953
+ });
3954
+ const advanceScanCursors = Effect.fn("SqliteSubscriptionStore.advanceScanCursors")(function* (input) {
3955
+ const cursors = yield* validate(SubscriptionScanCursors, input, "scan-cursors");
3956
+ yield* transact(Effect.gen(function* () {
3957
+ yield* failpoint.hit("subscription:advance-scan-cursors:before");
3958
+ yield* query(sql`
3959
+ UPDATE effect_agent_subscription_sequences
3960
+ SET event_scan_cursor=${cursors.events}, delivery_scan_cursor=${cursors.deliveries}, recovery_scan_cursor=${cursors.recovery}
3961
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
3962
+ `, "advance subscription scan cursors");
3963
+ }));
3964
+ yield* failpoint.hit("subscription:advance-scan-cursors:after");
3965
+ });
3966
+ const indexedDeadline = query(sql`
3967
+ SELECT MIN(deadline) AS deadline FROM (
3968
+ SELECT next_attempt_at_millis AS deadline FROM effect_agent_subscription_events
3969
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND routing_complete=0
3970
+ UNION ALL SELECT next_attempt_at_millis FROM effect_agent_subscription_deliveries
3971
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
3972
+ UNION ALL SELECT recovery_at_millis FROM effect_agent_subscriptions
3973
+ WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active' AND recovery_at_millis IS NOT NULL
3974
+ )
3975
+ `, "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"))));
3976
+ const nextDeadline = Effect.gen(function* () {
3977
+ const cursors = yield* readScanCursors;
3978
+ if (cursors.events !== "" || cursors.deliveries !== "" || cursors.recovery !== 0) return 0;
3979
+ return yield* indexedDeadline;
3980
+ });
3981
+ return SubscriptionStore.of({
3982
+ partition,
3983
+ register,
3984
+ get,
3985
+ list,
3986
+ cancel,
3987
+ accept,
3988
+ event,
3989
+ pendingEvents,
3990
+ candidates,
3991
+ select,
3992
+ catchUp,
3993
+ deferEvent,
3994
+ delivery,
3995
+ pendingDeliveries,
3996
+ listDeliveries,
3997
+ changeDelivery,
3998
+ recovering,
3999
+ deferRecovery,
4000
+ readScanCursors,
4001
+ advanceScanCursors,
4002
+ nextDeadline
4003
+ });
4004
+ });
4005
+ const subscriptionStoreLayer = (partition) => Layer.effect(SubscriptionStore, makeSubscriptionStore(partition));
4006
+ //#endregion
4007
+ export { CurrentSqliteStorageVersion, SqliteAppendConflict, SqliteCheckpointConflict, SqliteFenceRejected, SqliteLedgerError, SqliteStorageCompatibilityError, SqliteStorageConfig, SqliteStorageConfigValue, SqliteStorageCorruptionError, SqliteStorageError, SqliteStorageFailpoint, SqliteStorageFailpointError, SqliteStorageFailpointLocation, SqliteWriteContention, layer, ledgerLayer, observationOffsetAt, scheduleStoreLayer, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer, subscriptionStoreLayer, threadStoreLayer };
3365
4008
 
3366
4009
  //# sourceMappingURL=index.mjs.map