@effect-agent/storage-sqlite 0.1.0-beta.69 → 0.1.0-beta.71

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.
@@ -1816,6 +1816,76 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1816
1816
  return reserved;
1817
1817
  });
1818
1818
 
1819
+ const validateFinalization = Effect.fn("SqliteSubmissionLedger.validateFinalization")(function* (
1820
+ validated: SettlementFinalization,
1821
+ reservation: Option.Option<ReservationRow>,
1822
+ ) {
1823
+ const operation = "ledger finalize settlement";
1824
+
1825
+ if (Option.isNone(reservation)) {
1826
+ return yield* LedgerError.make({
1827
+ operation,
1828
+ message: `No settlement reservation exists for submission ${validated.submissionId}.`,
1829
+ });
1830
+ }
1831
+ if (reservation.value.settlement_id !== validated.settlementId) {
1832
+ return yield* SettlementConflict.make({
1833
+ submissionId: validated.submissionId,
1834
+ existingOutcome: reservation.value.outcome,
1835
+ });
1836
+ }
1837
+
1838
+ const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(
1839
+ Effect.mapError((error) =>
1840
+ corruptionFailure(
1841
+ operation,
1842
+ "effect_agent_settlement_reservations",
1843
+ validated.submissionId,
1844
+ error.message,
1845
+ ),
1846
+ ),
1847
+ );
1848
+
1849
+ const settlementFailure = settlementFailureFromRecord(reservationRecord);
1850
+
1851
+ if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
1852
+ return yield* corruptionFailure(
1853
+ operation,
1854
+ "effect_agent_settlement_reservations",
1855
+ validated.submissionId,
1856
+ "The reserved outcome and canonical failure diagnostic disagree.",
1857
+ );
1858
+ }
1859
+
1860
+ return { reservation: reservation.value, settlementFailure };
1861
+ });
1862
+
1863
+ const replayFinalization = Effect.fn("SqliteSubmissionLedger.replayFinalization")(function* (
1864
+ validated: SettlementFinalization,
1865
+ submission: SubmissionRow,
1866
+ { reservation, settlementFailure }: Effect.Success<ReturnType<typeof validateFinalization>>,
1867
+ ) {
1868
+ const operation = "ledger finalize settlement";
1869
+
1870
+ if (reservation.finalized_at === null) {
1871
+ return yield* corruptionFailure(
1872
+ operation,
1873
+ "effect_agent_settlement_reservations",
1874
+ validated.submissionId,
1875
+ "A settled Submission's reservation carries no finalization timestamp.",
1876
+ );
1877
+ }
1878
+
1879
+ return yield* decodeSettlement({
1880
+ submissionId: validated.submissionId,
1881
+ settlementId: validated.settlementId,
1882
+ receiptId: submission.receipt_id,
1883
+ outcome: reservation.outcome,
1884
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1885
+ settledAt: reservation.finalized_at,
1886
+ }).pipe(Effect.mapError(internalFailure(operation)));
1887
+ });
1888
+
1819
1889
  const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
1820
1890
  "SqliteSubmissionLedger.finalizeSettlement",
