@effect-agent/storage-sqlite 0.1.0-beta.114 → 0.1.0-beta.116

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.
@@ -16,6 +16,9 @@ import {
16
16
  } from "effect-agent/records";
17
17
  import {
18
18
  AbortCommand,
19
+ WorkerStopCommand,
20
+ WorkerLedgerState,
21
+ workerTerminalFromRecord,
19
22
  AbortIntent,
20
23
  AbortIntentRequest,
21
24
  AdmissionAdmitted,
@@ -1146,6 +1149,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1146
1149
  }).pipe(Effect.mapError(internalFailure(operation)));
1147
1150
  }
1148
1151
 
1152
+ const stopped =
1153
+ yield* sql`SELECT thread_id FROM effect_agent_worker_stops WHERE thread_id = ${validated.threadId}`.pipe(
1154
+ Effect.mapError(sqlFailure(operation)),
1155
+ );
1156
+
1157
+ if (stopped.length > 0)
1158
+ return yield* AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" });
1159
+
1149
1160
  // The first accepted input fixes ordinary/worker lane identity atomically with admission.
1150
1161
  // Canonical origin materialization can lag admission; a log scan cannot fence that race.
1151
1162
  const firstRows = yield* sql<Record<string, unknown>>`
@@ -1883,7 +1894,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1883
1894
  );
1884
1895
  }
1885
1896
 
1886
- return { reservation: reservation.value, settlementFailure };
1897
+ return { reservation: reservation.value, reservationRecord, settlementFailure };
1887
1898
  });
1888
1899
 
1889
1900
  const replayFinalization = Effect.fn("SqliteSubmissionLedger.replayFinalization")(function* (
@@ -1981,13 +1992,49 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1981
1992
  yield* readReservation(operation, validated.submissionId),
1982
1993
  );
1983
1994
 
1984
- const { reservation, settlementFailure } = state;
1995
+ const { reservation, reservationRecord, settlementFailure } = state;
1985
1996
  const submission = yield* requireSubmission(operation, validated.submissionId);
1986
1997
 
1987
1998
  if (submission.state === "settled")
1988
1999
  return yield* replayFinalization(validated, submission, state);
1989
2000
  const now = yield* currentInstant;
1990
2001
 
2002
+ const terminal =
2003
+ submission.worker_admission_json === null
2004
+ ? undefined
2005
+ : workerTerminalFromRecord(
2006
+ yield* decodeSubmissionSnapshot(operation, submission),
2007
+ reservationRecord,
2008
+ );
2009
+
2010
+ if (terminal !== undefined) {
2011
+ // Admission and finalization serialize here. An accepted correction that this Run
2012
+ // has not applied vetoes its completion, including admission after RunCompleted.
2013
+ const pending =
2014
+ terminal === "completed"
2015
+ ? yield* sql`SELECT submission_id FROM effect_agent_submissions
2016
+ WHERE thread_id = ${submission.thread_id} AND queue_sequence > ${submission.queue_sequence}
2017
+ AND queue_sequence = (SELECT MAX(queue_sequence) FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id})
2018
+ AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
2019
+ OR input_applied_record_id IS NULL) LIMIT 1`.pipe(
2020
+ Effect.mapError(sqlFailure(operation)),
2021
+ )
2022
+ : [];
2023
+
2024
+ if (pending.length === 0) {
2025
+ yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id, terminal)
2026
+ VALUES (${submission.thread_id}, ${terminal})`.pipe(
2027
+ Effect.mapError(sqlFailure(operation)),
2028
+ );
2029
+ yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
2030
+ SELECT submission_id, ${submission.principal}, ${`Worker assignment ${terminal}`}, ${now.iso}
2031
+ FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id} AND state <> 'settled'
2032
+ AND submission_id <> ${submission.submission_id}
2033
+ AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
2034
+ OR input_applied_record_id IS NULL)`.pipe(Effect.mapError(sqlFailure(operation)));
2035
+ }
2036
+ }
2037
+
1991
2038
  yield* sql`
1992
2039
  UPDATE effect_agent_submissions
1993
2040
  SET state = 'settled', settled_outcome = ${reservation.outcome}
@@ -2019,6 +2066,84 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2019
2066
  return settlement;
2020
2067
  });
2021
2068
 
