@zq-silk/yui 0.14.2 → 0.15.1

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.
Files changed (44) hide show
  1. package/ARCHITECTURE.md +27 -12
  2. package/README.md +85 -61
  3. package/dist/cli/commandCatalog.js +6 -6
  4. package/dist/cli/updateCommand.js +17 -9
  5. package/dist/cli/updateOrchestrator.js +81 -15
  6. package/dist/cli/updatePorts.js +72 -10
  7. package/dist/cli/upgradeCommand.js +104 -19
  8. package/dist/cli.js +2 -2
  9. package/dist/commands/agentCommands.js +13 -6
  10. package/dist/commands/controllerCommands.js +1 -1
  11. package/dist/commands/globalRoleCommands.js +11 -3
  12. package/dist/commands/roleConfiguration.js +7 -0
  13. package/dist/commands/roleRuntimeGuard.js +30 -0
  14. package/dist/commands/taskCommands.js +10 -3
  15. package/dist/controller/fileSchedulerStoreAdapter.js +4 -4
  16. package/dist/controller/runtime.js +11 -25
  17. package/dist/controller/runtimeLaunchCoordinator.js +9 -30
  18. package/dist/controller/sessionNotify.js +5 -0
  19. package/dist/core/controllerServer.js +5 -5
  20. package/dist/doctor/doctor.js +37 -14
  21. package/dist/executor/agentExecutor.js +8 -11
  22. package/dist/executor/effectiveLaunch.js +34 -17
  23. package/dist/executor/fileRoleLaunchPlanner.js +11 -8
  24. package/dist/observability/runtimeIdentity.js +48 -50
  25. package/dist/release/runtimeRelease.js +9 -1
  26. package/dist/runtime/agentHost.js +7 -0
  27. package/dist/runtime/codexInteractiveHost.js +191 -0
  28. package/dist/runtime/exactControlPlane.js +20 -29
  29. package/dist/runtime/structuredProviderHost.js +35 -0
  30. package/dist/runtime/tmuxAdapters.js +51 -9
  31. package/dist/scheduler/activeRoleTurnDelivery.js +4 -4
  32. package/dist/scheduler/leaderWakeupProcessor.js +3 -4
  33. package/dist/storage/currentTaskStore.js +6 -4
  34. package/dist/storage/sqliteSchema.js +134 -59
  35. package/dist/storage/sqliteStore.js +7 -5
  36. package/dist/storage/storageSchema.js +92 -223
  37. package/dist/storage/storageVersions.js +12 -16
  38. package/dist/storage/upgrade/upgradeOrchestrator.js +224 -62
  39. package/dist/tmux/tmuxManager.js +43 -28
  40. package/dist/version.js +3 -3
  41. package/docs/task-local-identity.md +9 -9
  42. package/i18n/README.zh-CN.md +33 -17
  43. package/package.json +1 -1
  44. package/dist/storage/upgrade/recordVersions.js +0 -82
@@ -8,26 +8,20 @@
8
8
  *
9
9
  * - `global_sequences` (§5.3): global record ID high-water marks.
10
10
  * - `outbox` (§5.4): durable outbox with `UNIQUE(request_id)` for exactly-once.
11
- * - `config`: the `YuiConfig` singleton (the design keeps `home_meta` for
12
- * identity/revision/versions; config is a separate singleton).
11
+ * - `config`: the `YuiConfig` singleton (`home_meta` keeps identity/revision).
13
12
  *
14
13
  * Record payloads are stored two ways, per §4: typed columns for fields that are
15
14
  * queried/filtered/used-for-CAS, and a `payload` JSON column holding the full
16
- * versioned record (including its family `schemaVersion`). The record axis of
17
- * the three-axis versioning is therefore unchanged.
15
+ * current record. Record-local `schemaVersion` tags remain validation guards;
16
+ * they are not independent Home compatibility axes. Any historical payload
17
+ * rewrite belongs to the ordered Home migration that introduced the new shape.
18
18
  *
