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

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,5 +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, Settlement, SettlementConflict, SettlementFinalization, SettlementOutcome, SettlementReservation, SettlementReservationSnapshot, SubmissionLedger, SubmissionLookup, SubmissionLookupByKey, SubmissionSnapshot, SubmissionState, SuspendRequest, SuspensionReason, SuspensionSnapshot, UnknownResolution, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, digestCanonicalBatch, settlementFailureFromRecord, submissionAbortRecordId } from "@effect-agent/session";
2
- import { Clock, Context, Crypto, DateTime, Duration, Effect, Exit, Layer, Option, Ref, Schema, Stream } from "effect";
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";
2
+ import { Clock, Context, Crypto, DateTime, Duration, Effect, Exit, Layer, Option, Ref, Result, Schema, Stream } from "effect";
3
3
  import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node";
4
4
  import * as SqlClient from "effect/unstable/sql/SqlClient";
5
5
  import { NodeCrypto } from "@effect/platform-node";
@@ -130,7 +130,7 @@ var SqliteStorageFailpointError = class extends Schema.TaggedError()("SqliteStor
130
130
  };
131
131
  //#endregion
132
132
  //#region src/migrations.ts
133
- const CurrentSqliteStorageVersion = 4;
133
+ const CurrentSqliteStorageVersion = 5;
134
134
  const sqliteMigrations = SqliteMigrator.fromRecord({
135
135
  "1_current_persistent_conversation_foundation": Effect.gen(function* () {
136
136
  const sql = yield* SqlClient.SqlClient;
@@ -360,9 +360,81 @@ const sqliteMigrations = SqliteMigrator.fromRecord({
360
360
  )
361
361
  `.withoutTransform;
362
362
  yield* sql`PRAGMA user_version = 4`.withoutTransform;
363
+ }),
364
+ "5_durable_schedules": Effect.gen(function* () {
365
+ const sql = yield* SqlClient.SqlClient;
366
+ yield* sql`
367
+ CREATE TABLE effect_agent_schedules (
368
+ tenant_id TEXT NOT NULL,
369
+ owner_id TEXT NOT NULL,
370
+ schedule_id TEXT NOT NULL,
371
+ deadline_at_millis INTEGER,
372
+ record_json TEXT NOT NULL,
373
+ PRIMARY KEY (tenant_id, owner_id, schedule_id)
374
+ )
375
+ `.withoutTransform;
376
+ yield* sql`
377
+ CREATE INDEX effect_agent_schedules_deadline
378
+ ON effect_agent_schedules (deadline_at_millis, tenant_id, owner_id, schedule_id)
379
+ WHERE deadline_at_millis IS NOT NULL
380
+ `.withoutTransform;
381
+ yield* sql`
382
+ CREATE INDEX effect_agent_schedules_owner_deadline
383
+ ON effect_agent_schedules (tenant_id, owner_id, deadline_at_millis, schedule_id)
384
+ WHERE deadline_at_millis IS NOT NULL
385
+ `.withoutTransform;
386
+ yield* sql`PRAGMA user_version = 5`.withoutTransform;
363
387
  })
364
388
  });
365
389
  //#endregion
390
+ //#region src/sqlite-storage-config.ts
391
+ const ObservationPollInterval = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
392
+ const BusyTimeoutMillis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
393
+ const OwnershipLeaseMillis = Schema.Int.check(Schema.isGreaterThan(0));
394
+ /**
395
+ * Validated construction configuration consumed by the SQLite storage Layer. The database
396
+ * identity itself belongs to the SqlClient Layer; duplicating it here could silently diverge
397
+ * from the connection actually in use.
398
+ */
399
+ var SqliteStorageConfigValue = class extends Schema.Class("@effect-agent/storage-sqlite/SqliteStorageConfigValue")({
400
+ observationPollInterval: ObservationPollInterval,
401
+ /** Bounded SQLITE_BUSY retry window for write-lock acquisition, in milliseconds. */
402
+ busyTimeout: BusyTimeoutMillis,
403
+ /**
404
+ * Submission ownership lease duration in milliseconds (D5). The lease is a liveness hint
405
+ * that makes an abandoned claim reclaimable; correctness never depends on it because every
406
+ * canonical append is fenced by producer epoch. Convenience layers default this to
407
+ * `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/session`.
408
+ */
409
+ ownershipLeaseDuration: OwnershipLeaseMillis,
410
+ /**
411
+ * Re-verify every stored payload and digest chain while opening the store. Per-operation
412
+ * Schema decoding and the digest chain already fail clearly on corrupt rows, so the full
413
+ * scan is an explicit opt-in integrity audit rather than a startup requirement.
414
+ */
415
+ verifyOnOpen: Schema.Boolean
416
+ }) {};
417
+ /** Explicit SQLite storage configuration authority. */
418
+ var SqliteStorageConfig = class extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageConfig") {};
419
+ //#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
366
438
  //#region src/sqlite-journal.ts
367
439
  const BoundedStoredText$1 = Schema.String.check(Schema.isMaxLength(16 * 1024 * 1024));
368
440
  const BoundedIdentifier$1 = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
@@ -441,40 +513,42 @@ var RawConversationExport = class extends Schema.Class("@effect-agent/storage-sq
441
513
  conversation: ConversationRow,
442
514
  records: Schema.Array(RecordRow)
443
515
  }) {};
444
- const noFailpoint$1 = () => Effect.void;
445
516
  const storageError = (operation) => (error) => SqliteStorageError.make({
446
517
  cause: error,
447
518
  operation,
448
519
  message: error.message
449
520
  });
450
521
  /** Decode raw SQLite rows against a Schema, reporting failures as typed corruption. */
451
- const decodeRows = Effect.fn("SqliteJournal.decodeRows")((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
522
+ const decodeRows$1 = Effect.fn("SqliteJournal.decodeRows")((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
452
523
  table,
453
524
  rowKey,
454
525
  message: String(error)
455
526
  }))));
456
527
  /** Decode exactly one raw SQLite row against a Schema, reporting failures as typed corruption. */
457
- const decodeSingleRow = Effect.fn("SqliteJournal.decodeSingleRow")((schema, table, rowKey, rows) => decodeRows(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(SqliteStorageCorruptionError.make({
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({
458
529
  table,
459
530
  rowKey,
460
531
  message: `Expected exactly one row but found ${decoded.length}.`
461
532
  })))));
462
- const ensureCurrentStorage = Effect.fn("SqliteJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint$1, busyTimeoutMillis = 5e3) {
533
+ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function* () {
534
+ const sql = yield* SqlClient.SqlClient;
535
+ const { hit: failpoint } = yield* SqliteStorageFailpoint;
536
+ const { busyTimeout } = yield* SqliteStorageConfig;
463
537
  yield* sql`PRAGMA foreign_keys = ON`.pipe(Effect.mapError(storageError("enable foreign keys")));
464
- yield* sql.unsafe(`PRAGMA busy_timeout = ${busyTimeoutMillis}`).pipe(Effect.mapError(storageError("configure busy timeout")));
538
+ yield* sql.unsafe(`PRAGMA busy_timeout = ${busyTimeout}`).pipe(Effect.mapError(storageError("configure busy timeout")));
465
539
  const journalModeRows = yield* sql`PRAGMA journal_mode`.pipe(Effect.mapError(storageError("read journal mode")));
466
540
  const journalMode = yield* decodeSingleRow(Schema.Array(SqliteJournalModeRow), "pragma_journal_mode", "singleton", journalModeRows);
467
541
  if (journalMode.journal_mode.toLowerCase() !== "wal") return yield* SqliteStorageCompatibilityError.make({
468
542
  actualVersion: 0,
469
- supportedVersion: 4,
543
+ supportedVersion: 5,
470
544
  message: `SQLite WAL mode is required; the database reported ${journalMode.journal_mode}.`
471
545
  });
472
546
  const versionRows = yield* sql`PRAGMA user_version`.pipe(Effect.mapError(storageError("read storage version")));
473
547
  const version = yield* decodeSingleRow(Schema.Array(SqliteVersionRow), "pragma_user_version", "singleton", versionRows);
474
- if (version.user_version !== 0 && version.user_version !== 4) return yield* SqliteStorageCompatibilityError.make({
548
+ if (version.user_version !== 0 && version.user_version !== 5) return yield* SqliteStorageCompatibilityError.make({
475
549
  actualVersion: version.user_version,
476
- supportedVersion: 4,
477
- message: `The SQLite file uses private-development storage version ${version.user_version}; this build supports exactly version 4. Reset the database file explicitly; automatic stored-data migrations are not provided during private development.`
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.`
478
552
  });
479
553
  if (version.user_version === 0) {
480
554
  const existingRows = yield* sql`
@@ -484,12 +558,12 @@ const ensureCurrentStorage = Effect.fn("SqliteJournal.ensureCurrentStorage")(fun
484
558
  AND name LIKE 'effect_agent_%'
485
559
  ORDER BY name
486
560
  `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
487
- if ((yield* decodeRows(Schema.Array(SqliteNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* SqliteStorageCompatibilityError.make({
561
+ if ((yield* decodeRows$1(Schema.Array(SqliteNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* SqliteStorageCompatibilityError.make({
488
562
  actualVersion: 0,
489
- supportedVersion: 4,
563
+ supportedVersion: 5,
490
564
  message: "The SQLite file contains unversioned Effect Agent tables. Reset it explicitly; refusing to mutate ambiguous stored data."
491
565
  });
492
- yield* SqliteMigrator.run({ loader: sqliteMigrations }).pipe(Effect.provideService(SqlClient.SqlClient, sql), Effect.mapError((error) => SqliteStorageError.make({
566
+ yield* SqliteMigrator.run({ loader: sqliteMigrations }).pipe(Effect.mapError((error) => SqliteStorageError.make({
493
567
  cause: error,
494
568
  operation: "initialize current storage",
495
569
  message: error.message
@@ -510,18 +584,16 @@ const ensureCurrentStorage = Effect.fn("SqliteJournal.ensureCurrentStorage")(fun
510
584
  'effect_agent_settlement_reservations',
511
585
  'effect_agent_abort_intents',
512
586
  'effect_agent_approval_decisions',
513
- 'effect_agent_unknown_resolutions'
587
+ 'effect_agent_unknown_resolutions',
588
+ 'effect_agent_schedules'
514
589
  )
515
590
  ORDER BY name
516
591
  `.pipe(Effect.mapError(storageError("verify storage tables")));
517
- if ((yield* decodeRows(Schema.Array(SqliteNameRow), "sqlite_master", "required_tables", requiredRows)).length !== 11) return yield* SqliteStorageCompatibilityError.make({
518
- actualVersion: 4,
519
- supportedVersion: 4,
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,
520
595
  message: "The SQLite file claims the current format but is missing required tables. Reset the corrupt private-development data."
521
596
  });
522
- return makeJournal(sql, failpoint);
523
- });
524
- const makeJournal = (sql, failpoint) => {
525
597
  const classifyWriteFailure = (operation) => (error) => error.reason._tag === "LockTimeoutError" ? SqliteWriteContention.make({
526
598
  cause: error,
527
599
  operation,
@@ -585,7 +657,7 @@ const makeJournal = (sql, failpoint) => {
585
657
  FROM effect_agent_conversations
586
658
  WHERE conversation_id = ${conversationId}
587
659
  `.pipe(Effect.mapError(storageError("read materialized conversation")));
588
- const existing = yield* decodeRows(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, existingRows);
660
+ const existing = yield* decodeRows$1(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, existingRows);
589
661
  if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
590
662
  table: "effect_agent_conversations",
591
663
  rowKey: conversationId,
@@ -632,7 +704,7 @@ const makeJournal = (sql, failpoint) => {
632
704
  FROM effect_agent_conversations
633
705
  WHERE conversation_id = ${conversationId}
634
706
  `.pipe(Effect.mapError(storageError("read conversation")));
635
- return yield* decodeRows(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, rows);
707
+ return yield* decodeRows$1(Schema.Array(ConversationRow), "effect_agent_conversations", conversationId, rows);
636
708
  });
637
709
  const append = Effect.fn("SqliteJournal.append")(function* (request) {
638
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({
@@ -674,7 +746,7 @@ const makeJournal = (sql, failpoint) => {
674
746
  WHERE conversation_id = ${request.conversationId}
675
747
  AND batch_id = ${request.batchId}
676
748
  `.pipe(Effect.mapError(storageError("read idempotent batch")));
677
- const batches = yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.conversationId}/${request.batchId}`, batchRows);
749
+ const batches = yield* decodeRows$1(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.conversationId}/${request.batchId}`, batchRows);
678
750
  if (batches.length > 1) return yield* SqliteStorageCorruptionError.make({
679
751
  table: "effect_agent_canonical_batches",
680
752
  rowKey: `${request.conversationId}/${request.batchId}`,
@@ -715,7 +787,7 @@ const makeJournal = (sql, failpoint) => {
715
787
  AND record_id IN ${sql.in(recordIds)}
716
788
  ORDER BY sequence
717
789
  `.pipe(Effect.mapError(storageError("check canonical record identities")));
718
- const existingRecords = yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}/record_ids`, existingRecordRows);
790
+ const existingRecords = yield* decodeRows$1(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}/record_ids`, existingRecordRows);
719
791
  if (existingRecords.length > 0) return yield* SqliteAppendConflict.make({
720
792
  message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
721
793
  reason: "record-identity"
@@ -799,7 +871,7 @@ const makeJournal = (sql, failpoint) => {
799
871
  ORDER BY sequence
800
872
  LIMIT ${request.limit}
801
873
  `.pipe(Effect.mapError(storageError("read canonical records")));
802
- return yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}>${request.fromSequenceExclusive}`, rows);
874
+ return yield* decodeRows$1(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.conversationId}>${request.fromSequenceExclusive}`, rows);
803
875
  });
804
876
  const exportConversation = Effect.fn("SqliteJournal.exportConversation")(function* (conversationId) {
805
877
  return yield* withReadTransaction("export transaction")(Effect.gen(function* () {
@@ -851,9 +923,9 @@ const makeJournal = (sql, failpoint) => {
851
923
  `.pipe(Effect.mapError(storageError("export checkpoints")));
852
924
  return RawConversationExport.make({
853
925
  conversation,
854
- batches: yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", conversationId, batchRows),
855
- records: yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", conversationId, recordRows),
856
- checkpoints: yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", conversationId, checkpointRows)
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)
857
929
  });
858
930
  }));
859
931
  });
@@ -885,7 +957,7 @@ const makeJournal = (sql, failpoint) => {
885
957
  WHERE conversation_id = ${checkpoint.conversationId}
886
958
  AND through_sequence = ${checkpoint.throughSequence}
887
959
  `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
888
- const existing = yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.conversationId}/${checkpoint.throughSequence}`, checkpointRows);
960
+ const existing = yield* decodeRows$1(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.conversationId}/${checkpoint.throughSequence}`, checkpointRows);
889
961
  if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
890
962
  table: "effect_agent_checkpoints",
891
963
  rowKey: `${checkpoint.conversationId}/${checkpoint.throughSequence}`,
@@ -923,7 +995,7 @@ const makeJournal = (sql, failpoint) => {
923
995
  ORDER BY through_sequence DESC
924
996
  LIMIT 1
925
997
  `.pipe(Effect.mapError(storageError("load checkpoint")));
926
- return yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${conversationId}<=${atOrBeforeSequence}`, rows);
998
+ return yield* decodeRows$1(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${conversationId}<=${atOrBeforeSequence}`, rows);
927
999
  });
928
1000
  return {
929
1001
  append,
@@ -947,7 +1019,7 @@ const makeJournal = (sql, failpoint) => {
947
1019
  WHERE conversation_id = ${conversationId}
948
1020
  AND last_sequence = ${sequence}
949
1021
  `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
950
- return (yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${conversationId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
1022
+ return (yield* decodeRows$1(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${conversationId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
951
1023
  }),
952
1024
  loadCheckpoint,
953
1025
  materialize,
@@ -997,65 +1069,16 @@ const makeJournal = (sql, failpoint) => {
997
1069
  ORDER BY conversation_id, through_sequence
998
1070
  `.pipe(Effect.mapError(storageError("scan checkpoints")));
999
1071
  return {
1000
- conversations: yield* decodeRows(Schema.Array(ConversationRow), "effect_agent_conversations", "startup_scan", conversations),
1001
- batches: yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
1002
- records: yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
1003
- checkpoints: yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
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)
1004
1076
  };
1005
1077
  }));
1006
1078
  }),
1007
1079
  withWriteTransaction
1008
1080
  };
1009
- };
1010
- const initializeSqliteJournal = ensureCurrentStorage;
1011
- //#endregion
1012
- //#region src/sqlite-storage-config.ts
1013
- const ObservationPollInterval = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
1014
- const BusyTimeoutMillis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
1015
- const OwnershipLeaseMillis = Schema.Int.check(Schema.isGreaterThan(0));
1016
- /**
1017
- * Validated construction configuration consumed by the SQLite storage Layer. The database
1018
- * identity itself belongs to the SqlClient Layer; duplicating it here could silently diverge
1019
- * from the connection actually in use.
1020
- */
1021
- var SqliteStorageConfigValue = class extends Schema.Class("@effect-agent/storage-sqlite/SqliteStorageConfigValue")({
1022
- observationPollInterval: ObservationPollInterval,
1023
- /** Bounded SQLITE_BUSY retry window for write-lock acquisition, in milliseconds. */
1024
- busyTimeout: BusyTimeoutMillis,
1025
- /**
1026
- * Submission ownership lease duration in milliseconds (D5). The lease is a liveness hint
1027
- * that makes an abandoned claim reclaimable; correctness never depends on it because every
1028
- * canonical append is fenced by producer epoch. Convenience layers default this to
1029
- * `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/session`.
1030
- */
1031
- ownershipLeaseDuration: OwnershipLeaseMillis,
1032
- /**
1033
- * Re-verify every stored payload and digest chain while opening the store. Per-operation
1034
- * Schema decoding and the digest chain already fail clearly on corrupt rows, so the full
1035
- * scan is an explicit opt-in integrity audit rather than a startup requirement.
1036
- */
1037
- verifyOnOpen: Schema.Boolean
1038
- }) {};
1039
- /** Explicit SQLite storage configuration authority. */
1040
- var SqliteStorageConfig = class extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageConfig") {};
1041
- //#endregion
1042
- //#region src/sqlite-storage-failpoint.ts
1043
- const noFailpoint = () => Effect.void;
1044
- /** Test-only control for replacing the active SQLite failpoint handler. */
1045
- var SqliteStorageFailpointTestControl = class extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageFailpointTestControl") {};
1046
- /** Explicit fault-injection authority used at SQLite operation boundaries. */
1047
- var SqliteStorageFailpoint = class SqliteStorageFailpoint extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageFailpoint") {
1048
- /** Production default: no fault injection. */
1049
- static layer = Layer.succeed(this)({ hit: noFailpoint });
1050
- /** Reusable test Layer with a control service backed by the same handler Ref. */
1051
- static layerTest = Layer.effectContext(Effect.gen(function* () {
1052
- const handler = yield* Ref.make(noFailpoint);
1053
- return Context.make(SqliteStorageFailpoint, SqliteStorageFailpoint.of({ hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))) })).pipe(Context.add(SqliteStorageFailpointTestControl, SqliteStorageFailpointTestControl.of({
1054
- clear: Ref.set(handler, noFailpoint),
1055
- setHandler: (next) => Ref.set(handler, next)
1056
- })));
1057
- }));
1058
- };
1081
+ });
1059
1082
  //#endregion
1060
1083
  //#region src/sqlite-conversation-store.ts
1061
1084
  const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
@@ -1258,9 +1281,8 @@ const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPa
1258
1281
  const makeServices$1 = Effect.fn("SqliteConversationStore.makeServices")(function* () {
1259
1282
  const config = yield* SqliteStorageConfig;
1260
1283
  const failpoint = yield* SqliteStorageFailpoint;
1261
- const sql = yield* SqlClient.SqlClient;
1262
1284
  const crypto = yield* Crypto.Crypto;
1263
- const journal = yield* initializeSqliteJournal(sql, failpoint.hit, config.busyTimeout);
1285
+ const journal = yield* initializeSqliteJournal();
1264
1286
  if (config.verifyOnOpen) yield* decodeStartupPayloads(journal, crypto);
1265
1287
  const provideCrypto = (effect) => Effect.provideService(effect, Crypto.Crypto, crypto);
1266
1288
  const hitFailpoint = Effect.fn("SqliteConversationStore.hitFailpoint")((location) => failpoint.hit(location).pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))));
@@ -1654,7 +1676,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1654
1676
  const failpoint = yield* SqliteStorageFailpoint;
1655
1677
  const sql = yield* SqlClient.SqlClient;
1656
1678
  const crypto = yield* Crypto.Crypto;
1657
- const journal = yield* initializeSqliteJournal(sql, failpoint.hit, config.busyTimeout);
1679
+ const journal = yield* initializeSqliteJournal();
1658
1680
  const hitFailpoint = (location, operation) => failpoint.hit(location).pipe(Effect.mapError((error) => internalFailure(operation)(error)));
1659
1681
  /**
1660
1682
  * Run one ledger mutation under the journal's `BEGIN IMMEDIATE` write transaction so
@@ -1669,7 +1691,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1669
1691
  iso: new Date(millis).toISOString()
1670
1692
  }));
1671
1693
  const timestampMillis = (operation, rowKey) => (timestamp) => decodeUtcInstant(timestamp).pipe(Effect.map(DateTime.toEpochMillis), Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submission_ownership", rowKey, error.message)));
1672
- const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1694
+ const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows$1(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1673
1695
  const readSubmission = Effect.fn("SqliteSubmissionLedger.readSubmission")(function* (operation, submissionId) {
1674
1696
  const rows = yield* sql`
1675
1697
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
@@ -1700,7 +1722,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1700
1722
  FROM effect_agent_submission_ownership
1701
1723
  WHERE submission_id = ${submissionId}
1702
1724
  `.pipe(Effect.mapError(sqlFailure(operation)));
1703
- const decoded = yield* decodeRows(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1725
+ const decoded = yield* decodeRows$1(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1704
1726
  if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submission_ownership", submissionId, "An ownership primary key returned more than one row.");
1705
1727
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1706
1728
  });
@@ -1765,7 +1787,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1765
1787
  FROM effect_agent_settlement_reservations
1766
1788
  WHERE submission_id = ${submissionId}
1767
1789
  `.pipe(Effect.mapError(sqlFailure(operation)));
1768
- const decoded = yield* decodeRows(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1790
+ const decoded = yield* decodeRows$1(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1769
1791
  if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", submissionId, "A settlement reservation primary key returned more than one row.");
1770
1792
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1771
1793
  });
@@ -1780,11 +1802,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1780
1802
  FROM effect_agent_abort_intents
1781
1803
  WHERE submission_id = ${submissionId}
1782
1804
  `.pipe(Effect.mapError(sqlFailure(operation)));
1783
- const decoded = yield* decodeRows(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1805
+ const decoded = yield* decodeRows$1(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1784
1806
  if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_abort_intents", submissionId, "An abort intent primary key returned more than one row.");
1785
1807
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
1786
1808
  });
1787
- const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1809
+ const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$1(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
1788
1810
  const readChildReservation = Effect.fn("SqliteSubmissionLedger.readChildReservation")(function* (operation, reservationId) {
1789
1811
  const rows = yield* sql`
1790
1812
  SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
@@ -1837,7 +1859,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1837
1859
  WHERE submission_id = ${submissionId}
1838
1860
  ORDER BY tool_call_id ASC
1839
1861
  `.pipe(Effect.mapError(sqlFailure(operation)));
1840
- return yield* decodeRows(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1862
+ return yield* decodeRows$1(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1841
1863
  });
1842
1864
  const approvalIntentFromRow = Effect.fn("SqliteSubmissionLedger.approvalIntentFromRow")(function* (operation, row) {
1843
1865
  return yield* decodeApprovalDecisionIntent({
@@ -1862,7 +1884,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1862
1884
  WHERE submission_id = ${submissionId}
1863
1885
  ORDER BY tool_call_id ASC
1864
1886
  `.pipe(Effect.mapError(sqlFailure(operation)));
1865
- return yield* decodeRows(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1887
+ return yield* decodeRows$1(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
1866
1888
  });
1867
1889
  const unknownResolutionIntentFromRow = Effect.fn("SqliteSubmissionLedger.unknownResolutionIntentFromRow")(function* (operation, row) {
1868
1890
  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)));
@@ -1893,7 +1915,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1893
1915
  WHERE conversation_id = ${conversationId}
1894
1916
  AND record_id = ${recordId}
1895
1917
  `.pipe(Effect.mapError(sqlFailure(operation)));
1896
- return (yield* decodeRows(Schema.Array(CanonicalRecordIdRow), "effect_agent_canonical_records", `${conversationId}/${recordId}`, rows).pipe(Effect.mapError(internalFailure(operation)))).length === 0 ? void 0 : recordId;
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;
1897
1919
  });
1898
1920
  const abortIntentFromRow = Effect.fn("SqliteSubmissionLedger.abortIntentFromRow")(function* (operation, submission, submissionId, row) {
1899
1921
  const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.conversation_id, submissionId);
@@ -1947,7 +1969,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1947
1969
  FROM effect_agent_submissions
1948
1970
  WHERE conversation_id = ${validated.conversationId}
1949
1971
  `.pipe(Effect.mapError(sqlFailure(operation)));
1950
- const decodedMax = yield* decodeRows(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.conversationId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
1972
+ const decodedMax = yield* decodeRows$1(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.conversationId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
1951
1973
  const queueSequence = yield* decodeQueueSequence((decodedMax[0]?.max_queue_sequence ?? 0) + 1).pipe(Effect.mapError(internalFailure(operation)));
1952
1974
  const now = yield* currentInstant;
1953
1975
  yield* sql`
@@ -3094,6 +3116,251 @@ const submissionLedgerLayer = Layer.effectContext(makeServices());
3094
3116
  */
3095
3117
  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)));
3096
3118
  //#endregion
3097
- export { CurrentSqliteStorageVersion, SqliteAppendConflict, SqliteCheckpointConflict, SqliteFenceRejected, SqliteLedgerError, SqliteStorageCompatibilityError, SqliteStorageConfig, SqliteStorageConfigValue, SqliteStorageCorruptionError, SqliteStorageError, SqliteStorageFailpoint, SqliteStorageFailpointError, SqliteStorageFailpointLocation, SqliteStorageFailpointTestControl, SqliteWriteContention, conversationStoreLayer, layer, ledgerLayer, observationOffsetAt, sqliteMigrations, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer };
3119
+ //#region src/sqlite-schedule-store.ts
3120
+ const StoredScheduleJson = Schema.String.check(Schema.isMaxLength(16 * 1024 * 1024));
3121
+ const StoredDeadline = Schema.NullOr(ScheduleInstant);
3122
+ var ScheduleRow = class extends Schema.Class("@effect-agent/storage-sqlite/ScheduleRow")({
3123
+ tenant_id: ScheduleOwner.fields.tenantId,
3124
+ owner_id: ScheduleOwner.fields.ownerId,
3125
+ schedule_id: ScheduleId,
3126
+ deadline_at_millis: StoredDeadline,
3127
+ record_json: StoredScheduleJson
3128
+ }) {};
3129
+ const ScheduleDueRow = Schema.Struct({
3130
+ tenant_id: ScheduleOwner.fields.tenantId,
3131
+ owner_id: ScheduleOwner.fields.ownerId,
3132
+ schedule_id: ScheduleId,
3133
+ deadline_at_millis: ScheduleInstant
3134
+ });
3135
+ var ScheduleCountRow = class extends Schema.Class("@effect-agent/storage-sqlite/ScheduleCountRow")({ schedule_count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) }) {};
3136
+ var ScheduleDeadlineRow = class extends Schema.Class("@effect-agent/storage-sqlite/ScheduleDeadlineRow")({ deadline_at_millis: StoredDeadline }) {};
3137
+ const unavailable = (operation) => ScheduleStorageError.make({
3138
+ operation,
3139
+ reason: "unavailable"
3140
+ });
3141
+ const corrupt = (operation) => ScheduleStorageError.make({
3142
+ operation,
3143
+ reason: "corrupt"
3144
+ });
3145
+ const decodeRows = Effect.fn("SqliteScheduleStore.decodeRows")(function* (schema, rows, operation) {
3146
+ return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt(operation)));
3147
+ });
3148
+ 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");
3151
+ return record;
3152
+ });
3153
+ const encodeRecord = Effect.fn("SqliteScheduleStore.encodeRecord")(function* (record) {
3154
+ return yield* Schema.encodeEffect(Schema.fromJsonString(ScheduleRecord))(record).pipe(Effect.mapError(() => corrupt("encode schedule")));
3155
+ });
3156
+ const decodeInput = Effect.fn("SqliteScheduleStore.decodeInput")(function* (operation, schema, value) {
3157
+ return yield* Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt(operation)));
3158
+ });
3159
+ const makeScheduleStore = Effect.gen(function* () {
3160
+ const sql = yield* SqlClient.SqlClient;
3161
+ const scheduleFailpoint = yield* ScheduleFailpoint;
3162
+ yield* initializeSqliteJournal();
3163
+ const readRows = Effect.fn("SqliteScheduleStore.readRows")(function* (key, operation) {
3164
+ const rows = yield* sql`
3165
+ SELECT tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
3166
+ FROM effect_agent_schedules
3167
+ WHERE tenant_id = ${key.owner.tenantId}
3168
+ AND owner_id = ${key.owner.ownerId}
3169
+ AND schedule_id = ${key.scheduleId}
3170
+ `.pipe(Effect.mapError(() => unavailable(operation)));
3171
+ return yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
3172
+ });
3173
+ const readOne = Effect.fn("SqliteScheduleStore.readOne")(function* (key, operation) {
3174
+ const rows = yield* readRows(key, operation);
3175
+ if (rows.length === 0) return null;
3176
+ if (rows.length !== 1) return yield* corrupt(operation);
3177
+ return yield* decodeRecord(rows[0]);
3178
+ });
3179
+ const insert = Effect.fn("SqliteScheduleStore.insert")(function* (record, ownerLimit) {
3180
+ const operation = "insert schedule";
3181
+ const canonical = yield* decodeInput(operation, ScheduleRecord, record);
3182
+ const recordJson = yield* encodeRecord(canonical);
3183
+ const result = yield* sql.withTransaction(Effect.gen(function* () {
3184
+ const existing = yield* readOne(canonical, operation);
3185
+ if (existing !== null) {
3186
+ if (existing.creationFingerprint === canonical.creationFingerprint) return {
3187
+ record: existing,
3188
+ inserted: false
3189
+ };
3190
+ return yield* ScheduleConflict.make({
3191
+ reason: "creation",
3192
+ key: {
3193
+ owner: canonical.owner,
3194
+ scheduleId: canonical.scheduleId
3195
+ }
3196
+ });
3197
+ }
3198
+ const rawCounts = yield* sql`
3199
+ SELECT COUNT(*) AS schedule_count
3200
+ FROM effect_agent_schedules
3201
+ WHERE tenant_id = ${canonical.owner.tenantId}
3202
+ AND owner_id = ${canonical.owner.ownerId}
3203
+ AND (json_extract(record_json, '$.pending') IS NOT NULL OR
3204
+ (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);
3208
+ if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
3209
+ yield* scheduleFailpoint.hit("schedule:insert:before");
3210
+ yield* sql`
3211
+ INSERT INTO effect_agent_schedules (
3212
+ tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
3213
+ ) VALUES (
3214
+ ${canonical.owner.tenantId},
3215
+ ${canonical.owner.ownerId},
3216
+ ${canonical.scheduleId},
3217
+ ${scheduleDeadline(canonical)},
3218
+ ${recordJson}
3219
+ )
3220
+ `.pipe(Effect.mapError(() => unavailable(operation)));
3221
+ return {
3222
+ record: canonical,
3223
+ inserted: true
3224
+ };
3225
+ })).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable(operation))));
3226
+ if (result.inserted) yield* scheduleFailpoint.hit("schedule:insert:after");
3227
+ return result.record;
3228
+ });
3229
+ const get = Effect.fn("SqliteScheduleStore.get")(function* (key) {
3230
+ const decodedKey = yield* decodeInput("get schedule", ScheduleKey, key);
3231
+ return yield* readOne(decodedKey, "get schedule");
3232
+ });
3233
+ const list = Effect.fn("SqliteScheduleStore.list")(function* (request) {
3234
+ const operation = "list schedules";
3235
+ const decodedRequest = yield* decodeInput(operation, SchedulePageRequest, request);
3236
+ const rows = decodedRequest.after === void 0 ? yield* sql`
3237
+ SELECT tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
3238
+ FROM effect_agent_schedules
3239
+ WHERE tenant_id = ${decodedRequest.owner.tenantId}
3240
+ AND owner_id = ${decodedRequest.owner.ownerId}
3241
+ ORDER BY schedule_id
3242
+ LIMIT ${decodedRequest.limit + 1}
3243
+ `.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
3244
+ SELECT tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
3245
+ FROM effect_agent_schedules
3246
+ WHERE tenant_id = ${decodedRequest.owner.tenantId}
3247
+ AND owner_id = ${decodedRequest.owner.ownerId}
3248
+ AND schedule_id > ${decodedRequest.after}
3249
+ ORDER BY schedule_id
3250
+ LIMIT ${decodedRequest.limit + 1}
3251
+ `.pipe(Effect.mapError(() => unavailable(operation)));
3252
+ const decoded = yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
3253
+ const records = yield* Effect.forEach(decoded, decodeRecord);
3254
+ const hasNext = records.length > decodedRequest.limit;
3255
+ const items = hasNext ? records.slice(0, decodedRequest.limit) : records;
3256
+ return {
3257
+ items,
3258
+ next: hasNext ? items.at(-1)?.scheduleId ?? null : null
3259
+ };
3260
+ });
3261
+ const change = Effect.fn("SqliteScheduleStore.change")(function* (key, change, ownerLimit = defaultSchedulingLimits.maxSchedulesPerOwner) {
3262
+ const operation = "change schedule";
3263
+ const decodedKey = yield* decodeInput(operation, ScheduleKey, key);
3264
+ const decodedChange = yield* decodeInput(operation, ScheduleChange, change);
3265
+ const result = yield* sql.withTransaction(Effect.gen(function* () {
3266
+ const current = yield* readOne(decodedKey, operation);
3267
+ if (current === null) return yield* ScheduleNotFound.make({ key: decodedKey });
3268
+ const transition = applyScheduleChange(current, decodedChange);
3269
+ if (Result.isFailure(transition)) return yield* transition.failure;
3270
+ const next = transition.success;
3271
+ if (!scheduleUsesCapacity(current) && scheduleUsesCapacity(next)) {
3272
+ const rawCounts = yield* sql`
3273
+ SELECT COUNT(*) AS schedule_count FROM effect_agent_schedules
3274
+ WHERE tenant_id = ${key.owner.tenantId} AND owner_id = ${key.owner.ownerId}
3275
+ AND (json_extract(record_json, '$.pending') IS NOT NULL OR
3276
+ (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);
3280
+ if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
3281
+ }
3282
+ if (next === current) return {
3283
+ record: current,
3284
+ changed: false
3285
+ };
3286
+ const recordJson = yield* encodeRecord(next);
3287
+ yield* scheduleFailpoint.hit(`schedule:${decodedChange._tag.toLowerCase()}:before`);
3288
+ yield* sql`
3289
+ UPDATE effect_agent_schedules
3290
+ SET deadline_at_millis = ${scheduleDeadline(next)}, record_json = ${recordJson}
3291
+ WHERE tenant_id = ${decodedKey.owner.tenantId}
3292
+ AND owner_id = ${decodedKey.owner.ownerId}
3293
+ AND schedule_id = ${decodedKey.scheduleId}
3294
+ `.pipe(Effect.mapError(() => unavailable(operation)));
3295
+ return {
3296
+ record: next,
3297
+ changed: true
3298
+ };
3299
+ })).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable(operation))));
3300
+ if (result.changed) yield* scheduleFailpoint.hit(`schedule:${decodedChange._tag.toLowerCase()}:after`);
3301
+ return result.record;
3302
+ });
3303
+ const due = Effect.fn("SqliteScheduleStore.due")(function* (nowMillis, limit, owner, after) {
3304
+ const operation = "query due schedules";
3305
+ 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)));
3307
+ const continuation = cursor === void 0 ? sql`1 = 1` : sql`
3308
+ (deadline_at_millis, tenant_id, owner_id, schedule_id) >
3309
+ (${cursor.deadlineAtMillis}, ${cursor.owner.tenantId}, ${cursor.owner.ownerId}, ${cursor.scheduleId})`;
3310
+ const rows = decodedOwner === void 0 ? yield* sql`
3311
+ SELECT tenant_id, owner_id, schedule_id, deadline_at_millis
3312
+ FROM effect_agent_schedules
3313
+ WHERE deadline_at_millis <= ${nowMillis} AND ${continuation}
3314
+ ORDER BY deadline_at_millis, tenant_id, owner_id, schedule_id
3315
+ LIMIT ${limit}
3316
+ `.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
3317
+ SELECT tenant_id, owner_id, schedule_id, deadline_at_millis
3318
+ FROM effect_agent_schedules
3319
+ WHERE tenant_id = ${decodedOwner.tenantId}
3320
+ AND owner_id = ${decodedOwner.ownerId}
3321
+ AND deadline_at_millis <= ${nowMillis} AND ${continuation}
3322
+ ORDER BY deadline_at_millis, schedule_id
3323
+ LIMIT ${limit}
3324
+ `.pipe(Effect.mapError(() => unavailable(operation)));
3325
+ return (yield* decodeRows(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
3326
+ owner: {
3327
+ tenantId: row.tenant_id,
3328
+ ownerId: row.owner_id
3329
+ },
3330
+ scheduleId: row.schedule_id,
3331
+ deadlineAtMillis: row.deadline_at_millis
3332
+ }));
3333
+ });
3334
+ const nextDeadline = Effect.fn("SqliteScheduleStore.nextDeadline")(function* (owner) {
3335
+ const operation = "query next schedule deadline";
3336
+ const decodedOwner = owner === void 0 ? void 0 : yield* decodeInput(operation, ScheduleOwner, owner);
3337
+ const rows = decodedOwner === void 0 ? yield* sql`
3338
+ SELECT MIN(deadline_at_millis) AS deadline_at_millis
3339
+ FROM effect_agent_schedules
3340
+ WHERE deadline_at_millis IS NOT NULL
3341
+ `.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
3342
+ SELECT MIN(deadline_at_millis) AS deadline_at_millis
3343
+ FROM effect_agent_schedules
3344
+ WHERE tenant_id = ${decodedOwner.tenantId}
3345
+ AND owner_id = ${decodedOwner.ownerId}
3346
+ 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);
3350
+ return decoded[0].deadline_at_millis;
3351
+ });
3352
+ return ScheduleStore.of({
3353
+ insert,
3354
+ get,
3355
+ list,
3356
+ change,
3357
+ due,
3358
+ nextDeadline
3359
+ });
3360
+ });
3361
+ /** SQLite implementation of the atomic ScheduleStore port. */
3362
+ const scheduleStoreLayer = Layer.effect(ScheduleStore)(makeScheduleStore);
3363
+ //#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 };
3098
3365
 
3099
3366
  //# sourceMappingURL=index.mjs.map