@zq-silk/yui 0.6.4 → 0.6.6
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/README.md +11 -3
- package/dist/cli/updateOrchestrator.js +12 -6
- package/dist/cli.js +12 -0
- package/dist/controller/agentRuntimeObserver.js +76 -16
- package/dist/controller/controller.js +398 -76
- package/dist/controller/fileSchedulerStoreAdapter.js +60 -51
- package/dist/controller/jobSupervisor.js +2 -16
- package/dist/controller/resourceInventoryLinux.js +73 -20
- package/dist/controller/runtime.js +1 -0
- package/dist/controller/runtimeEventProcessor.js +171 -31
- package/dist/coordination/keyedWorkQueue.js +189 -0
- package/dist/runtime/runtimeSessionCandidate.js +43 -0
- package/dist/scheduler/activeRoleRunDelivery.js +2 -4
- package/dist/scheduler/activeTaskProgress.js +2 -4
- package/dist/scheduler/leaderWakeupProcessor.js +3 -1
- package/dist/scheduler/ports.js +35 -2
- package/dist/scheduler/roleRunLiveness.js +44 -22
- package/dist/scheduler/roleRunStall.js +49 -25
- package/dist/storage/sqliteSchema.js +363 -14
- package/dist/storage/sqliteStore.js +403 -30
- package/dist/storage/storeRpc.js +5 -0
- package/dist/storage/taskStore.js +104 -6
- package/i18n/README.zh-CN.md +7 -1
- package/package.json +1 -1
|
@@ -27,7 +27,7 @@ export const SQLITE_LAYOUT_VERSION = 7;
|
|
|
27
27
|
/** The aggregate version of the normalized SQLite schema. */
|
|
28
28
|
export const SQLITE_AGGREGATE_VERSION = 1;
|
|
29
29
|
/** The current schema migration version. */
|
|
30
|
-
export const SQLITE_SCHEMA_VERSION =
|
|
30
|
+
export const SQLITE_SCHEMA_VERSION = 13;
|
|
31
31
|
/** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
|
|
32
32
|
export const TELEMETRY_KEEP_PER_GENERATION = 200;
|
|
33
33
|
export const TELEMETRY_RUN_CAP = 50_000;
|
|
@@ -608,6 +608,170 @@ CREATE TABLE IF NOT EXISTS gate_artifact_logs (
|
|
|
608
608
|
FOREIGN KEY (artifact_key) REFERENCES gate_artifacts(key) ON DELETE CASCADE
|
|
609
609
|
);
|
|
610
610
|
`;
|
|
611
|
+
/**
|
|
612
|
+
* Migration 10: bounded ready-mailbox lookup for the Controller hot path.
|
|
613
|
+
*
|
|
614
|
+
* Historical empty mailboxes remain durable, but only mailboxes with a
|
|
615
|
+
* processing or pending batch require scheduling. The partial index contains
|
|
616
|
+
* exactly that unsettled set in target-key order, so the Controller's recovery
|
|
617
|
+
* query is O(log H + ready) instead of scanning every historical mailbox.
|
|
618
|
+
*/
|
|
619
|
+
const MIGRATION_10_SQL = `
|
|
620
|
+
CREATE INDEX IF NOT EXISTS idx_mailboxes_ready
|
|
621
|
+
ON mailboxes(target_key)
|
|
622
|
+
WHERE processing IS NOT NULL OR pending IS NOT NULL;
|
|
623
|
+
`;
|
|
624
|
+
/**
|
|
625
|
+
* Migration 11: bounded current-Session projection for runtime cleanup.
|
|
626
|
+
*
|
|
627
|
+
* RoleSessionSet payloads remain authoritative. This table contains only the
|
|
628
|
+
* current active Agent Session while it is non-stopped, so terminal Session
|
|
629
|
+
* history cannot enlarge Controller cleanup discovery. Runtime writes maintain
|
|
630
|
+
* it in the same transaction as the source payload; this migration performs
|
|
631
|
+
* the one-time deterministic backfill for existing layout-7 Homes.
|
|
632
|
+
*/
|
|
633
|
+
const MIGRATION_11_SQL = `
|
|
634
|
+
CREATE TABLE IF NOT EXISTS runtime_session_candidates (
|
|
635
|
+
scope TEXT NOT NULL CHECK (scope IN ('task','global')),
|
|
636
|
+
task_id TEXT NOT NULL,
|
|
637
|
+
role_name TEXT NOT NULL,
|
|
638
|
+
agent_id TEXT NOT NULL,
|
|
639
|
+
adapter_id TEXT NOT NULL,
|
|
640
|
+
native_session_id TEXT NOT NULL,
|
|
641
|
+
launch_id TEXT,
|
|
642
|
+
status TEXT NOT NULL CHECK (status IN ('reserved','ready','running','broken')),
|
|
643
|
+
session_updated_at TEXT NOT NULL,
|
|
644
|
+
cleanup_required INTEGER NOT NULL CHECK (cleanup_required IN (0,1)),
|
|
645
|
+
PRIMARY KEY (scope, task_id, role_name),
|
|
646
|
+
CHECK (
|
|
647
|
+
(scope = 'task' AND length(task_id) > 0)
|
|
648
|
+
OR (scope = 'global' AND task_id = '')
|
|
649
|
+
),
|
|
650
|
+
CHECK (
|
|
651
|
+
cleanup_required = CASE
|
|
652
|
+
WHEN status NOT IN ('stopped','broken')
|
|
653
|
+
AND (status = 'running' OR launch_id IS NOT NULL)
|
|
654
|
+
THEN 1 ELSE 0
|
|
655
|
+
END
|
|
656
|
+
)
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
CREATE INDEX IF NOT EXISTS idx_runtime_session_cleanup_required
|
|
660
|
+
ON runtime_session_candidates(scope, task_id, role_name)
|
|
661
|
+
WHERE cleanup_required = 1;
|
|
662
|
+
|
|
663
|
+
INSERT INTO runtime_session_candidates (
|
|
664
|
+
scope, task_id, role_name, agent_id, adapter_id, native_session_id,
|
|
665
|
+
launch_id, status, session_updated_at, cleanup_required
|
|
666
|
+
)
|
|
667
|
+
SELECT
|
|
668
|
+
'task', source.task_id, source.role_name,
|
|
669
|
+
json_extract(active.value, '$.agentId'),
|
|
670
|
+
json_extract(active.value, '$.adapterId'),
|
|
671
|
+
json_extract(active.value, '$.nativeSessionId'),
|
|
672
|
+
json_extract(active.value, '$.launchId'),
|
|
673
|
+
json_extract(active.value, '$.status'),
|
|
674
|
+
json_extract(active.value, '$.updatedAt'),
|
|
675
|
+
CASE
|
|
676
|
+
WHEN json_extract(active.value, '$.status') NOT IN ('stopped','broken')
|
|
677
|
+
AND (
|
|
678
|
+
json_extract(active.value, '$.status') = 'running'
|
|
679
|
+
OR json_type(active.value, '$.launchId') = 'text'
|
|
680
|
+
)
|
|
681
|
+
THEN 1 ELSE 0
|
|
682
|
+
END
|
|
683
|
+
FROM role_session_sets AS source
|
|
684
|
+
JOIN json_each(source.payload, '$.sessions') AS active
|
|
685
|
+
ON active.key = json_extract(source.payload, '$.activeAgentId')
|
|
686
|
+
WHERE json_extract(active.value, '$.status') <> 'stopped';
|
|
687
|
+
|
|
688
|
+
INSERT INTO runtime_session_candidates (
|
|
689
|
+
scope, task_id, role_name, agent_id, adapter_id, native_session_id,
|
|
690
|
+
launch_id, status, session_updated_at, cleanup_required
|
|
691
|
+
)
|
|
692
|
+
SELECT
|
|
693
|
+
'global', '', source.name,
|
|
694
|
+
json_extract(active.value, '$.agentId'),
|
|
695
|
+
json_extract(active.value, '$.adapterId'),
|
|
696
|
+
json_extract(active.value, '$.nativeSessionId'),
|
|
697
|
+
json_extract(active.value, '$.launchId'),
|
|
698
|
+
json_extract(active.value, '$.status'),
|
|
699
|
+
json_extract(active.value, '$.updatedAt'),
|
|
700
|
+
CASE
|
|
701
|
+
WHEN json_extract(active.value, '$.status') NOT IN ('stopped','broken')
|
|
702
|
+
AND (
|
|
703
|
+
json_extract(active.value, '$.status') = 'running'
|
|
704
|
+
OR json_type(active.value, '$.launchId') = 'text'
|
|
705
|
+
)
|
|
706
|
+
THEN 1 ELSE 0
|
|
707
|
+
END
|
|
708
|
+
FROM global_role_session_sets AS source
|
|
709
|
+
JOIN json_each(source.payload, '$.sessions') AS active
|
|
710
|
+
ON active.key = json_extract(source.payload, '$.activeAgentId')
|
|
711
|
+
WHERE json_extract(active.value, '$.status') <> 'stopped';
|
|
712
|
+
`;
|
|
713
|
+
/**
|
|
714
|
+
* Migration 12: bounded open-InputRequest lookup for Controller deadlines.
|
|
715
|
+
*
|
|
716
|
+
* The baseline index's historical predicate predates the current
|
|
717
|
+
* open/answered/cancelled status contract and retains terminal rows. This
|
|
718
|
+
* partial index contains only the live InputRequest set, so deadline arming and
|
|
719
|
+
* targeted auto-resolution never scan terminal request history.
|
|
720
|
+
*/
|
|
721
|
+
const MIGRATION_12_SQL = `
|
|
722
|
+
CREATE INDEX IF NOT EXISTS idx_input_requests_open_hot
|
|
723
|
+
ON input_requests(task_id, input_id)
|
|
724
|
+
WHERE status = 'open';
|
|
725
|
+
`;
|
|
726
|
+
/**
|
|
727
|
+
* Migration 13: durable pending native Turn-completion hot projection.
|
|
728
|
+
*
|
|
729
|
+
* A Task Role may have an unsettled Turn completion after the native Session
|
|
730
|
+
* has ended. That completion is independent lifecycle state, so it cannot be
|
|
731
|
+
* discovered through the current-Session projection (which intentionally
|
|
732
|
+
* removes stopped Sessions). Keep one typed row per Task Role and maintain it
|
|
733
|
+
* in the same transaction as `role_session_sets`; deadline discovery can then
|
|
734
|
+
* read this table without parsing a RoleSessionSet or its historical Sessions.
|
|
735
|
+
*/
|
|
736
|
+
const MIGRATION_13_SQL = `
|
|
737
|
+
CREATE TABLE IF NOT EXISTS pending_runtime_turn_completions (
|
|
738
|
+
task_id TEXT NOT NULL,
|
|
739
|
+
role_name TEXT NOT NULL,
|
|
740
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 1),
|
|
741
|
+
agent_id TEXT NOT NULL,
|
|
742
|
+
native_session_id TEXT NOT NULL,
|
|
743
|
+
turn_id TEXT NOT NULL,
|
|
744
|
+
run_id TEXT NOT NULL,
|
|
745
|
+
summary TEXT NOT NULL,
|
|
746
|
+
observed_at TEXT NOT NULL,
|
|
747
|
+
due_at TEXT NOT NULL,
|
|
748
|
+
PRIMARY KEY (task_id, role_name),
|
|
749
|
+
CHECK (due_at >= observed_at),
|
|
750
|
+
FOREIGN KEY (task_id, role_name)
|
|
751
|
+
REFERENCES role_session_sets(task_id, role_name) ON DELETE CASCADE
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
CREATE INDEX IF NOT EXISTS idx_pending_runtime_turn_completions_due
|
|
755
|
+
ON pending_runtime_turn_completions(task_id, due_at, role_name);
|
|
756
|
+
|
|
757
|
+
INSERT INTO pending_runtime_turn_completions (
|
|
758
|
+
task_id, role_name, schema_version, agent_id, native_session_id,
|
|
759
|
+
turn_id, run_id, summary, observed_at, due_at
|
|
760
|
+
)
|
|
761
|
+
SELECT
|
|
762
|
+
source.task_id,
|
|
763
|
+
source.role_name,
|
|
764
|
+
json_extract(source.payload, '$.pendingTurnCompletion.schemaVersion'),
|
|
765
|
+
json_extract(source.payload, '$.pendingTurnCompletion.agentId'),
|
|
766
|
+
json_extract(source.payload, '$.pendingTurnCompletion.nativeSessionId'),
|
|
767
|
+
json_extract(source.payload, '$.pendingTurnCompletion.turnId'),
|
|
768
|
+
json_extract(source.payload, '$.pendingTurnCompletion.runId'),
|
|
769
|
+
json_extract(source.payload, '$.pendingTurnCompletion.summary'),
|
|
770
|
+
json_extract(source.payload, '$.pendingTurnCompletion.observedAt'),
|
|
771
|
+
json_extract(source.payload, '$.pendingTurnCompletion.dueAt')
|
|
772
|
+
FROM role_session_sets AS source
|
|
773
|
+
WHERE json_type(source.payload, '$.pendingTurnCompletion') = 'object';
|
|
774
|
+
`;
|
|
611
775
|
const MIGRATIONS = [
|
|
612
776
|
{ version: 1, axis: "layout", sql: MIGRATION_1_SQL },
|
|
613
777
|
{ version: 2, axis: "record", recordKind: "durableJob+capability-grant+release-workflow", sql: MIGRATION_2_SQL },
|
|
@@ -617,11 +781,197 @@ const MIGRATIONS = [
|
|
|
617
781
|
{ version: 6, axis: "record", recordKind: "reviewFinding", sql: MIGRATION_6_SQL },
|
|
618
782
|
{ version: 7, axis: "record", recordKind: "sessionOwner", sql: MIGRATION_7_SQL },
|
|
619
783
|
{ version: 8, axis: "record", recordKind: "resource-registry", sql: MIGRATION_8_SQL },
|
|
620
|
-
{ version: 9, axis: "record", recordKind: "gateArtifact", sql: MIGRATION_9_SQL }
|
|
784
|
+
{ version: 9, axis: "record", recordKind: "gateArtifact", sql: MIGRATION_9_SQL },
|
|
785
|
+
{ version: 10, axis: "layout", sql: MIGRATION_10_SQL },
|
|
786
|
+
{ version: 11, axis: "layout", sql: MIGRATION_11_SQL },
|
|
787
|
+
{ version: 12, axis: "layout", sql: MIGRATION_12_SQL },
|
|
788
|
+
{ version: 13, axis: "layout", sql: MIGRATION_13_SQL }
|
|
789
|
+
];
|
|
790
|
+
/** Current hot-path indexes whose absence would invalidate a current Home. */
|
|
791
|
+
const REQUIRED_SCHEMA_INDEXES = [
|
|
792
|
+
"idx_mailboxes_ready",
|
|
793
|
+
"idx_runtime_session_cleanup_required",
|
|
794
|
+
"idx_input_requests_open_hot",
|
|
795
|
+
"idx_pending_runtime_turn_completions_due"
|
|
621
796
|
];
|
|
622
797
|
function checksum(sql) {
|
|
623
798
|
return createHash("sha256").update(sql).digest("hex");
|
|
624
799
|
}
|
|
800
|
+
/**
|
|
801
|
+
* A SQLite Home is only safe to open when its migration ledger proves exactly
|
|
802
|
+
* which schema definition was applied. The ledger is durable metadata, not a
|
|
803
|
+
* best-effort cache: a missing row, a changed checksum, or an unknown version
|
|
804
|
+
* must stop startup before any pending migration is run.
|
|
805
|
+
*/
|
|
806
|
+
export class SqliteSchemaMigrationError extends Error {
|
|
807
|
+
constructor(detail, subject = "metadata") {
|
|
808
|
+
super(`SQLite schema migration ${subject} is invalid: ${detail}`);
|
|
809
|
+
this.name = "SqliteSchemaMigrationError";
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const SCHEMA_MIGRATIONS_SQL = `
|
|
813
|
+
CREATE TABLE schema_migrations (
|
|
814
|
+
version INTEGER PRIMARY KEY,
|
|
815
|
+
axis TEXT NOT NULL CHECK (axis IN ('layout','aggregate','record')),
|
|
816
|
+
record_kind TEXT,
|
|
817
|
+
applied_at TEXT NOT NULL,
|
|
818
|
+
checksum TEXT NOT NULL
|
|
819
|
+
)
|
|
820
|
+
`;
|
|
821
|
+
/**
|
|
822
|
+
* Admit the migration ledger only for a genuinely empty SQLite database.
|
|
823
|
+
* Recreating an absent ledger on top of existing Yui tables would make the
|
|
824
|
+
* migration runner mistake a live Home for a fresh one and replay destructive
|
|
825
|
+
* layout migrations. A database with any sqlite_master object is therefore
|
|
826
|
+
* diagnosed as corrupt/partially initialized and left untouched.
|
|
827
|
+
*/
|
|
828
|
+
function ensureMigrationLedger(db) {
|
|
829
|
+
const objects = db.prepare("SELECT type, name FROM sqlite_master WHERE name IS NOT NULL").all();
|
|
830
|
+
const ledger = objects.find(({ name }) => name === "schema_migrations");
|
|
831
|
+
if (ledger === undefined) {
|
|
832
|
+
if (objects.length !== 0) {
|
|
833
|
+
throw new SqliteSchemaMigrationError("schema_migrations ledger is missing from a non-empty database");
|
|
834
|
+
}
|
|
835
|
+
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
if (ledger.type !== "table") {
|
|
839
|
+
throw new SqliteSchemaMigrationError(`schema_migrations has type ${String(ledger.type)} instead of table`);
|
|
840
|
+
}
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
/** Validate the applied prefix and return its versions for the migration loop. */
|
|
844
|
+
function validateAppliedMigrations(db, ledgerWasCreated) {
|
|
845
|
+
const expected = new Map(MIGRATIONS.map((migration) => [migration.version, migration]));
|
|
846
|
+
const rows = db.prepare("SELECT version, axis, record_kind, checksum FROM schema_migrations ORDER BY version").all();
|
|
847
|
+
if (rows.length === 0 && !ledgerWasCreated) {
|
|
848
|
+
throw new SqliteSchemaMigrationError("schema_migrations ledger is empty in an existing database");
|
|
849
|
+
}
|
|
850
|
+
const applied = new Set();
|
|
851
|
+
for (const row of rows) {
|
|
852
|
+
if (!Number.isInteger(row.version) || row.version < 1) {
|
|
853
|
+
throw new SqliteSchemaMigrationError(`invalid migration version ${String(row.version)}`);
|
|
854
|
+
}
|
|
855
|
+
const version = row.version;
|
|
856
|
+
const migration = expected.get(version);
|
|
857
|
+
if (migration === undefined) {
|
|
858
|
+
throw new SqliteSchemaMigrationError(`unknown migration version ${version}`);
|
|
859
|
+
}
|
|
860
|
+
if (applied.has(version)) {
|
|
861
|
+
throw new SqliteSchemaMigrationError(`duplicate migration version ${version}`);
|
|
862
|
+
}
|
|
863
|
+
applied.add(version);
|
|
864
|
+
const expectedRecordKind = migration.recordKind ?? null;
|
|
865
|
+
if (row.axis !== migration.axis) {
|
|
866
|
+
throw new SqliteSchemaMigrationError(`migration ${version} axis ${String(row.axis)} does not match ${migration.axis}`);
|
|
867
|
+
}
|
|
868
|
+
if (row.record_kind !== expectedRecordKind) {
|
|
869
|
+
throw new SqliteSchemaMigrationError(`migration ${version} record_kind ${String(row.record_kind)} does not match ${String(expectedRecordKind)}`);
|
|
870
|
+
}
|
|
871
|
+
const expectedChecksum = checksum(migration.sql);
|
|
872
|
+
if (row.checksum !== expectedChecksum) {
|
|
873
|
+
throw new SqliteSchemaMigrationError(`migration ${version} checksum ${String(row.checksum)} does not match current definition`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const versions = [...applied].sort((left, right) => left - right);
|
|
877
|
+
for (let index = 0; index < versions.length; index += 1) {
|
|
878
|
+
const expectedVersion = index + 1;
|
|
879
|
+
if (versions[index] !== expectedVersion) {
|
|
880
|
+
throw new SqliteSchemaMigrationError(`migration ledger has a gap before version ${versions[index]}`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return applied;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Validate the physical objects promised by the migration ledger. Ledger
|
|
887
|
+
* rows can be forged independently of SQLite's schema, so a complete and
|
|
888
|
+
* checksummed ledger is not enough to authorize startup when an object was
|
|
889
|
+
* manually removed or replaced.
|
|
890
|
+
*/
|
|
891
|
+
function validateSchemaObjects(db) {
|
|
892
|
+
const objects = new Map(db.prepare("SELECT type, name FROM sqlite_master WHERE name IS NOT NULL").all().flatMap(({ type, name }) => (typeof type === "string" && typeof name === "string" ? [[name, type]] : [])));
|
|
893
|
+
for (const table of SQLITE_SCHEMA_TABLES) {
|
|
894
|
+
if (objects.get(table) !== "table") {
|
|
895
|
+
throw new SqliteSchemaMigrationError(`required table '${table}' is missing or has the wrong type`, "schema object");
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
for (const index of REQUIRED_SCHEMA_INDEXES) {
|
|
899
|
+
if (objects.get(index) !== "index") {
|
|
900
|
+
throw new SqliteSchemaMigrationError(`required index '${index}' is missing or has the wrong type`, "schema object");
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/** Validate migration 13's source invariant before creating its hot table. */
|
|
905
|
+
function validatePendingProjectionBackfillSources(db) {
|
|
906
|
+
const existing = db.prepare(`SELECT name
|
|
907
|
+
FROM sqlite_master
|
|
908
|
+
WHERE name IN (
|
|
909
|
+
'pending_runtime_turn_completions',
|
|
910
|
+
'idx_pending_runtime_turn_completions_due'
|
|
911
|
+
)
|
|
912
|
+
LIMIT 1`).get();
|
|
913
|
+
if (existing !== undefined) {
|
|
914
|
+
throw new SqliteSchemaMigrationError(`schema object '${existing.name}' exists before migration 13 is recorded`, "backfill");
|
|
915
|
+
}
|
|
916
|
+
const mismatch = db.prepare(`SELECT task_id, role_name
|
|
917
|
+
FROM role_session_sets
|
|
918
|
+
WHERE COALESCE(json_type(payload, '$.pendingTurnCompletion'), 'missing')
|
|
919
|
+
NOT IN ('null', 'object')
|
|
920
|
+
OR (
|
|
921
|
+
json_type(payload, '$.pendingTurnCompletion') = 'object'
|
|
922
|
+
AND (
|
|
923
|
+
json_extract(payload, '$.owner.scope') <> 'task'
|
|
924
|
+
OR json_extract(payload, '$.owner.taskId') IS NULL
|
|
925
|
+
OR json_extract(payload, '$.owner.taskId') <> task_id
|
|
926
|
+
OR json_extract(payload, '$.owner.roleName') IS NULL
|
|
927
|
+
OR json_extract(payload, '$.owner.roleName') <> role_name
|
|
928
|
+
OR json_extract(payload, '$.pendingTurnCompletion.taskId') IS NULL
|
|
929
|
+
OR json_extract(payload, '$.pendingTurnCompletion.taskId') <> task_id
|
|
930
|
+
OR json_extract(payload, '$.pendingTurnCompletion.roleName') IS NULL
|
|
931
|
+
OR json_extract(payload, '$.pendingTurnCompletion.roleName') <> role_name
|
|
932
|
+
OR json_extract(payload, '$.pendingTurnCompletion.schemaVersion') IS NOT 1
|
|
933
|
+
OR COALESCE(json_type(payload, '$.pendingTurnCompletion.agentId'), 'missing') <> 'text'
|
|
934
|
+
OR COALESCE(json_type(payload, '$.pendingTurnCompletion.nativeSessionId'), 'missing') <> 'text'
|
|
935
|
+
OR COALESCE(json_type(payload, '$.pendingTurnCompletion.turnId'), 'missing') <> 'text'
|
|
936
|
+
OR COALESCE(json_type(payload, '$.pendingTurnCompletion.runId'), 'missing') <> 'text'
|
|
937
|
+
OR COALESCE(json_type(payload, '$.pendingTurnCompletion.summary'), 'missing') <> 'text'
|
|
938
|
+
OR COALESCE(json_type(payload, '$.pendingTurnCompletion.observedAt'), 'missing') <> 'text'
|
|
939
|
+
OR COALESCE(json_type(payload, '$.pendingTurnCompletion.dueAt'), 'missing') <> 'text'
|
|
940
|
+
OR COALESCE(json_type(payload, '$.inFlight'), 'missing') <> 'object'
|
|
941
|
+
OR COALESCE(json_type(payload, '$.activeAgentId'), 'missing') <> 'text'
|
|
942
|
+
OR COALESCE(json_type(payload, '$.inFlight.agentId'), 'missing') <> 'text'
|
|
943
|
+
OR COALESCE(json_type(payload, '$.inFlight.runId'), 'missing') <> 'text'
|
|
944
|
+
OR json_extract(payload, '$.activeAgentId')
|
|
945
|
+
<> json_extract(payload, '$.pendingTurnCompletion.agentId')
|
|
946
|
+
OR json_extract(payload, '$.inFlight.agentId')
|
|
947
|
+
<> json_extract(payload, '$.pendingTurnCompletion.agentId')
|
|
948
|
+
OR json_extract(payload, '$.inFlight.runId')
|
|
949
|
+
<> json_extract(payload, '$.pendingTurnCompletion.runId')
|
|
950
|
+
OR COALESCE(json_type(payload, '$.inFlight.pushedAt'), 'missing') <> 'text'
|
|
951
|
+
OR COALESCE(
|
|
952
|
+
json_type(
|
|
953
|
+
payload,
|
|
954
|
+
'$.sessions.' || json_quote(
|
|
955
|
+
json_extract(payload, '$.pendingTurnCompletion.agentId')
|
|
956
|
+
) || '.nativeSessionId'
|
|
957
|
+
),
|
|
958
|
+
'missing'
|
|
959
|
+
) <> 'text'
|
|
960
|
+
OR json_extract(
|
|
961
|
+
payload,
|
|
962
|
+
'$.sessions.' || json_quote(
|
|
963
|
+
json_extract(payload, '$.pendingTurnCompletion.agentId')
|
|
964
|
+
) || '.nativeSessionId'
|
|
965
|
+
) <> json_extract(payload, '$.pendingTurnCompletion.nativeSessionId')
|
|
966
|
+
OR json_extract(payload, '$.pendingTurnCompletion.dueAt')
|
|
967
|
+
< json_extract(payload, '$.pendingTurnCompletion.observedAt')
|
|
968
|
+
)
|
|
969
|
+
)
|
|
970
|
+
LIMIT 1`).get();
|
|
971
|
+
if (mismatch !== undefined) {
|
|
972
|
+
throw new SqliteSchemaMigrationError(`pending Turn completion source is invalid for ${mismatch.task_id}/${mismatch.role_name}`, "backfill");
|
|
973
|
+
}
|
|
974
|
+
}
|
|
625
975
|
/**
|
|
626
976
|
* Apply pending migrations idempotently inside transactions.
|
|
627
977
|
*
|
|
@@ -631,21 +981,18 @@ function checksum(sql) {
|
|
|
631
981
|
* database performs no work (every version is already recorded).
|
|
632
982
|
*/
|
|
633
983
|
export function migrateSqliteSchema(db) {
|
|
634
|
-
db
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
applied_at TEXT NOT NULL,
|
|
640
|
-
checksum TEXT NOT NULL
|
|
641
|
-
)
|
|
642
|
-
`);
|
|
643
|
-
const applied = new Set(db.prepare("SELECT version FROM schema_migrations").all().map((row) => row.version));
|
|
984
|
+
const ledgerWasCreated = ensureMigrationLedger(db);
|
|
985
|
+
// Validate the complete ledger before touching any pending migration. This
|
|
986
|
+
// prevents a manually altered or partially recorded ledger from silently
|
|
987
|
+
// skipping the partial-index/projection migrations added after a valid Home.
|
|
988
|
+
const applied = validateAppliedMigrations(db, ledgerWasCreated);
|
|
644
989
|
const newlyApplied = [];
|
|
645
990
|
for (const migration of MIGRATIONS) {
|
|
646
991
|
if (applied.has(migration.version))
|
|
647
992
|
continue;
|
|
648
993
|
const apply = db.transaction(() => {
|
|
994
|
+
if (migration.version === 13)
|
|
995
|
+
validatePendingProjectionBackfillSources(db);
|
|
649
996
|
db.exec(migration.sql);
|
|
650
997
|
db.prepare(`INSERT INTO schema_migrations (version, axis, record_kind, applied_at, checksum)
|
|
651
998
|
VALUES (?, ?, ?, ?, ?)`).run(migration.version, migration.axis, migration.recordKind ?? null, new Date().toISOString(), checksum(migration.sql));
|
|
@@ -653,8 +1000,8 @@ export function migrateSqliteSchema(db) {
|
|
|
653
1000
|
apply();
|
|
654
1001
|
newlyApplied.push(migration.version);
|
|
655
1002
|
}
|
|
656
|
-
|
|
657
|
-
return { applied: newlyApplied, version };
|
|
1003
|
+
validateSchemaObjects(db);
|
|
1004
|
+
return { applied: newlyApplied, version: SQLITE_SCHEMA_VERSION };
|
|
658
1005
|
}
|
|
659
1006
|
/** The names of every table the schema creates (for tests/introspection). */
|
|
660
1007
|
export const SQLITE_SCHEMA_TABLES = [
|
|
@@ -699,6 +1046,8 @@ export const SQLITE_SCHEMA_TABLES = [
|
|
|
699
1046
|
"capability_grants",
|
|
700
1047
|
"release_workflows",
|
|
701
1048
|
"session_owners",
|
|
1049
|
+
"runtime_session_candidates",
|
|
1050
|
+
"pending_runtime_turn_completions",
|
|
702
1051
|
"resource_registry",
|
|
703
1052
|
"gate_artifacts",
|
|
704
1053
|
"gate_artifact_logs"
|