19
- * The migration runner is idempotent: it records applied versions in
20
- * `schema_migrations` and uses `CREATE TABLE IF NOT EXISTS`, so re-running on an
21
- * already-current database is a no-op and a crash mid-migration is rolled back
22
- * by SQLite (DDL is transactional) and re-applied on the next open.
19
+ * The migration runner is append-only and idempotent: it records one ordered
20
+ * Home version in `schema_migrations`. Re-running an already-current database
21
+ * is a no-op; a crash mid-upgrade rolls the whole migration transaction back.
23
22
  */
24
23
  import { createHash } from "node:crypto";
25
- /** The current SQLite layout version. */
26
- export const SQLITE_LAYOUT_VERSION = 8;
27
- /** The aggregate version of the normalized SQLite schema. */
28
- export const SQLITE_AGGREGATE_VERSION = 2;
29
- /** The current schema migration version. */
30
- export const SQLITE_SCHEMA_VERSION = 1;
24
+ import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
31
25
  /** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
32
26
  export const TELEMETRY_KEEP_PER_GENERATION = 200;
33
27
  export const TELEMETRY_TURN_CAP = 50_000;
@@ -38,20 +32,19 @@ export const TELEMETRY_TURN_CAP = 50_000;
38
32
  * the store; `journal_mode=WAL` is a persistent database property set on open.
39
33
  * The migration itself only contains schema objects.
40
34
  *
41
- * This is the direct current baseline for a new Home. Its checksum rejects any
42
- * database initialized from a different physical contract.
35
+ * This is the Yui 0.15.0 / storage-version-1 baseline. Future releases append
36
+ * migrations after it so fresh and upgraded databases converge on the same
37
+ * current contract.
43
38
  */
44
39
  const BASELINE_CORE_SQL = `
45
40
  -- Global catalog and coordination (§4.1) -------------------------------------
46
41
 
47
42
  CREATE TABLE IF NOT EXISTS home_meta (
48
- id INTEGER PRIMARY KEY CHECK (id = 1),
49
- home_identity TEXT NOT NULL,
50
- revision INTEGER NOT NULL,
51
- layout_version INTEGER NOT NULL,
52
- aggregate_version INTEGER NOT NULL,
53
- created_at TEXT NOT NULL,
54
- updated_at TEXT NOT NULL
43
+ id INTEGER PRIMARY KEY CHECK (id = 1),
44
+ home_identity TEXT NOT NULL,
45
+ revision INTEGER NOT NULL,
46
+ created_at TEXT NOT NULL,
47
+ updated_at TEXT NOT NULL
55
48
  );
56
49
 
57
50
  CREATE TABLE IF NOT EXISTS config (
@@ -464,7 +457,7 @@ END;
464
457
  * Session owner physical identity records.
465
458
  *
466
459
  * One row per runtime generation, keyed by runtime generation id. The payload column
467
- * stores the full versioned JSON record; typed columns support the
460
+ * stores the full current JSON record; typed columns support the
468
461
  * reconciliation queries (task/role lookup, PID liveness).
469
462
  */
470
463
  const BASELINE_SESSION_OWNER_SQL = `
@@ -485,9 +478,8 @@ CREATE INDEX IF NOT EXISTS idx_session_owners_task
485
478
  /**
486
479
  * Resource GC registry.
487
480
  *
488
- * GC-owned table for resource lifecycle records. The registry is GC's own
489
- * state it is not part of the aggregate and never participates in aggregate
490
- * versioning. Records are stored as full versioned JSON in `payload`, with
481
+ * GC-owned table for resource lifecycle records. The registry is GC's own
482
+ * state. Records are stored as full current JSON in `payload`, with
491
483
  * typed columns for the fields GC queries (disposition, kind, task_id).
492
484
  */