2069
+ const inspectWorker = Effect.fn("SqliteSubmissionLedger.inspectWorker")(function* (
2070
+ threadId: SubmissionSnapshot["threadId"],
2071
+ ) {
2072
+ const operation = "inspect worker";
2073
+
2074
+ return yield* sql
2075
+ .withTransaction(
2076
+ Effect.gen(function* () {
2077
+ const read = Effect.fnUntraced(function* (active: boolean) {
2078
+ const rows =
2079
+ yield* sql`SELECT ${sql.literal(SUBMISSION_COLUMNS)} FROM effect_agent_submissions
2080
+ WHERE thread_id = ${threadId} ${active ? sql`AND state <> 'settled'` : sql``}
2081
+ ORDER BY queue_sequence ${active ? sql`ASC` : sql`DESC`} LIMIT 1`.pipe(
2082
+ Effect.mapError(sqlFailure(operation)),
2083
+ );
2084
+
2085
+ const decoded = yield* decodeSubmissionRows(operation, threadId, rows);
2086
+
2087
+ return decoded[0] === undefined
2088
+ ? null
2089
+ : yield* decodeSubmissionSnapshot(operation, decoded[0]);
2090
+ });
2091
+
2092
+ const latest = yield* read(false);
2093
+ const active = yield* read(true);
2094
+
2095
+ const stops =
2096
+ yield* sql`SELECT terminal FROM effect_agent_worker_stops WHERE thread_id = ${threadId}`.pipe(
2097
+ Effect.mapError(sqlFailure(operation)),
2098
+ );
2099
+
2100
+ return yield* Schema.decodeUnknownEffect(Schema.toType(WorkerLedgerState))({
2101
+ latest,
2102
+ active,
2103
+ stopped: stops.length > 0,
2104
+ ...(stops[0] === undefined || stops[0].terminal === null
2105
+ ? {}
2106
+ : { terminal: stops[0].terminal }),
2107
+ }).pipe(Effect.mapError(internalFailure(operation)));
2108
+ }),
2109
+ )
2110
+ .pipe(Effect.catchTag("SqlError", (cause) => sqlFailure(operation)(cause)));
2111
+ });
2112
+
2113
+ const stopWorker = Effect.fn("SqliteSubmissionLedger.stopWorker")(function* (
2114
+ request: WorkerStopCommand,
2115
+ ) {
2116
+ const operation = "ledger stop worker";
2117
+
2118
+ const validated = yield* Schema.decodeEffect(WorkerStopCommand)(request).pipe(
2119
+ Effect.mapError(internalFailure(operation)),
2120
+ );
2121
+
2122
+ return yield* inWriteTransaction(
2123
+ operation,
2124
+ Effect.gen(function* () {
2125
+ const now = yield* currentInstant;
2126
+
2127
+ yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id) VALUES (${validated.threadId})`.pipe(
2128
+ Effect.mapError(sqlFailure(operation)),
2129
+ );
2130
+ yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
2131
+ SELECT submission_id, ${validated.author}, 'Worker owner stopped the worker', ${now.iso}
2132
+ FROM effect_agent_submissions WHERE thread_id = ${validated.threadId} AND state <> 'settled'`.pipe(
2133
+ Effect.mapError(sqlFailure(operation)),
2134
+ );
2135
+
2136
+ const rows = yield* sql`SELECT o.submission_id FROM effect_agent_submission_ownership o
2137
+ JOIN effect_agent_submissions s ON s.submission_id = o.submission_id
2138
+ WHERE s.thread_id = ${validated.threadId} AND s.state <> 'settled'`.pipe(
2139
+ Effect.mapError(sqlFailure(operation)),
2140
+ );
2141
+
2142
+ return rows.length;
2143
+ }),
2144
+ );
2145
+ });
2146
+
2022
2147
  const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fn(
2023
2148
  "SqliteSubmissionLedger.requestAbort",
2024
2149
  )(function* (request: AbortCommand) {
@@ -2144,6 +2269,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2144
2269
  // The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
2145
2270
  yield* requireOwnership(operation, host, validated.ownershipToken);
2146
2271
 
2272
+ const stopped =
2273
+ yield* sql`SELECT thread_id FROM effect_agent_worker_stops WHERE thread_id = ${validated.threadId}`.pipe(
2274
+ Effect.mapError(sqlFailure(operation)),
2275
+ );
2276
+
2277
+ if (stopped.length > 0) return [];
2278
+
2147
2279
  const laterRows = yield* sql<Record<string, unknown>>`
2148
2280
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2149
2281
  FROM effect_agent_submissions
@@ -3496,6 +3628,8 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
3496
3628
  reserveSettlement,
3497
3629
  finalizeSettlement,
3498
3630
  requestAbort,
3631
+ stopWorker,
3632
+ inspectWorker,
3499
3633
  claimJoining,
3500
3634
  markJoined,
3501
3635
  revertJoining,
@@ -7,7 +7,23 @@ import * as SqlClient from "effect/unstable/sql/SqlClient";
7
7
  import { createMessageDeliveryTables } from "./message-delivery-schema.ts";
8
8
  import { createRecoveryCheckpointTable } from "./recovery-checkpoint-schema.ts";
9
9
 
10
- export const CurrentSqliteStorageVersion = 12;
10
+ export const CurrentSqliteStorageVersion = 14;
11
+
12
+ /** One permanent destination inbox fence, including workers stopped before admission. */
13
+ export const createWorkerStops = Effect.gen(function* () {
14
+ const sql = yield* SqlClient.SqlClient;
15
+
16
+ yield* sql`CREATE TABLE effect_agent_worker_stops (thread_id TEXT PRIMARY KEY NOT NULL, terminal TEXT)`;
17
+ yield* sql`CREATE INDEX effect_agent_worker_starts ON effect_agent_message_deliveries(owner_thread_id,
18
+ json_extract(record_json, '$.envelope.workerAdmission.origin.worker.delegationId'),
19
+ json_extract(record_json, '$.envelope.workerAdmission.origin.worker.targetAgentId'), message_id)
20
+ WHERE message_id = json_extract(record_json, '$.envelope.workerAdmission.origin.firstMessageId')`;
21
+ yield* sql`CREATE INDEX effect_agent_worker_pending ON effect_agent_message_deliveries(owner_thread_id,
22
+ json_extract(record_json, '$.envelope.workerAdmission.origin.worker.threadId'), message_id)
23
+ WHERE state IN ('pending', 'parked') AND json_extract(record_json, '$.receipt') IS NULL`;
24
+ yield* sql`CREATE INDEX effect_agent_worker_execution ON effect_agent_canonical_records(thread_id,
25
+ json_extract(record_json, '$.payload._tag'), sequence) WHERE json_extract(record_json, '$.payload.runId') IS NOT NULL`;
26
+ });
11
27
 
12
28
  /** Index only outstanding obligations, ordered by the recovery scan's stable cursor. */
13
29
  export const createNonterminalIndex = Effect.gen(function* () {
@@ -351,6 +367,7 @@ export const sqliteMigrations = SqliteMigrator.fromRecord({
351
367
  yield* createMessageDeliveryTables;
352
368
  yield* createMessageDeliveryPendingIndex;
353
369
  yield* createRecoveryCheckpointTable;
354
- yield* sql`PRAGMA user_version = 12`.withoutTransform;
370
+ yield* createWorkerStops;
371
+ yield* sql`PRAGMA user_version = 14`.withoutTransform;
355
372
  }),
356
373
  });
@@ -42,6 +42,7 @@ import { SqliteStorageFailpoint } from "../SqliteStorageFailpoint.ts";
42
42
  import { createMessageDeliveryTables } from "./message-delivery-schema.ts";
43
43
  import {
44
44
  CurrentSqliteStorageVersion,
45
+ createWorkerStops,
45
46
  createNonterminalIndex,
46
47
  sqliteMigrations,
47
48
  } from "./migrations.ts";
@@ -388,7 +389,7 @@ const predecessorColumns = {
388
389
  } as const;
389
390
 
390
391
  const checkPredecessorLayout = Effect.fn("SqliteJournal.checkPredecessorLayout")(function* (
391
- version: 8 | 9 | 10,
392
+ version: 8 | 9 | 10 | 12,
392
393
  ) {
393
394
  const sql = yield* SqlClient.SqlClient;
394
395
 
@@ -414,7 +415,15 @@ const checkPredecessorLayout = Effect.fn("SqliteJournal.checkPredecessorLayout")
414
415
 
415
416
  const expectedColumns = {
416
417
  ...messageColumns,
417
- ...(version === 10
418
+ ...(version === 12
419
+ ? {
420
+ effect_agent_canonical_records: [
421
+ ...predecessorColumns.effect_agent_canonical_records,
422
+ "outstanding",
423
+ ],
424
+ }
425
+ : {}),
426
+ ...(version >= 10
418
427
  ? {
419
428
  effect_agent_recovery_checkpoints: [
420
429
  "thread_id",
@@ -487,6 +496,48 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
487
496
  versionRows,
488
497
  );
489
498
 
499
+ const verifyWorkerPredecessor = Effect.fnUntraced(function* (workerContract: boolean) {
500
+ const requiredRows = yield* sql<Record<string, unknown>>`
501
+ SELECT name
502
+ FROM sqlite_master
503
+ WHERE (type = 'table'
504
+ AND name IN (
505
+ 'effect_agent_threads',
506
+ 'effect_agent_canonical_batches',
507
+ 'effect_agent_canonical_records',
508
+ 'effect_agent_checkpoints',
509
+ 'effect_agent_submissions',
510
+ 'effect_agent_submission_ownership',
511
+ 'effect_agent_attempts',
512
+ 'effect_agent_settlement_reservations',
513
+ 'effect_agent_abort_intents',
514
+ 'effect_agent_approval_decisions',
515
+ 'effect_agent_unknown_resolutions',
516
+ 'effect_agent_schedules',
517
+ 'effect_agent_message_deliveries',
518
+ 'effect_agent_recovery_checkpoints'
519
+ )) OR (type = 'index' AND name IN ('effect_agent_submissions_nonterminal', 'effect_agent_records_subtree', 'effect_agent_message_deliveries_pending', 'effect_agent_records_outstanding', 'effect_agent_records_call', 'effect_agent_records_run_input', 'effect_agent_records_worker_input'))
520
+ OR (${workerContract ? 1 : 0} = 1 AND name IN ('effect_agent_worker_stops', 'effect_agent_worker_starts', 'effect_agent_worker_pending', 'effect_agent_worker_execution'))
521
+ ORDER BY name
522
+ `.pipe(Effect.mapError(storageError("verify storage tables")));
523
+
524
+ const required = yield* decodeRows(
525
+ Schema.Array(SqliteNameRow),
526
+ "sqlite_master",
527
+ "required_tables",
528
+ requiredRows,
529
+ );
530
+
531
+ if (required.length !== 21 + (workerContract ? 4 : 0)) {
532
+ return yield* SqliteStorageCompatibilityError.make({
533
+ actualVersion: CurrentSqliteStorageVersion,
534
+ supportedVersion: CurrentSqliteStorageVersion,
535
+ message:
536
+ "The SQLite file claims the current format but is missing required tables or its nonterminal index. Retain the original store for inspection.",
537
+ });
538
+ }
539
+ });
540
+
490
541
  // Support the known beta49/beta50 and immediate predecessor formats atomically.
491
542
  if (
492
543
  version.user_version !== 0 &&
@@ -495,6 +546,8 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
495
546
  version.user_version !== 9 &&
496
547
  version.user_version !== 10 &&
497
548
  version.user_version !== 11 &&
549
+ version.user_version !== 12 &&
550
+ version.user_version !== 13 &&
498
551
  version.user_version !== CurrentSqliteStorageVersion
499
552
  ) {
500
553
  return yield* SqliteStorageCompatibilityError.make({
@@ -503,7 +556,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
503
556
  message:
504
557
  `The SQLite file uses unsupported storage version ${version.user_version}; ` +
505
558
  `this build supports exactly version ${CurrentSqliteStorageVersion}. ` +
506
- "Only supported v7, v8, v9, v10 and v11 can be upgraded automatically. Keep the original file and use a compatible library version.",
559
+ "Only supported v7, v8, v9, v10, v11, v12 and v13 can be upgraded automatically. Keep the original file and use a compatible library version.",
507
560
  });
508
561
  }
509
562
 
@@ -638,7 +691,8 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
638
691
  }),
639
692
  ),
640
693
  );
641
- yield* sql`PRAGMA user_version = 12`;
694
+ yield* createWorkerStops;
695
+ yield* sql`PRAGMA user_version = 14`;
642
696
  yield* failpoint("upgrade:after-version");
643
697
  }),
644
698
  )
@@ -718,11 +772,11 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
718
772
  Effect.gen(function* () {
719
773
  const current = yield* sql<{ user_version: number }>`PRAGMA user_version`;
720
774
 
721
- if (current[0]?.user_version === 12) return;
775
+ if (current[0]?.user_version === 14) return;
722
776
  if (current[0]?.user_version !== 11)
723
777
  return yield* SqliteStorageCompatibilityError.make({
724
778
  actualVersion: current[0]?.user_version ?? -1,
725
- supportedVersion: 12,
779
+ supportedVersion: 14,
726
780
  message: "Storage version changed during native index upgrade",
727
781
  });
728
782
  yield* checkPredecessorLayout(10);
@@ -733,7 +787,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
733
787
  if (requiredIndex.length !== 1)
734
788
  return yield* SqliteStorageCompatibilityError.make({
735
789
  actualVersion: 11,
736
- supportedVersion: 12,
790
+ supportedVersion: 14,
737
791
  message: "Predecessor storage is missing its required nonterminal index",
738
792
  });
739
793
  yield* failpoint("upgrade:before-mutation");
@@ -750,7 +804,8 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
750
804
  );
751
805
  yield* failpoint("upgrade:after-mutation");
752
806
  yield* failpoint("upgrade:before-version");
753
- yield* sql`PRAGMA user_version = 12`;
807
+ yield* createWorkerStops;
808
+ yield* sql`PRAGMA user_version = 14`;
754
809
  yield* failpoint("upgrade:after-version");
755
810
  }),
756
811
  )
@@ -765,45 +820,98 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
765
820
  );
766
821
  }
767
822
 
768
- const requiredRows = yield* sql<Record<string, unknown>>`
769
- SELECT name
770
- FROM sqlite_master
771
- WHERE (type = 'table'
772
- AND name IN (
773
- 'effect_agent_threads',
774
- 'effect_agent_canonical_batches',
775
- 'effect_agent_canonical_records',
776
- 'effect_agent_checkpoints',
777
- 'effect_agent_submissions',
778
- 'effect_agent_submission_ownership',
779
- 'effect_agent_attempts',
780
- 'effect_agent_settlement_reservations',
781
- 'effect_agent_abort_intents',
782
- 'effect_agent_approval_decisions',
783
- 'effect_agent_unknown_resolutions',
784
- 'effect_agent_schedules',
785
- 'effect_agent_message_deliveries',
786
- 'effect_agent_recovery_checkpoints'
787
- )) OR (type = 'index' AND name IN ('effect_agent_submissions_nonterminal', 'effect_agent_records_subtree', 'effect_agent_message_deliveries_pending', 'effect_agent_records_outstanding', 'effect_agent_records_call', 'effect_agent_records_run_input', 'effect_agent_records_worker_input'))
788
- ORDER BY name
789
- `.pipe(Effect.mapError(storageError("verify storage tables")));
823
+ if (version.user_version === 12) {
824
+ yield* sql
825
+ .withTransaction(
826
+ Effect.gen(function* () {
827
+ const current = yield* sql<{ user_version: number }>`PRAGMA user_version`;
790
828
 
791
- const required = yield* decodeRows(
792
- Schema.Array(SqliteNameRow),
793
- "sqlite_master",
794
- "required_tables",
795
- requiredRows,
796
- );
829
+ if (current[0]?.user_version === 14) return;
830
+ if (current[0]?.user_version !== 12)
831
+ return yield* SqliteStorageCompatibilityError.make({
832
+ actualVersion: current[0]?.user_version ?? -1,
833
+ supportedVersion: 14,
834
+ message: "Storage version changed during worker stop upgrade",
835
+ });
836
+ yield* checkPredecessorLayout(12);
837
+ yield* verifyWorkerPredecessor(false);
838
+ yield* failpoint("upgrade:before-mutation");
839
+ yield* createWorkerStops;
840
+ yield* failpoint("upgrade:after-mutation");
841
+ yield* failpoint("upgrade:before-version");
842
+ yield* sql`PRAGMA user_version = 14`;
843
+ yield* failpoint("upgrade:after-version");
844
+ }),
845
+ )
846
+ .pipe(
847
+ Effect.mapError((cause) =>
848
+ SqliteStorageError.make({
849
+ operation: "upgrade worker stop",
850
+ message: "Worker stop upgrade failed",
851
+ cause,
852
+ }),
853
+ ),
854
+ );
855
+ }
797
856
 
798
- if (required.length !== 21) {
799
- return yield* SqliteStorageCompatibilityError.make({
800
- actualVersion: CurrentSqliteStorageVersion,
801
- supportedVersion: CurrentSqliteStorageVersion,
802
- message:
803
- "The SQLite file claims the current format but is missing required tables or its nonterminal index. Retain the original store for inspection.",
804
- });
857
+ if (version.user_version === 13) {
858
+ yield* sql
859
+ .withTransaction(
860
+ Effect.gen(function* () {
861
+ const current = yield* sql<{ user_version: number }>`PRAGMA user_version`;
862
+
863
+ if (current[0]?.user_version === 14) return;
864
+ if (current[0]?.user_version !== 13)
865
+ return yield* SqliteStorageCompatibilityError.make({
866
+ actualVersion: current[0]?.user_version ?? -1,
867
+ supportedVersion: 14,
868
+ message: "Storage version changed during assignment seal upgrade",
869
+ });
870
+ yield* checkPredecessorLayout(12);
871
+ yield* verifyWorkerPredecessor(true);
872
+ const columns = yield* sql`PRAGMA table_info(effect_agent_worker_stops)`;
873
+
874
+ yield* Schema.decodeUnknownEffect(
875
+ Schema.Tuple([
876
+ Schema.Struct({
877
+ cid: Schema.Literal(0),
878
+ name: Schema.Literal("thread_id"),
879
+ type: Schema.Literal("TEXT"),
880
+ notnull: Schema.Literal(1),
881
+ dflt_value: Schema.Null,
882
+ pk: Schema.Literal(1),
883
+ }),
884
+ ]),
885
+ )(columns).pipe(
886
+ Effect.mapError(() =>
887
+ SqliteStorageCompatibilityError.make({
888
+ actualVersion: 13,
889
+ supportedVersion: 14,
890
+ message: "Unsupported worker seal layout; no upgrade was committed",
891
+ }),
892
+ ),
893
+ );
894
+ yield* failpoint("upgrade:before-mutation");
895
+ yield* sql`ALTER TABLE effect_agent_worker_stops ADD COLUMN terminal TEXT`;
896
+ yield* failpoint("upgrade:after-mutation");
897
+ yield* failpoint("upgrade:before-version");
898
+ yield* sql`PRAGMA user_version = 14`;
899
+ yield* failpoint("upgrade:after-version");
900
+ }),
901
+ )
902
+ .pipe(
903
+ Effect.mapError((cause) =>
904
+ SqliteStorageError.make({
905
+ operation: "upgrade assignment seals",
906
+ message: "Assignment seal upgrade failed",
907
+ cause,
908
+ }),
909
+ ),
910
+ );
805
911
  }
806
912
 
913
+ yield* verifyWorkerPredecessor(true);
914
+
807
915
  const classifyWriteFailure =
808
916
  (operation: string) =>
809
917
  (error: SqlError): SqliteStorageError | SqliteWriteContention =>
@@ -1 +0,0 @@
1
- {"version":3,"file":"migrations-BZ89iaTY.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 { createMessageDeliveryPendingIndex } from \"effect-agent/sql-message-delivery-store\";\nimport { createNativeReadIndexes } from \"effect-agent/sql-thread-native-reads\";\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 = 12;\n\n/** Index only outstanding obligations, ordered by the recovery scan's stable cursor. */\nexport const createNonterminalIndex = Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n\n yield* sql`CREATE INDEX effect_agent_submissions_nonterminal ON effect_agent_submissions (thread_id, queue_sequence) WHERE state <> 'settled'`\n .withoutTransform;\n});\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* createNativeReadIndexes;\n yield* createNonterminalIndex;\n yield* createMessageDeliveryTables;\n yield* createMessageDeliveryPendingIndex;\n yield* createRecoveryCheckpointTable;\n yield* sql`PRAGMA user_version = 12`.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;;;ACPD,MAAa,8BAA8B;;AAG3C,MAAa,yBAAyB,OAAO,IAAI,aAAa;CAG5D,OAAO,CAAA,OAFY,UAAU,UAAA,AAEnB,qIACP;AACL,CAAC;;AAGD,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;CACP,OAAO;CACP,OAAO;CACP,OAAO,GAAG,2BAA2B;AACvC,CAAC,EACH,CAAC"}