@effect-agent/storage-sqlite 0.1.0-beta.36 → 0.1.0-beta.38
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.d.mts +7 -3
- package/dist/index.mjs +367 -100
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/migrations.ts +31 -1
- package/src/sqlite-conversation-store.ts +2 -3
- package/src/sqlite-journal.ts +13 -27
- package/src/sqlite-ledger.ts +1 -1
- package/src/sqlite-schedule-store.ts +363 -0
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 =
|
|
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
|
|
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 = ${
|
|
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:
|
|
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 !==
|
|
548
|
+
if (version.user_version !== 0 && version.user_version !== 5) return yield* SqliteStorageCompatibilityError.make({
|
|
475
549
|
actualVersion: version.user_version,
|
|
476
|
-
supportedVersion:
|
|
477
|
-
message: `The SQLite file uses private-development storage version ${version.user_version}; this build supports exactly 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.`
|
|
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:
|
|
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.
|
|
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 !==
|
|
518
|
-
actualVersion:
|
|
519
|
-
supportedVersion:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|