493
485
  const BASELINE_RESOURCE_REGISTRY_SQL = `
@@ -661,10 +653,26 @@ const MIGRATION_1_SQL = [
661
653
  BASELINE_TASK_WAKE_SQL,
662
654
  BASELINE_CONTEXT_SNAPSHOT_SQL
663
655
  ].join("\n");
664
- /** Future compatible releases append new migrations after this baseline. */
665
- const MIGRATIONS = [
666
- { version: 1, axis: "layout", sql: MIGRATION_1_SQL }
667
- ];
656
+ /** Released migrations are append-only and must never be rewritten. */
657
+ const MIGRATIONS = Object.freeze([
658
+ {
659
+ version: 1,
660
+ name: "v0.15.0-baseline",
661
+ introducedIn: "0.15.0",
662
+ sql: MIGRATION_1_SQL
663
+ }
664
+ ]);
665
+ for (let index = 0; index < MIGRATIONS.length; index += 1) {
666
+ const expectedVersion = MIN_SUPPORTED_STORAGE_VERSION + index;
667
+ if (MIGRATIONS[index]?.version !== expectedVersion) {
668
+ throw new Error(`Storage migration registry must be contiguous from `
669
+ + `${MIN_SUPPORTED_STORAGE_VERSION}; missing version ${expectedVersion}.`);
670
+ }
671
+ }
672
+ if (MIGRATIONS.at(-1)?.version !== CURRENT_STORAGE_VERSION) {
673
+ throw new Error(`Storage migration registry head ${String(MIGRATIONS.at(-1)?.version)} does not match `
674
+ + `CURRENT_STORAGE_VERSION ${CURRENT_STORAGE_VERSION}.`);
675
+ }
668
676
  /** Current hot-path indexes whose absence would invalidate a current Home. */
669
677
  const REQUIRED_SCHEMA_INDEXES = [
670
678
  "idx_mailboxes_ready",
@@ -688,11 +696,10 @@ export class SqliteSchemaMigrationError extends Error {
688
696
  }
689
697
  const SCHEMA_MIGRATIONS_SQL = `
690
698
  CREATE TABLE schema_migrations (
691
- version INTEGER PRIMARY KEY,
692
- axis TEXT NOT NULL CHECK (axis IN ('layout','aggregate','record')),
693
- record_kind TEXT,
694
- applied_at TEXT NOT NULL,
695
- checksum TEXT NOT NULL
699
+ version INTEGER PRIMARY KEY,
700
+ name TEXT NOT NULL,
701
+ applied_at TEXT NOT NULL,
702
+ checksum TEXT NOT NULL
696
703
  )