1821
1891
  )(function* (request: SettlementFinalization) {
@@ -1827,73 +1897,74 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1827
1897
 
1828
1898
  yield* hitFailpoint("ledger:finalize-settlement:before", operation);
1829
1899
 
1830
- const settlement = yield* inWriteTransaction(
1831
- operation,
1832
- Effect.gen(function* () {
1833
- const reservation = yield* readReservation(operation, validated.submissionId);
1900
+ // A single statement captures the settled row and its immutable reservation together.
1901
+ // A miss does not authorize finalization: the write path re-reads under its transaction.
1902
+ const replayRows = yield* sql<Record<string, unknown>>`
1903
+ SELECT submission.*, reservation.settlement_id, reservation.outcome,
1904
+ reservation.record_id, reservation.record_json, reservation.record_digest,
1905
+ reservation.reserved_at, reservation.finalized_at
1906
+ FROM effect_agent_submissions AS submission
1907
+ INNER JOIN effect_agent_settlement_reservations AS reservation
1908
+ ON reservation.submission_id = submission.submission_id
1909
+ WHERE submission.submission_id = ${validated.submissionId} AND submission.state = 'settled'
1910
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1834
1911
 
1835
- if (Option.isNone(reservation)) {
1836
- return yield* LedgerError.make({
1837
- operation,
1838
- message: `No settlement reservation exists for submission ${validated.submissionId}.`,
1839
- });
1840
- }
1841
- if (reservation.value.settlement_id !== validated.settlementId) {
1842
- return yield* SettlementConflict.make({
1843
- submissionId: validated.submissionId,
1844
- existingOutcome: reservation.value.outcome,
1845
- });
1846
- }
1912
+ if (replayRows.length > 1) {
1913
+ return yield* corruptionFailure(
1914
+ operation,
1915
+ "effect_agent_submissions",
1916
+ validated.submissionId,
1917
+ "A submission primary key returned more than one row.",
1918
+ );
1919
+ }
1920
+ const replayRow = replayRows[0];
1847
1921
 
1848
- const reservationRecord = yield* decodeRecordEnvelopeText(
1849
- reservation.value.record_json,
1850
- ).pipe(
1851
- Effect.mapError((error) =>
1852
- corruptionFailure(
1853
- operation,
1854
- "effect_agent_settlement_reservations",
1855
- validated.submissionId,
1856
- error.message,
1857
- ),
1858
- ),
1859
- );
1922
+ if (replayRow !== undefined) {
1923
+ const reservations = yield* decodeRows(
1924
+ Schema.Array(ReservationRow),
1925
+ "effect_agent_settlement_reservations",
1926
+ validated.submissionId,
1927
+ replayRows,
1928
+ ).pipe(Effect.mapError(internalFailure(operation)));
1860
1929
 
1861
- const settlementFailure = settlementFailureFromRecord(reservationRecord);
1930
+ const state = yield* validateFinalization(validated, Option.fromUndefinedOr(reservations[0]));
1862
1931
 
1863
- if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
1864
- return yield* corruptionFailure(
1932
+ const submission = yield* Schema.decodeUnknownEffect(SubmissionRow)(replayRow).pipe(
1933
+ Effect.mapError((error) =>
1934
+ corruptionFailure(
1865
1935
  operation,
1866
- "effect_agent_settlement_reservations",
1936
+ "effect_agent_submissions",
1867
1937
  validated.submissionId,
1868
- "The reserved outcome and canonical failure diagnostic disagree.",
1869
- );
1870
- }
1871
- const submission = yield* requireSubmission(operation, validated.submissionId);
1938
+ error.message,
1939
+ ),
1940
+ ),
1941
+ );
1872
1942
 
1873
- if (submission.state === "settled") {
1874
- if (reservation.value.finalized_at === null) {
1875
- return yield* corruptionFailure(
1876
- operation,
1877
- "effect_agent_settlement_reservations",
1878
- validated.submissionId,
1879
- "A settled Submission's reservation carries no finalization timestamp.",
1880
- );
1881
- }
1943
+ const settlement = yield* replayFinalization(validated, submission, state);
1882
1944
 
1883
- return yield* decodeSettlement({
1884
- submissionId: validated.submissionId,
1885
- settlementId: validated.settlementId,
1886
- receiptId: submission.receipt_id,
1887
- outcome: reservation.value.outcome,
1888
- ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1889
- settledAt: reservation.value.finalized_at,
1890
- }).pipe(Effect.mapError(internalFailure(operation)));
1891
- }
1945
+ yield* hitFailpoint("ledger:finalize-settlement:after", operation);
1946
+
1947
+ return settlement;
1948
+ }
1949
+
1950
+ const settlement = yield* inWriteTransaction(
1951
+ operation,
1952
+ Effect.gen(function* () {
1953
+ const state = yield* validateFinalization(
1954
+ validated,
1955
+ yield* readReservation(operation, validated.submissionId),
1956
+ );
1957
+
1958
+ const { reservation, settlementFailure } = state;
1959
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1960
+
1961
+ if (submission.state === "settled")
1962
+ return yield* replayFinalization(validated, submission, state);
1892
1963
  const now = yield* currentInstant;
1893
1964
 
1894
1965
  yield* sql`
1895
1966
  UPDATE effect_agent_submissions
1896
- SET state = 'settled', settled_outcome = ${reservation.value.outcome}
1967
+ SET state = 'settled', settled_outcome = ${reservation.outcome}
1897
1968
  WHERE submission_id = ${validated.submissionId}
1898
1969
  `.pipe(Effect.mapError(sqlFailure(operation)));
1899
1970
  yield* sql`
@@ -1910,7 +1981,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1910
1981
  submissionId: validated.submissionId,
1911
1982
  settlementId: validated.settlementId,
1912
1983
  receiptId: submission.receipt_id,
1913
- outcome: reservation.value.outcome,
1984
+ outcome: reservation.outcome,
1914
1985
  ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1915
1986
  settledAt: now.iso,
1916
1987
  }).pipe(Effect.mapError(internalFailure(operation)));
@@ -3051,13 +3122,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
3051
3122
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3052
3123
  FROM effect_agent_submissions
3053
3124
  WHERE state <> 'settled'
3054
- AND (
3055
- thread_id > ${cursor.threadId}
3056
- OR (
3057
- thread_id = ${cursor.threadId}
3058
- AND queue_sequence > ${cursor.queueSequence}
3059
- )
3060
- )
3125
+ AND (thread_id, queue_sequence) > (${cursor.threadId}, ${cursor.queueSequence})
3061
3126
  ORDER BY thread_id ASC, queue_sequence ASC
3062
3127
  LIMIT ${SCAN_PAGE_SIZE}
3063
3128
  `
@@ -5,7 +5,15 @@ import * as SqlClient from "effect/unstable/sql/SqlClient";
5
5
  import { createMessageDeliveryTables } from "./message-delivery-schema.ts";
6
6
  import { createRecoveryCheckpointTable } from "./recovery-checkpoint-schema.ts";
7
7
 
8
- export const CurrentSqliteStorageVersion = 10;
8
+ export const CurrentSqliteStorageVersion = 11;
9
+
10
+ /** Index only outstanding obligations, ordered by the recovery scan's stable cursor. */
11
+ export const createNonterminalIndex = Effect.gen(function* () {
12
+ const sql = yield* SqlClient.SqlClient;
13
+
14
+ yield* sql`CREATE INDEX effect_agent_submissions_nonterminal ON effect_agent_submissions (thread_id, queue_sequence) WHERE state <> 'settled'`
15
+ .withoutTransform;
16
+ });
9
17
 
10
18
  /** Initialize empty storage with the complete current schema. */
11
19
  export const sqliteMigrations = SqliteMigrator.fromRecord({
@@ -336,8 +344,9 @@ export const sqliteMigrations = SqliteMigrator.fromRecord({
336
344
  .withoutTransform;
337
345
  yield* sql`CREATE INDEX effect_agent_subscription_deliveries_registration ON effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, delivery_key)`
338
346
  .withoutTransform;
347
+ yield* createNonterminalIndex;
339
348
  yield* createMessageDeliveryTables;
340
349
  yield* createRecoveryCheckpointTable;
341
- yield* sql`PRAGMA user_version = 10`.withoutTransform;
350
+ yield* sql`PRAGMA user_version = 11`.withoutTransform;
342
351
  }),
343
352
  });
@@ -37,7 +37,11 @@ import {
37
37
  } from "../SqliteStorageError.ts";
38
38
  import { SqliteStorageFailpoint } from "../SqliteStorageFailpoint.ts";
39
39
  import { createMessageDeliveryTables } from "./message-delivery-schema.ts";
40
- import { CurrentSqliteStorageVersion, sqliteMigrations } from "./migrations.ts";
40
+ import {
41
+ CurrentSqliteStorageVersion,
42
+ createNonterminalIndex,
43
+ sqliteMigrations,
44
+ } from "./migrations.ts";
41
45
  import { createRecoveryCheckpointTable } from "./recovery-checkpoint-schema.ts";
42
46
 
43
47
  const BoundedStoredText = Schema.String.check(Schema.isMaxLength(16 * 1024 * 1024));
@@ -381,11 +385,11 @@ const predecessorColumns = {
381
385
  } as const;
382
386
 
383
387
  const checkPredecessorLayout = Effect.fn("SqliteJournal.checkPredecessorLayout")(function* (
384
- version: 8 | 9,
388
+ version: 8 | 9 | 10,
385
389
  ) {
386
390
  const sql = yield* SqlClient.SqlClient;
387
391
 
388
- const expectedColumns =
392
+ const messageColumns =
389
393
  version === 8
390
394
  ? predecessorColumns
391
395
  : {
@@ -405,6 +409,20 @@ const checkPredecessorLayout = Effect.fn("SqliteJournal.checkPredecessorLayout")
405
409
  ],
406
410
  };
407
411
 
412
+ const expectedColumns = {
413
+ ...messageColumns,
414
+ ...(version === 10
415
+ ? {
416
+ effect_agent_recovery_checkpoints: [
417
+ "thread_id",
418
+ "through_sequence",
419
+ "tail_digest",
420
+ "checkpoint_json",
421
+ ],
422
+ }
423
+ : {}),
424
+ };
425
+
408
426
  for (const [table, expected] of Object.entries(expectedColumns)) {
409
427
  const columns = yield* decodeRows(
410
428
  Schema.Array(Schema.Struct({ name: BoundedIdentifier })),
@@ -472,6 +490,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
472
490
  version.user_version !== 7 &&
473
491
  version.user_version !== 8 &&
474
492
  version.user_version !== 9 &&
493
+ version.user_version !== 10 &&
475
494
  version.user_version !== CurrentSqliteStorageVersion
476
495
  ) {
477
496
  return yield* SqliteStorageCompatibilityError.make({
@@ -480,11 +499,16 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
480
499
  message:
481
500
  `The SQLite file uses unsupported storage version ${version.user_version}; ` +
482
501
  `this build supports exactly version ${CurrentSqliteStorageVersion}. ` +
483
- "Only supported v7, v8 and v9 can be upgraded automatically. Keep the original file and use a compatible library version.",
502
+ "Only supported v7, v8, v9 and v10 can be upgraded automatically. Keep the original file and use a compatible library version.",
484
503
  });
485
504
  }
486
505
 
487
- if (version.user_version === 7 || version.user_version === 8 || version.user_version === 9) {
506
+ if (
507
+ version.user_version === 7 ||
508
+ version.user_version === 8 ||
509
+ version.user_version === 9 ||
510
+ version.user_version === 10
511
+ ) {
488
512
  yield* sql
489
513
  .withTransaction(
490
514
  Effect.gen(function* () {
@@ -496,7 +520,8 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
496
520
  current.length !== 1 ||
497
521
  (current[0].user_version !== 7 &&
498
522
  current[0].user_version !== 8 &&
499
- current[0].user_version !== 9)
523
+ current[0].user_version !== 9 &&
524
+ current[0].user_version !== 10)
500
525
  )
501
526
  return yield* SqliteStorageCompatibilityError.make({
502
527
  actualVersion: -1,
@@ -524,12 +549,23 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
524
549
  const recoveryTables =
525
550
  yield* sql`SELECT name FROM sqlite_master WHERE type='table' AND name='effect_agent_recovery_checkpoints'`;
526
551
 
527
- if (recoveryTables.length !== 0)
552
+ if (recoveryTables.length !== (current[0].user_version === 10 ? 1 : 0))
528
553
  return yield* SqliteStorageCompatibilityError.make({
529
554
  actualVersion: current[0].user_version,
530
555
  supportedVersion: CurrentSqliteStorageVersion,
531
556
  message:
532
- "The predecessor already contains recovery checkpoint storage; refusing ambiguous data without mutation.",
557
+ "The predecessor recovery checkpoint storage does not match its version; refusing ambiguous data without mutation.",
558
+ });
559
+
560
+ const indexes =
561
+ yield* sql`SELECT name FROM sqlite_master WHERE name='effect_agent_submissions_nonterminal'`;
562
+
563
+ if (indexes.length !== 0)
564
+ return yield* SqliteStorageCompatibilityError.make({
565
+ actualVersion: current[0].user_version,
566
+ supportedVersion: CurrentSqliteStorageVersion,
567
+ message:
568
+ "The predecessor already contains the nonterminal index; refusing ambiguous storage without mutation.",
533
569
  });
534
570
  if (current[0].user_version === 7) {
535
571
  yield* checkV2ThreadLayout();
@@ -561,9 +597,13 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
561
597
  }),
562
598
  );
563
599
  }
564
- if (current[0].user_version === 8 || current[0].user_version === 9)
600
+ if (
601
+ current[0].user_version === 8 ||
602
+ current[0].user_version === 9 ||
603
+ current[0].user_version === 10
604
+ )
565
605
  yield* checkPredecessorLayout(current[0].user_version);
566
- if (current[0].user_version !== 9) {
606
+ if (current[0].user_version === 7 || current[0].user_version === 8) {
567
607
  yield* failpoint("upgrade:before-mutation");
568
608
  yield* sql`ALTER TABLE effect_agent_submissions ADD COLUMN worker_admission_json TEXT`;
569
609
  yield* failpoint("upgrade:after-mutation");
@@ -574,11 +614,16 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
574
614
  yield* createMessageDeliveryTables;
575
615
  yield* failpoint("upgrade:after-mutation");
576
616
  }
617
+ if (current[0].user_version !== 10) {
618
+ yield* failpoint("upgrade:before-mutation");
619
+ yield* createRecoveryCheckpointTable;
620
+ yield* failpoint("upgrade:after-mutation");
621
+ }
577
622
  yield* failpoint("upgrade:before-mutation");
578
- yield* createRecoveryCheckpointTable;
623
+ yield* createNonterminalIndex;
579
624
  yield* failpoint("upgrade:after-mutation");
580
625
  yield* failpoint("upgrade:before-version");
581
- yield* sql`PRAGMA user_version = 10`;
626
+ yield* sql`PRAGMA user_version = 11`;
582
627
  yield* failpoint("upgrade:after-version");
583
628
  }),
584
629
  )
@@ -655,7 +700,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
655
700
  const requiredRows = yield* sql<Record<string, unknown>>`
656
701
  SELECT name
657
702
  FROM sqlite_master
658
- WHERE type = 'table'
703
+ WHERE (type = 'table'
659
704
  AND name IN (
660
705
  'effect_agent_threads',
661
706
  'effect_agent_canonical_batches',
@@ -671,7 +716,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
671
716
  'effect_agent_schedules',
672
717
  'effect_agent_message_deliveries',
673
718
  'effect_agent_recovery_checkpoints'
674
- )
719
+ )) OR (type = 'index' AND name = 'effect_agent_submissions_nonterminal')
675
720
  ORDER BY name
676
721
  `.pipe(Effect.mapError(storageError("verify storage tables")));
677
722
 
@@ -682,12 +727,12 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
682
727
  requiredRows,
683
728
  );
684
729
 
685
- if (required.length !== 14) {
730
+ if (required.length !== 15) {
686
731
  return yield* SqliteStorageCompatibilityError.make({
687
732
  actualVersion: CurrentSqliteStorageVersion,
688
733
  supportedVersion: CurrentSqliteStorageVersion,
689
734
  message:
690
- "The SQLite file claims the current format but is missing required tables. Retain the original store for inspection.",
735
+ "The SQLite file claims the current format but is missing required tables or its nonterminal index. Retain the original store for inspection.",
691
736
  });
692
737
  }
693
738
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"migrations-NWxfq43W.mjs","names":[],"sources":["../src/internal/message-delivery-schema.ts","../src/internal/recovery-checkpoint-schema.ts","../src/internal/migrations.ts"],"sourcesContent":["import { Effect } from \"effect\";\nimport * as SqlClient from \"effect/unstable/sql/SqlClient\";\n\n/** Additive storage owned by this adapter; creation participates in its version transaction. */\nexport const createMessageDeliveryTables = Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n\n yield* sql`\n CREATE TABLE effect_agent_message_deliveries (\n owner_thread_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n version INTEGER NOT NULL,\n state TEXT NOT NULL,\n deadline_at_millis INTEGER,\n record_json TEXT NOT NULL,\n PRIMARY KEY (owner_thread_id, message_id)\n )\n `.withoutTransform;\n yield* sql`\n CREATE INDEX effect_agent_message_deliveries_due\n ON effect_agent_message_deliveries (deadline_at_millis, owner_thread_id, message_id)\n WHERE deadline_at_millis IS NOT NULL\n `.withoutTransform;\n});\n","import { Effect } from \"effect\";\nimport * as SqlClient from \"effect/unstable/sql/SqlClient\";\n\n/** One disposable recovery snapshot per Thread, independent of generic projections. */\nexport const createRecoveryCheckpointTable = Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n\n yield* sql`\n CREATE TABLE effect_agent_recovery_checkpoints (\n thread_id TEXT PRIMARY KEY NOT NULL,\n through_sequence INTEGER NOT NULL,\n tail_digest TEXT NOT NULL,\n checkpoint_json TEXT NOT NULL,\n FOREIGN KEY (thread_id) REFERENCES effect_agent_threads(thread_id) ON DELETE RESTRICT\n )\n `.withoutTransform;\n});\n","import { SqliteMigrator } from \"@effect/sql-sqlite-node\";\nimport { Effect } from \"effect\";\nimport * as SqlClient from \"effect/unstable/sql/SqlClient\";\n\nimport { createMessageDeliveryTables } from \"./message-delivery-schema.ts\";\nimport { createRecoveryCheckpointTable } from \"./recovery-checkpoint-schema.ts\";\n\nexport const CurrentSqliteStorageVersion = 10;\n\n/** Initialize empty storage with the complete current schema. */\nexport const sqliteMigrations = SqliteMigrator.fromRecord({\n \"1_current_thread_storage\": Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n\n yield* sql`\n CREATE TABLE effect_agent_threads (\n thread_id TEXT PRIMARY KEY NOT NULL,\n created_at TEXT NOT NULL,\n tail_sequence INTEGER NOT NULL,\n tail_digest TEXT NOT NULL,\n producer_epoch INTEGER NOT NULL\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_canonical_batches (\n thread_id TEXT NOT NULL,\n batch_id TEXT NOT NULL,\n first_sequence INTEGER NOT NULL,\n last_sequence INTEGER NOT NULL,\n batch_digest TEXT NOT NULL,\n tail_digest TEXT NOT NULL,\n batch_json TEXT NOT NULL,\n PRIMARY KEY (thread_id, batch_id),\n FOREIGN KEY (thread_id)\n REFERENCES effect_agent_threads(thread_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_canonical_records (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n record_id TEXT NOT NULL,\n batch_id TEXT NOT NULL,\n record_json TEXT NOT NULL,\n PRIMARY KEY (thread_id, sequence),\n UNIQUE (thread_id, record_id),\n FOREIGN KEY (thread_id, batch_id)\n REFERENCES effect_agent_canonical_batches(thread_id, batch_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE INDEX effect_agent_canonical_records_batch\n ON effect_agent_canonical_records (thread_id, batch_id, sequence)\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_checkpoints (\n thread_id TEXT NOT NULL,\n through_sequence INTEGER NOT NULL,\n tail_digest TEXT NOT NULL,\n checkpoint_json TEXT NOT NULL,\n PRIMARY KEY (thread_id, through_sequence),\n FOREIGN KEY (thread_id)\n REFERENCES effect_agent_threads(thread_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n // Admission rows exist before Thread materialization (durability §4), so\n // thread_id intentionally carries no foreign key into effect_agent_threads.\n yield* sql`\n CREATE TABLE effect_agent_submissions (\n submission_id TEXT PRIMARY KEY NOT NULL,\n thread_id TEXT NOT NULL,\n queue_sequence INTEGER NOT NULL,\n principal TEXT NOT NULL,\n idempotency_key TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n agent_digests_json TEXT NOT NULL,\n deployment_id TEXT NOT NULL,\n input_json TEXT NOT NULL,\n input_digest TEXT NOT NULL,\n receipt_id TEXT NOT NULL,\n state TEXT NOT NULL,\n settled_outcome TEXT,\n created_at TEXT NOT NULL,\n ready_at TEXT,\n input_applied_record_id TEXT,\n input_applied_sequence INTEGER,\n joined_host_submission_id TEXT,\n suspended_reason_json TEXT,\n suspended_at TEXT,\n unknown_reason TEXT,\n unknown_tool_call_ids_json TEXT,\n parent_submission_id TEXT,\n parent_tool_call_id TEXT,\n admission_group TEXT,\n admission_fence_json TEXT,\n worker_admission_json TEXT,\n message_admission_json TEXT,\n UNIQUE (thread_id, principal, idempotency_key),\n UNIQUE (thread_id, queue_sequence)\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE INDEX effect_agent_submissions_group\n ON effect_agent_submissions (thread_id, admission_group, state)\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_submission_ownership (\n submission_id TEXT PRIMARY KEY NOT NULL,\n attempt_id TEXT NOT NULL,\n ownership_token TEXT NOT NULL,\n producer_epoch INTEGER NOT NULL,\n owner_producer_id TEXT NOT NULL,\n lease_expires_at TEXT NOT NULL,\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_attempts (\n attempt_id TEXT PRIMARY KEY NOT NULL,\n submission_id TEXT NOT NULL,\n thread_id TEXT NOT NULL,\n owner_producer_id TEXT NOT NULL,\n producer_epoch INTEGER NOT NULL,\n claimed_at TEXT NOT NULL,\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_settlement_reservations (\n submission_id TEXT PRIMARY KEY NOT NULL,\n settlement_id TEXT NOT NULL,\n outcome TEXT NOT NULL,\n record_id TEXT NOT NULL,\n record_json TEXT NOT NULL,\n record_digest TEXT NOT NULL,\n reserved_at TEXT NOT NULL,\n finalized_at TEXT,\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_abort_intents (\n submission_id TEXT PRIMARY KEY NOT NULL,\n author TEXT NOT NULL,\n reason TEXT NOT NULL,\n requested_at TEXT NOT NULL,\n canonical_record_id TEXT,\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE INDEX effect_agent_submissions_joined_host\n ON effect_agent_submissions (joined_host_submission_id)\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_approval_decisions (\n submission_id TEXT NOT NULL,\n tool_call_id TEXT NOT NULL,\n decision TEXT NOT NULL,\n resolver TEXT NOT NULL,\n reason TEXT NOT NULL,\n decided_at TEXT NOT NULL,\n PRIMARY KEY (submission_id, tool_call_id),\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_unknown_resolutions (\n submission_id TEXT NOT NULL,\n tool_call_id TEXT NOT NULL,\n author TEXT NOT NULL,\n reason TEXT NOT NULL,\n resolution_json TEXT NOT NULL,\n resolved_at TEXT NOT NULL,\n PRIMARY KEY (submission_id, tool_call_id),\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n // Durable attached children (spec §12, SUB-004): a child Submission records its immutable\n // parent linkage at admission; the parent-side index serves the recovery attachment view.\n yield* sql`\n CREATE INDEX effect_agent_submissions_parent\n ON effect_agent_submissions (parent_submission_id)\n `.withoutTransform;\n\n // Parent-owned child budget reservations (spec §12 steps 2 and 6, SUB-010): generic\n // opaque-payload state-machine rows (D8) — allocation and accounting are Schema-encoded\n // JSON documents the adapter never interprets; status moves\n // reserved → releasePending → released, applied exactly once.\n yield* sql`\n CREATE TABLE effect_agent_child_reservations (\n reservation_id TEXT PRIMARY KEY NOT NULL,\n parent_submission_id TEXT NOT NULL,\n parent_tool_call_id TEXT NOT NULL,\n child_submission_id TEXT,\n status TEXT NOT NULL,\n allocation_json TEXT NOT NULL,\n allocation_digest TEXT NOT NULL,\n accounting_json TEXT,\n reserved_at TEXT NOT NULL,\n release_began_at TEXT,\n released_at TEXT,\n UNIQUE (parent_submission_id, parent_tool_call_id),\n FOREIGN KEY (parent_submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n // record_json is authoritative. The remaining columns support owner keyset paging and\n // deadline queries without decoding unrelated future schedules.\n yield* sql`\n CREATE TABLE effect_agent_schedules (\n tenant_id TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n schedule_id TEXT NOT NULL,\n deadline_at_millis INTEGER,\n record_json TEXT NOT NULL,\n PRIMARY KEY (tenant_id, owner_id, schedule_id)\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE INDEX effect_agent_schedules_deadline\n ON effect_agent_schedules (deadline_at_millis, tenant_id, owner_id, schedule_id)\n WHERE deadline_at_millis IS NOT NULL\n `.withoutTransform;\n\n yield* sql`\n CREATE INDEX effect_agent_schedules_owner_deadline\n ON effect_agent_schedules (tenant_id, owner_id, deadline_at_millis, schedule_id)\n WHERE deadline_at_millis IS NOT NULL\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_subscription_sequences (\n tenant_id TEXT NOT NULL,\n source_address TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n event_scan_cursor TEXT NOT NULL,\n delivery_scan_cursor TEXT NOT NULL,\n recovery_scan_cursor INTEGER NOT NULL,\n PRIMARY KEY (tenant_id, source_address)\n )\n `.withoutTransform;\n yield* sql`\n CREATE TABLE effect_agent_subscriptions (\n tenant_id TEXT NOT NULL,\n source_address TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n subscription_id TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n source_name TEXT NOT NULL,\n source_version TEXT NOT NULL,\n matching_key TEXT NOT NULL,\n state TEXT NOT NULL,\n expires_at_millis INTEGER,\n recovery_at_millis INTEGER,\n recovery_present INTEGER NOT NULL DEFAULT 0,\n record_json TEXT NOT NULL,\n PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id),\n UNIQUE (tenant_id, source_address, ordinal)\n )\n `.withoutTransform;\n yield* sql`CREATE INDEX effect_agent_subscriptions_owner ON effect_agent_subscriptions (tenant_id, source_address, owner_id, ordinal)`\n .withoutTransform;\n yield* sql`CREATE INDEX effect_agent_subscriptions_candidates ON effect_agent_subscriptions (tenant_id, source_address, source_name, source_version, matching_key, ordinal)`\n .withoutTransform;\n yield* sql`CREATE INDEX effect_agent_subscriptions_recovery ON effect_agent_subscriptions (tenant_id, source_address, recovery_at_millis, ordinal) WHERE recovery_at_millis IS NOT NULL`\n .withoutTransform;\n yield* sql`\n CREATE TABLE effect_agent_subscription_events (\n tenant_id TEXT NOT NULL,\n source_address TEXT NOT NULL,\n event_id TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_version TEXT NOT NULL,\n matching_key TEXT NOT NULL,\n payload_digest TEXT NOT NULL,\n cutoff INTEGER NOT NULL,\n cursor INTEGER NOT NULL,\n routing_complete INTEGER NOT NULL,\n tombstone INTEGER NOT NULL DEFAULT 0,\n next_attempt_at_millis INTEGER NOT NULL,\n record_json TEXT NOT NULL,\n PRIMARY KEY (tenant_id, source_address, event_id)\n )\n `.withoutTransform;\n yield* sql`CREATE INDEX effect_agent_subscription_events_pending ON effect_agent_subscription_events (tenant_id, source_address, routing_complete, next_attempt_at_millis, event_id)`\n .withoutTransform;\n yield* sql`\n CREATE TABLE effect_agent_subscription_deliveries (\n tenant_id TEXT NOT NULL,\n source_address TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n subscription_id TEXT NOT NULL,\n event_id TEXT NOT NULL,\n delivery_key TEXT NOT NULL,\n state TEXT NOT NULL,\n next_attempt_at_millis INTEGER NOT NULL,\n record_json TEXT NOT NULL,\n PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id, event_id),\n UNIQUE (tenant_id, source_address, delivery_key)\n )\n `.withoutTransform;\n yield* sql`CREATE INDEX effect_agent_subscription_deliveries_pending ON effect_agent_subscription_deliveries (tenant_id, source_address, state, next_attempt_at_millis, delivery_key)`\n .withoutTransform;\n yield* sql`CREATE INDEX effect_agent_subscription_deliveries_registration ON effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, delivery_key)`\n .withoutTransform;\n yield* createMessageDeliveryTables;\n yield* createRecoveryCheckpointTable;\n yield* sql`PRAGMA user_version = 10`.withoutTransform;\n }),\n});\n"],"mappings":";;;;;AAIA,MAAa,8BAA8B,OAAO,IAAI,aAAa;CACjE,MAAM,MAAM,OAAO,UAAU;CAE7B,OAAO,GAAG;;;;;;;;;;IAUR;CACF,OAAO,GAAG;;;;IAIR;AACJ,CAAC;;;;ACnBD,MAAa,gCAAgC,OAAO,IAAI,aAAa;CAGnE,OAAO,CAAA,OAFY,UAAU,UAAA,AAEnB;;;;;;;;IAQR;AACJ,CAAC;;;ACTD,MAAa,8BAA8B;;AAG3C,MAAa,mBAAmB,eAAe,WAAW,EACxD,4BAA4B,OAAO,IAAI,aAAa;CAClD,MAAM,MAAM,OAAO,UAAU;CAE7B,OAAO,GAAG;;;;;;;;MAQR;CAEF,OAAO,GAAG;;;;;;;;;;;;;;MAcR;CAEF,OAAO,GAAG;;;;;;;;;;;;;MAaR;CAEF,OAAO,GAAG;;;MAGR;CAEF,OAAO,GAAG;;;;;;;;;;;MAWR;CAIF,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAiCR;CAEF,OAAO,GAAG;;;MAGR;CAEF,OAAO,GAAG;;;;;;;;;;;;MAYR;CAEF,OAAO,GAAG;;;;;;;;;;;;MAYR;CAEF,OAAO,GAAG;;;;;;;;;;;;;;MAcR;CAEF,OAAO,GAAG;;;;;;;;;;;MAWR;CAEF,OAAO,GAAG;;;MAGR;CAEF,OAAO,GAAG;;;;;;;;;;;;;MAaR;CAEF,OAAO,GAAG;;;;;;;;;;;;;MAaR;CAIF,OAAO,GAAG;;;MAGR;CAMF,OAAO,GAAG;;;;;;;;;;;;;;;;;;MAkBR;CAIF,OAAO,GAAG;;;;;;;;;MASR;CAEF,OAAO,GAAG;;;;MAIR;CAEF,OAAO,GAAG;;;;MAIR;CAEF,OAAO,GAAG;;;;;;;;;;MAUR;CACF,OAAO,GAAG;;;;;;;;;;;;;;;;;;MAkBR;CACF,OAAO,GAAG,6HACP;CACH,OAAO,GAAG,mKACP;CACH,OAAO,GAAG,+KACP;CACH,OAAO,GAAG;;;;;;;;;;;;;;;;;MAiBR;CACF,OAAO,GAAG,4KACP;CACH,OAAO,GAAG;;;;;;;;;;;;;;MAcR;CACF,OAAO,GAAG,6KACP;CACH,OAAO,GAAG,8KACP;CACH,OAAO;CACP,OAAO;CACP,OAAO,GAAG,2BAA2B;AACvC,CAAC,EACH,CAAC"}