697
704
  `;
698
705
  /**
@@ -720,10 +727,20 @@ function ensureMigrationLedger(db, mode) {
720
727
  }
721
728
  return false;
722
729
  }
723
- /** Validate the applied prefix and return its versions for the migration loop. */
730
+ function validateMigrationLedgerColumns(db) {
731
+ const columns = new Set(db.prepare("PRAGMA table_info(schema_migrations)").all()
732
+ .flatMap(({ name }) => typeof name === "string" ? [name] : []));
733
+ if (columns.size === 4
734
+ && ["version", "name", "applied_at", "checksum"].every((name) => columns.has(name))) {
735
+ return;
736
+ }
737
+ throw new SqliteSchemaMigrationError("schema_migrations columns do not match the storage-version-1 ledger");
738
+ }
739
+ /** Validate the applied linear prefix and return its current head. */
724
740
  function validateAppliedMigrations(db, ledgerWasCreated) {
741
+ validateMigrationLedgerColumns(db);
725
742
  const expected = new Map(MIGRATIONS.map((migration) => [migration.version, migration]));
726
- const rows = db.prepare("SELECT version, axis, record_kind, checksum FROM schema_migrations ORDER BY version").all();
743
+ const rows = db.prepare("SELECT version, name, checksum FROM schema_migrations ORDER BY version").all();
727
744
  if (rows.length === 0 && !ledgerWasCreated) {
728
745
  throw new SqliteSchemaMigrationError("schema_migrations ledger is empty in an existing database");
729
746
  }
@@ -734,19 +751,22 @@ function validateAppliedMigrations(db, ledgerWasCreated) {
734
751
  }
735
752
  const version = row.version;
736
753
  const migration = expected.get(version);
737
- if (migration === undefined) {
738
- throw new SqliteSchemaMigrationError(`unknown migration version ${version}`);
739
- }
740
754
  if (applied.has(version)) {
741
755
  throw new SqliteSchemaMigrationError(`duplicate migration version ${version}`);
742
756
  }
743
757
  applied.add(version);
744
- const expectedRecordKind = migration.recordKind ?? null;
745
- if (row.axis !== migration.axis) {
746
- throw new SqliteSchemaMigrationError(`migration ${version} axis ${String(row.axis)} does not match ${migration.axis}`);
758
+ if (migration === undefined) {
759
+ if (version <= CURRENT_STORAGE_VERSION) {
760
+ throw new SqliteSchemaMigrationError(`unknown migration version ${version}`);
761
+ }
762
+ if (typeof row.name !== "string" || row.name.length === 0
763
+ || typeof row.checksum !== "string" || row.checksum.length === 0) {
764
+ throw new SqliteSchemaMigrationError(`future migration ${version} metadata is invalid`);
765
+ }
766
+ continue;
747
767
  }
748
- if (row.record_kind !== expectedRecordKind) {
749
- throw new SqliteSchemaMigrationError(`migration ${version} record_kind ${String(row.record_kind)} does not match ${String(expectedRecordKind)}`);
768
+ if (row.name !== migration.name) {
769
+ throw new SqliteSchemaMigrationError(`migration ${version} name ${String(row.name)} does not match ${migration.name}`);
750
770
  }
751
771
  const expectedChecksum = checksum(migration.sql);
752
772
  if (row.checksum !== expectedChecksum) {
@@ -755,12 +775,18 @@ function validateAppliedMigrations(db, ledgerWasCreated) {
755
775
  }
756
776
  const versions = [...applied].sort((left, right) => left - right);
757
777
  for (let index = 0; index < versions.length; index += 1) {
758
- const expectedVersion = index + 1;
778
+ const expectedVersion = MIN_SUPPORTED_STORAGE_VERSION + index;
759
779
  if (versions[index] !== expectedVersion) {
760
780
  throw new SqliteSchemaMigrationError(`migration ledger has a gap before version ${versions[index]}`);
761
781
  }
762
782
  }
763
- return applied;
783
+ const currentVersion = versions.at(-1) ?? 0;
784
+ const head = rows.at(-1);
785
+ return {
786
+ versions: applied,
787
+ currentVersion,
788
+ currentChecksum: typeof head?.checksum === "string" ? head.checksum : ""
789
+ };
764
790
  }
765
791
  /**
766
792
  * Validate the physical objects promised by the migration ledger. Ledger
@@ -780,25 +806,63 @@ function validateSchemaObjects(db) {
780
806
  throw new SqliteSchemaMigrationError(`required index '${index}' is missing or has the wrong type`, "schema object");
781
807
  }
782
808
  }
809
+ const homeMetaColumns = db.prepare("PRAGMA table_info(home_meta)").all().map(({ name }) => name);
810
+ const expectedHomeMetaColumns = [
811
+ "id",
812
+ "home_identity",
813
+ "revision",
814
+ "created_at",
815
+ "updated_at"
816
+ ];
817
+ if (homeMetaColumns.length !== expectedHomeMetaColumns.length
818
+ || homeMetaColumns.some((name, index) => name !== expectedHomeMetaColumns[index])) {
819
+ throw new SqliteSchemaMigrationError("home_meta columns do not match the current storage contract", "schema object");
820
+ }
821
+ }
822
+ /** Return the one linear upgrade path from a supported source to this release. */
823
+ export function storageMigrationPlan(currentVersion) {
824
+ if (!Number.isInteger(currentVersion)
825
+ || currentVersion < MIN_SUPPORTED_STORAGE_VERSION
826
+ || currentVersion > CURRENT_STORAGE_VERSION) {
827
+ return null;
828
+ }
829
+ return MIGRATIONS
830
+ .filter(({ version }) => version > currentVersion)
831
+ .map(({ version, name, introducedIn }) => ({
832
+ fromVersion: version - 1,
833
+ toVersion: version,
834
+ name,
835
+ introducedIn
836
+ }));
783
837
  }
784
838
  /** Inspect a recognized migration prefix without changing it. */
785
839
  export function inspectSqliteSchemaMigrations(db) {
786
840
  const ledgerWasCreated = ensureMigrationLedger(db, "validate");
787
841
  const applied = validateAppliedMigrations(db, ledgerWasCreated);
842
+ if (applied.currentVersion > CURRENT_STORAGE_VERSION) {
843
+ return {
844
+ currentVersion: applied.currentVersion,
845
+ currentChecksum: applied.currentChecksum,
846
+ targetVersion: CURRENT_STORAGE_VERSION,
847
+ minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
848
+ targetChecksum: checksum(MIGRATIONS.at(-1).sql),
849
+ pendingVersions: []
850
+ };
851
+ }
788
852
  const pendingVersions = MIGRATIONS
789
- .filter((migration) => !applied.has(migration.version))
853
+ .filter((migration) => !applied.versions.has(migration.version))
790
854
  .map((migration) => migration.version);
791
855
  if (pendingVersions.length === 0)
792
856
  validateSchemaObjects(db);
793
- const current = MIGRATIONS[applied.size - 1];
794
857
  const target = MIGRATIONS.at(-1);
795
- if (current === undefined) {
858
+ if (applied.currentVersion === 0) {
796
859
  throw new SqliteSchemaMigrationError("schema_migrations ledger has no current head");
797
860
  }
798
861
  return {
799
- currentVersion: applied.size,
800
- currentChecksum: checksum(current.sql),
801
- targetVersion: SQLITE_SCHEMA_VERSION,
862
+ currentVersion: applied.currentVersion,
863
+ currentChecksum: applied.currentChecksum,
864
+ targetVersion: CURRENT_STORAGE_VERSION,
865
+ minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
802
866
  targetChecksum: checksum(target.sql),
803
867
  pendingVersions
804
868
  };
@@ -819,15 +883,26 @@ export function migrateSqliteSchema(db, options) {
819
883
  // prevents a manually altered or partially recorded ledger from silently
820
884
  // skipping a later schema/data step.
821
885
  const applied = validateAppliedMigrations(db, ledgerWasCreated);
822
- const pending = MIGRATIONS.filter((migration) => !applied.has(migration.version));
886
+ if (applied.currentVersion > CURRENT_STORAGE_VERSION) {
887
+ throw new SqliteSchemaMigrationError(`storage version ${applied.currentVersion} is newer than supported `
888
+ + `${CURRENT_STORAGE_VERSION}`, "admission");
889
+ }
890
+ if (applied.currentVersion !== 0
891
+ && applied.currentVersion < MIN_SUPPORTED_STORAGE_VERSION) {
892
+ throw new SqliteSchemaMigrationError(`storage version ${applied.currentVersion} is older than the minimum supported `
893
+ + `${MIN_SUPPORTED_STORAGE_VERSION}`, "admission");
894
+ }
895
+ const pending = MIGRATIONS.filter((migration) => !applied.versions.has(migration.version));
823
896
  if (!ledgerWasCreated && pending.length > 0 && options.mode === "validate") {
824
- throw new SqliteSchemaMigrationError(`SQLite schema ${applied.size} requires an explicit upgrade to ${SQLITE_SCHEMA_VERSION}`, "admission");
897
+ throw new SqliteSchemaMigrationError(`Storage version ${applied.currentVersion} requires an explicit upgrade to `
898
+ + `${CURRENT_STORAGE_VERSION}`, "admission");
825
899
  }
826
900
  const newlyApplied = [];
827
901
  for (const migration of pending) {
828
902
  db.exec(migration.sql);
829
- db.prepare(`INSERT INTO schema_migrations (version, axis, record_kind, applied_at, checksum)
830
- VALUES (?, ?, ?, ?, ?)`).run(migration.version, migration.axis, migration.recordKind ?? null, new Date().toISOString(), checksum(migration.sql));
903
+ const appliedAt = new Date().toISOString();
904
+ db.prepare(`INSERT INTO schema_migrations (version, name, applied_at, checksum)
905
+ VALUES (?, ?, ?, ?)`).run(migration.version, migration.name, appliedAt, checksum(migration.sql));
831
906
  newlyApplied.push(migration.version);
832
907
  }
833
908
  validateSchemaObjects(db);
@@ -836,7 +911,7 @@ export function migrateSqliteSchema(db, options) {
836
911
  const newlyApplied = options.mode === "apply" && !db.inTransaction
837
912
  ? db.transaction(migrate)()
838
913
  : migrate();
839
- return { applied: newlyApplied, version: SQLITE_SCHEMA_VERSION };
914
+ return { applied: newlyApplied, version: CURRENT_STORAGE_VERSION };
840
915
  }
841
916
  /** The names of every table the schema creates (for tests/introspection). */
842
917
  export const SQLITE_SCHEMA_TABLES = [
@@ -17,11 +17,12 @@
17
17
  * on the durable outbox.
18
18
  * - Crash recovery .............. WAL rollback of uncommitted transactions;
19
19
  * outbox replay of committed-but-unacked effects.
20
- * - Record family versioning .... full record (incl. schemaVersion) in payload.
20
+ * - Record validation ........... current record (incl. local schemaVersion)
21
+ * in payload.
21
22
  * - Evidence retention .......... events/review_rounds/change_sets/
22
23
  * integration_attempts are never pruned.
23
24
  *
24
- * Records are stored as full versioned JSON in `payload` columns, with typed
25
+ * Records are stored as full current JSON in `payload` columns, with typed
25
26
  * columns for the fields that are queried/filtered/used-for-CAS (§4). A
26
27
  * high-frequency runtime telemetry observation is a single-row upsert into
27
28
  * `telemetry` scoped by its primary key — it never rewrites global
@@ -51,7 +52,7 @@ import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
51
52
  import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveTurnKey, executionLaneActiveTurnKeyParts, StorageConflictError, StorageCancelledError, StorageRecordError, storedCapabilityGrant, storedPublicationReference, storedReleaseWorkflow, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, pendingWakeupProjection, validateYuiConfig } from "./taskStore.js";
52
53
  import { publicationExternalKey } from "../task/publicationReference.js";
53
54
  import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
54
- import { inspectSqliteSchemaMigrations, migrateSqliteSchema, SqliteSchemaMigrationError, SQLITE_AGGREGATE_VERSION, SQLITE_LAYOUT_VERSION, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_TURN_CAP } from "./sqliteSchema.js";
55
+ import { inspectSqliteSchemaMigrations, migrateSqliteSchema, SqliteSchemaMigrationError, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_TURN_CAP } from "./sqliteSchema.js";
55
56
  import { StorageSchemaError } from "./storageSchema.js";
56
57
  /** Read the immutable Home identity without opening a writable Store connection. */
57
58
  export function readSqliteHomeIdentity(rootDir, databaseFilename = "yui.db") {
@@ -192,8 +193,9 @@ export class SqliteTaskStore {
192
193
  #seedHomeMeta() {
193
194
  const now = new Date().toISOString();
194
195
  const identity = generateHomeIdentity(new Date());
195
- this.#db.prepare(`INSERT OR IGNORE INTO home_meta (id, home_identity, revision, layout_version, aggregate_version, created_at, updated_at)
196
- VALUES (1, ?, 0, ?, ?, ?, ?)`).run(JSON.stringify(identity), SQLITE_LAYOUT_VERSION, SQLITE_AGGREGATE_VERSION, now, now);
196
+ this.#db.prepare(`INSERT OR IGNORE INTO home_meta
197
+ (id, home_identity, revision, created_at, updated_at)
198
+ VALUES (1, ?, 0, ?, ?)`).run(JSON.stringify(identity), now, now);
197
199
  }
198
200
  #seedConfig() {
199
201
  this.#db.prepare(`INSERT OR IGNORE INTO config (id, payload, updated_at) VALUES (1, ?, ?)`).run(JSON.stringify(DEFAULT_CONFIG), new Date().toISOString());