@zq-silk/yui 0.11.3 → 0.12.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.
- package/ARCHITECTURE.md +23 -4
- package/README.md +44 -22
- package/dist/cli/commandCatalog.js +13 -10
- package/dist/cli/interactionPolicy.js +5 -6
- package/dist/cli/updateCommand.js +3 -1
- package/dist/cli/updateOrchestrator.js +173 -28
- package/dist/cli/updatePorts.js +137 -8
- package/dist/cli/upgradeCommand.js +19 -9
- package/dist/cli.js +156 -57
- package/dist/commands/configCommands.js +13 -46
- package/dist/commands/executionAuditCommands.js +3 -2
- package/dist/commands/jobCommands.js +4 -10
- package/dist/commands/taskCommands.js +234 -200
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +61 -13
- package/dist/commands/taskInputCommands.js +8 -5
- package/dist/commands/taskNextActionCommand.js +56 -6
- package/dist/commands/taskOverviewCommand.js +17 -31
- package/dist/commands/taskRoleRuntimeStatus.js +55 -7
- package/dist/commands/taskWorkspaceCommands.js +5 -2
- package/dist/config/configCatalog.js +3 -3
- package/dist/config/yuiConfig.js +3 -7
- package/dist/context/wakeNotification.js +20 -5
- package/dist/controller/agentRuntimeObserver.js +247 -51
- package/dist/controller/clientRuntime.js +39 -3
- package/dist/controller/controller.js +31 -27
- package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
- package/dist/controller/runtime.js +58 -3
- package/dist/controller/runtimeEventInbox.js +17 -7
- package/dist/controller/runtimeEventProcessor.js +12 -19
- package/dist/controller/runtimeHookRunFence.js +19 -4
- package/dist/controller/runtimeObservationHook.js +45 -0
- package/dist/controller/structuredProviderObservation.js +20 -3
- package/dist/core/controllerClient.js +20 -2
- package/dist/core/controllerServer.js +1 -0
- package/dist/execution/resourceBroker.js +5 -4
- package/dist/executor/agentExecutor.js +52 -50
- package/dist/executor/effectiveLaunch.js +8 -48
- package/dist/executor/fileRoleLaunchPlanner.js +129 -45
- package/dist/executor/workspacePreflightClassification.js +23 -2
- package/dist/interaction/operatorPresentation.js +33 -89
- package/dist/lifecycle/exactRunTerminalization.js +69 -5
- package/dist/observability/executionAudit.js +5 -0
- package/dist/observability/orchestrationMetrics.js +8 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +123 -0
- package/dist/release/runtimeRelease.js +20 -0
- package/dist/repository/taskWorkspacePreparer.js +176 -60
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/deltaRecheck.js +12 -51
- package/dist/review/reviewAcceptance.js +26 -0
- package/dist/review/reviewConfig.js +0 -31
- package/dist/review/reviewDecision.js +113 -0
- package/dist/review/reviewOutcomeClassifier.js +1 -1
- package/dist/review/reviewRound.js +1 -1
- package/dist/review/reviewerAvailability.js +69 -0
- package/dist/run/recoveryProjection.js +45 -6
- package/dist/runtime/agentDriverObservation.js +24 -10
- package/dist/runtime/agentHost.js +159 -85
- package/dist/runtime/builtinAgentDrivers.js +4 -3
- package/dist/runtime/builtinTranscriptObserver.js +301 -64
- package/dist/runtime/builtinTranscriptUsage.js +9 -7
- package/dist/runtime/conversationSwitch.js +277 -0
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/launchBroker.js +12 -0
- package/dist/runtime/processExitOutbox.js +88 -0
- package/dist/runtime/providerRuntimeIdentity.js +29 -1
- package/dist/runtime/runtimeHealthPolicy.js +5 -5
- package/dist/runtime/runtimeObservation.js +28 -0
- package/dist/runtime/runtimeProjection.js +22 -32
- package/dist/runtime/sessionTokenMetrics.js +181 -0
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +4 -1
- package/dist/scheduler/activeRoleRunDelivery.js +34 -199
- package/dist/scheduler/activeTaskProgress.js +7 -6
- package/dist/scheduler/leaderWakeupProcessor.js +42 -138
- package/dist/scheduler/operatorEvent.js +34 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
- package/dist/scheduler/roleRunStall.js +53 -31
- package/dist/scheduler/taskExecutionProjection.js +97 -76
- package/dist/scheduler/taskObservabilityProjection.js +10 -11
- package/dist/storage/migration/productionRegistry.js +379 -0
- package/dist/storage/sqliteSchema.js +66 -23
- package/dist/storage/sqliteStore.js +46 -23
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +15 -30
- package/dist/storage/upgrade/homeClassification.js +52 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
- package/dist/storage/upgrade/recordVersions.js +3 -2
- package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
- package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
- package/dist/task/completionReadiness.js +10 -6
- package/dist/task/nextAction.js +82 -58
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/client/components.js +13 -3
- package/dist/web/assets/client/i18n.js +6 -2
- package/dist/web/webSnapshot.js +7 -1
- package/i18n/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +60 -20
- package/skills/yui-operator/SKILL.md +13 -4
- package/skills/yui-reviewer/SKILL.md +35 -11
- package/dist/context/sessionContextBudget.js +0 -71
- package/dist/lifecycle/contextBudgetRollover.js +0 -81
- package/dist/lifecycle/taskRoleSessionReset.js +0 -126
- package/dist/scheduler/operatorNotification.js +0 -59
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* dropped, duplicated, or mutated record.
|
|
23
23
|
*/
|
|
24
24
|
import { createHash } from "node:crypto";
|
|
25
|
+
import { existsSync, rmSync } from "node:fs";
|
|
25
26
|
import { join } from "node:path";
|
|
26
27
|
import Database from "better-sqlite3";
|
|
27
28
|
import { mailboxTargetKey } from "../../coordination/workMailbox.js";
|
|
@@ -33,6 +34,50 @@ import { CURRENT_STORED_TASK_SCHEMA_VERSION } from "../taskStore.js";
|
|
|
33
34
|
export const STAGED_DATABASE_FILENAME = "yui.db.staged";
|
|
34
35
|
/** The committed database filename. */
|
|
35
36
|
export const COMMITTED_DATABASE_FILENAME = "yui.db";
|
|
37
|
+
/** Disposable, schema-current snapshot used to reconstruct an older source. */
|
|
38
|
+
export const MIGRATION_SOURCE_DATABASE_FILENAME = "yui.db.migration-source";
|
|
39
|
+
/**
|
|
40
|
+
* Reconstruct a migration snapshot without advancing the authoritative DB.
|
|
41
|
+
*
|
|
42
|
+
* `VACUUM INTO` takes a consistent SQLite snapshot, including committed WAL
|
|
43
|
+
* state, without writing the source. Pending physical migrations are then
|
|
44
|
+
* applied only to that disposable copy before the current reader enumerates
|
|
45
|
+
* record families that may not exist in the older source schema.
|
|
46
|
+
*/
|
|
47
|
+
export function readMigrationSourceStateFromSqlite(home) {
|
|
48
|
+
const sourcePath = join(home, COMMITTED_DATABASE_FILENAME);
|
|
49
|
+
const snapshotPath = join(home, MIGRATION_SOURCE_DATABASE_FILENAME);
|
|
50
|
+
if (existsSync(snapshotPath)) {
|
|
51
|
+
throw new Error(`Refusing to overwrite an existing SQLite migration-source snapshot: ${snapshotPath}. `
|
|
52
|
+
+ "Discard it and retry.");
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const source = new Database(sourcePath, { readonly: true });
|
|
56
|
+
try {
|
|
57
|
+
source.prepare("VACUUM INTO ?").run(snapshotPath);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
source.close();
|
|
61
|
+
}
|
|
62
|
+
// Physical migration 14 rewrites the WorkMailbox family while adding its
|
|
63
|
+
// current columns. Preserve the source rows before applying it so the
|
|
64
|
+
// logical record migration still sees and transforms v1 exactly once.
|
|
65
|
+
const sourceMailboxes = readWorkMailboxesFromSqlite(snapshotPath);
|
|
66
|
+
const stagedStore = new SqliteTaskStore(home, {
|
|
67
|
+
databaseFilename: MIGRATION_SOURCE_DATABASE_FILENAME,
|
|
68
|
+
migration: true
|
|
69
|
+
});
|
|
70
|
+
stagedStore.close();
|
|
71
|
+
const state = readStateFromSqlite(home, MIGRATION_SOURCE_DATABASE_FILENAME);
|
|
72
|
+
state.mailboxes = sourceMailboxes;
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
rmSync(snapshotPath, { force: true });
|
|
77
|
+
rmSync(`${snapshotPath}-wal`, { force: true });
|
|
78
|
+
rmSync(`${snapshotPath}-shm`, { force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
36
81
|
/**
|
|
37
82
|
* Preserve SQLite-owned durable state that intentionally sits outside the
|
|
38
83
|
* state.json-shaped Task aggregate snapshot. Volatile coordination locks and
|
|
@@ -351,9 +396,6 @@ export function populateSqliteFromState(home, state, databaseFilename) {
|
|
|
351
396
|
if (stored.leaderFailure !== null) {
|
|
352
397
|
store.saveLeaderFailure(stored.leaderFailure);
|
|
353
398
|
}
|
|
354
|
-
if (stored.operatorNotification !== null) {
|
|
355
|
-
store.saveOperatorNotification(stored.operatorNotification);
|
|
356
|
-
}
|
|
357
399
|
// Per-task ID high-water marks.
|
|
358
400
|
for (const [kind, highWater] of Object.entries(stored.idHighWaterMarks)) {
|
|
359
401
|
if (typeof highWater === "number" && Number.isFinite(highWater)) {
|
|
@@ -595,6 +637,20 @@ function readWorkMailboxRows(db) {
|
|
|
595
637
|
const hasInputDelivery = db.prepare("PRAGMA table_info(mailboxes)").all().some(({ name }) => name === "input_delivery");
|
|
596
638
|
return db.prepare(`SELECT target_kind, task_id, role_name, next_sequence, processing, pending${hasInputDelivery ? ", input_delivery" : ""} FROM mailboxes ORDER BY target_key`).all();
|
|
597
639
|
}
|
|
640
|
+
function readWorkMailboxesFromSqlite(dbPath) {
|
|
641
|
+
const db = new Database(dbPath, { readonly: true });
|
|
642
|
+
try {
|
|
643
|
+
const mailboxes = {};
|
|
644
|
+
for (const row of readWorkMailboxRows(db)) {
|
|
645
|
+
const mailbox = rowToMailbox(row);
|
|
646
|
+
mailboxes[mailboxTargetKey(mailbox.target)] = mailbox;
|
|
647
|
+
}
|
|
648
|
+
return mailboxes;
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
db.close();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
598
654
|
/**
|
|
599
655
|
* Compute per-family checksums from the SQLite database. The database is
|
|
600
656
|
* opened read-only; this is the independent verification read.
|
|
@@ -692,8 +748,8 @@ export function verifySqliteChecksums(state, home, databaseFilename) {
|
|
|
692
748
|
*
|
|
693
749
|
* The database is opened read-only; this function never writes.
|
|
694
750
|
*/
|
|
695
|
-
export function readStateFromSqlite(home) {
|
|
696
|
-
const dbPath = join(home,
|
|
751
|
+
export function readStateFromSqlite(home, databaseFilename = COMMITTED_DATABASE_FILENAME) {
|
|
752
|
+
const dbPath = join(home, databaseFilename);
|
|
697
753
|
const db = new Database(dbPath, { readonly: true });
|
|
698
754
|
try {
|
|
699
755
|
const manifest = readStorageSchemaManifest(home);
|
|
@@ -737,7 +793,7 @@ export function readStateFromSqlite(home) {
|
|
|
737
793
|
const tasks = state.tasks;
|
|
738
794
|
const taskRows = db.prepare("SELECT task_id, payload, brief FROM task_records").all();
|
|
739
795
|
for (const row of taskRows) {
|
|
740
|
-
|
|
796
|
+
const aggregate = {
|
|
741
797
|
schemaVersion: storedTaskVersion,
|
|
742
798
|
task: JSON.parse(row.payload),
|
|
743
799
|
brief: row.brief === null ? null : JSON.parse(row.brief),
|
|
@@ -761,12 +817,14 @@ export function readStateFromSqlite(home) {
|
|
|
761
817
|
events: {},
|
|
762
818
|
wakes: {},
|
|
763
819
|
leaderFailure: null,
|
|
764
|
-
operatorNotification: null,
|
|
765
820
|
idHighWaterMarks: {},
|
|
766
821
|
capabilityGrants: {},
|
|
767
822
|
releaseWorkflows: {},
|
|
768
823
|
publicationReferences: {}
|
|
769
824
|
};
|
|
825
|
+
if (storedTaskVersion <= 17)
|
|
826
|
+
aggregate.operatorNotification = null;
|
|
827
|
+
tasks[row.task_id] = aggregate;
|
|
770
828
|
}
|
|
771
829
|
// Per-task record families, keyed by their state.json map keys.
|
|
772
830
|
loadTaskPayloadMap(db, "task_roles", tasks, "roles", (record) => record.name);
|
|
@@ -808,7 +866,7 @@ export function readStateFromSqlite(home) {
|
|
|
808
866
|
if (row.kind === "leader-failure") {
|
|
809
867
|
stored.leaderFailure = JSON.parse(row.payload);
|
|
810
868
|
}
|
|
811
|
-
else {
|
|
869
|
+
else if (storedTaskVersion <= 17) {
|
|
812
870
|
stored.operatorNotification = JSON.parse(row.payload);
|
|
813
871
|
}
|
|
814
872
|
}
|
|
@@ -34,14 +34,17 @@
|
|
|
34
34
|
* authoritative input byte-for-byte unchanged.
|
|
35
35
|
*/
|
|
36
36
|
import { spawn } from "node:child_process";
|
|
37
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
37
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
38
|
+
import { tmpdir } from "node:os";
|
|
38
39
|
import { dirname, join } from "node:path";
|
|
40
|
+
import Database from "better-sqlite3";
|
|
39
41
|
import { describeReport, runMigration } from "../migration/index.js";
|
|
40
42
|
import { validateCompatibleFileTaskStore } from "../compatibleTaskStore.js";
|
|
41
43
|
import { stopFileTaskController, ensureFileTaskController, ensureFileTaskControllerIdentity } from "../../controller/clientRuntime.js";
|
|
42
44
|
import { callController } from "../../core/controllerClient.js";
|
|
43
45
|
import { FileTaskStore, STORAGE_STATE_FILE, withStorageWriteLock } from "../taskStore.js";
|
|
44
46
|
import { SqliteTaskStore } from "../sqliteStore.js";
|
|
47
|
+
import { migrateSqliteSchema } from "../sqliteSchema.js";
|
|
45
48
|
import { clearUpgradeFence, placeUpgradeFence, readUpgradeFence, UpgradeFenceError } from "../upgradeFence.js";
|
|
46
49
|
import { withUpgradeCoordinationLock } from "../upgradeCoordination.js";
|
|
47
50
|
import { clearUpgradeReceipt, writeUpgradeReceipt, upgradeReceiptPath } from "./upgradeReceipt.js";
|
|
@@ -52,7 +55,7 @@ import { repairPseudoLayout7 } from "./pseudoLayoutRepair.js";
|
|
|
52
55
|
import { createSqliteMigrationTarget } from "./sqliteMigrationTarget.js";
|
|
53
56
|
import { createSqliteRecordMigrationTarget } from "./sqliteRecordMigrationTarget.js";
|
|
54
57
|
import { COMMITTED_DATABASE_FILENAME } from "./sqliteStateMigration.js";
|
|
55
|
-
import { inspectOfflineUpgradeInventory } from "./offlineUpgradeInventory.js";
|
|
58
|
+
import { inspectOfflineUpgradeInventory, inspectSqliteDurableUpgradeInventory } from "./offlineUpgradeInventory.js";
|
|
56
59
|
/**
|
|
57
60
|
* Run the storage upgrade for one Home. Never throws for an expected blocker;
|
|
58
61
|
* it returns a structured `blocked` result instead. It only throws on a truly
|
|
@@ -95,6 +98,9 @@ export async function runStorageUpgrade(options) {
|
|
|
95
98
|
action: blocker.action
|
|
96
99
|
}, classification);
|
|
97
100
|
}
|
|
101
|
+
if (classification.sqliteMigration !== undefined) {
|
|
102
|
+
return runSqliteInPlaceUpgrade(options, classification, now, callerPid);
|
|
103
|
+
}
|
|
98
104
|
// A pseudo-layout-7 Home (manifest 7, no yui.db, readable state.json) needs
|
|
99
105
|
// the deterministic staged state.json→SQLite repair, not the version
|
|
100
106
|
// migration engine (Issue 01). The update preflight reports it as
|
|
@@ -357,6 +363,329 @@ function offlineInventoryBlocker(inventory, sceneUnchanged) {
|
|
|
357
363
|
...(sceneUnchanged ? { sceneUnchanged: true } : {})
|
|
358
364
|
};
|
|
359
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* A valid SQLite migration prefix advances the authoritative database in
|
|
368
|
+
* place. It still uses the offline runtime gate because the old Controller
|
|
369
|
+
* cannot safely resume after the schema transaction commits, but it never
|
|
370
|
+
* snapshots, copies, rebuilds, or swaps the database.
|
|
371
|
+
*/
|
|
372
|
+
async function runSqliteInPlaceUpgrade(options, classification, now, callerPid) {
|
|
373
|
+
const migration = classification.sqliteMigration;
|
|
374
|
+
const inventory = await readOfflineInventory(options, options.home);
|
|
375
|
+
if (inventory.total > 0) {
|
|
376
|
+
return withClassification(offlineInventoryBlocker(inventory, true), classification);
|
|
377
|
+
}
|
|
378
|
+
if (options.mode === "update-preflight") {
|
|
379
|
+
return {
|
|
380
|
+
outcome: "update-preflight",
|
|
381
|
+
status: "in-place-migration",
|
|
382
|
+
stepCount: migration.pendingVersions.length,
|
|
383
|
+
classification,
|
|
384
|
+
sqliteMigration: migration
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (options.mode === "dry-run") {
|
|
388
|
+
return validateSqliteInPlaceDryRun(options, classification, now);
|
|
389
|
+
}
|
|
390
|
+
return executeSqliteInPlaceUpgrade(options, classification, now, callerPid);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Exercise every pending SQLite migration against a consistent disposable
|
|
394
|
+
* snapshot, then open that snapshot through the current production loader.
|
|
395
|
+
* The authoritative database is opened read-only and is never migrated.
|
|
396
|
+
*/
|
|
397
|
+
async function validateSqliteInPlaceDryRun(options, classification, now) {
|
|
398
|
+
const migration = classification.sqliteMigration;
|
|
399
|
+
const stagingRoot = mkdtempSync(join(tmpdir(), "yui-sqlite-upgrade-dry-run-"));
|
|
400
|
+
const stagedDatabasePath = join(stagingRoot, COMMITTED_DATABASE_FILENAME);
|
|
401
|
+
let source;
|
|
402
|
+
let staged;
|
|
403
|
+
let loader;
|
|
404
|
+
try {
|
|
405
|
+
source = new Database(join(options.home, COMMITTED_DATABASE_FILENAME), {
|
|
406
|
+
readonly: true,
|
|
407
|
+
fileMustExist: true
|
|
408
|
+
});
|
|
409
|
+
source.pragma("busy_timeout = 5000");
|
|
410
|
+
await source.backup(stagedDatabasePath);
|
|
411
|
+
source.close();
|
|
412
|
+
source = undefined;
|
|
413
|
+
staged = new Database(stagedDatabasePath);
|
|
414
|
+
staged.pragma("journal_mode = WAL");
|
|
415
|
+
staged.pragma("synchronous = FULL");
|
|
416
|
+
staged.pragma("foreign_keys = ON");
|
|
417
|
+
const applied = migrateSqliteSchema(staged, { mode: "apply" }).applied;
|
|
418
|
+
if (applied.join(",") !== migration.pendingVersions.join(",")) {
|
|
419
|
+
throw new Error(`staged SQLite migration applied versions ${applied.join(",") || "none"}; `
|
|
420
|
+
+ `expected ${migration.pendingVersions.join(",")}`);
|
|
421
|
+
}
|
|
422
|
+
staged.close();
|
|
423
|
+
staged = undefined;
|
|
424
|
+
loader = new SqliteTaskStore(stagingRoot);
|
|
425
|
+
const quickCheck = loader.databaseHandle().pragma("quick_check", { simple: true });
|
|
426
|
+
if (quickCheck !== "ok") {
|
|
427
|
+
throw new Error(`staged SQLite quick_check returned ${String(quickCheck)}`);
|
|
428
|
+
}
|
|
429
|
+
loader.close();
|
|
430
|
+
loader = undefined;
|
|
431
|
+
return {
|
|
432
|
+
outcome: "dry-run",
|
|
433
|
+
classification,
|
|
434
|
+
report: sqliteInPlaceReport(options.latest, migration, "dry-run", now)
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
return withClassification({
|
|
439
|
+
outcome: "blocked",
|
|
440
|
+
stage: "validate",
|
|
441
|
+
message: `SQLite dry-run validation failed: ${messageOf(error)}`,
|
|
442
|
+
action: "The authoritative database was not changed. Resolve the reported migration or loader failure, then retry the dry run."
|
|
443
|
+
}, classification);
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
try {
|
|
447
|
+
loader?.close();
|
|
448
|
+
}
|
|
449
|
+
catch { /* best-effort disposable cleanup */ }
|
|
450
|
+
try {
|
|
451
|
+
staged?.close();
|
|
452
|
+
}
|
|
453
|
+
catch { /* best-effort disposable cleanup */ }
|
|
454
|
+
try {
|
|
455
|
+
source?.close();
|
|
456
|
+
}
|
|
457
|
+
catch { /* best-effort disposable cleanup */ }
|
|
458
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/** Acquire a local write fence or prove the exact parent-owned fence exists. */
|
|
462
|
+
function acquireUpgradeAdmission(options, reason, now, callerPid) {
|
|
463
|
+
const externalOwnerPid = options.externalUpgradeFenceOwnerPid;
|
|
464
|
+
if (externalOwnerPid !== undefined) {
|
|
465
|
+
const fence = readUpgradeFence(options.home);
|
|
466
|
+
if (!isPositivePid(externalOwnerPid) || fence?.ownerPid !== externalOwnerPid) {
|
|
467
|
+
throw new UpgradeFenceError(`expected parent-owned upgrade fence for PID ${String(externalOwnerPid)}`);
|
|
468
|
+
}
|
|
469
|
+
return () => { };
|
|
470
|
+
}
|
|
471
|
+
return placeUpgradeFence(options.home, {
|
|
472
|
+
reason,
|
|
473
|
+
createdAt: now().toISOString(),
|
|
474
|
+
ownerPid: callerPid
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
async function executeSqliteInPlaceUpgrade(options, classification, now, callerPid) {
|
|
478
|
+
const { home } = options;
|
|
479
|
+
const migration = classification.sqliteMigration;
|
|
480
|
+
let releaseFence;
|
|
481
|
+
try {
|
|
482
|
+
releaseFence = acquireUpgradeAdmission(options, "SQLite schema migration in progress", now, callerPid);
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
if (!(error instanceof UpgradeFenceError))
|
|
486
|
+
throw error;
|
|
487
|
+
return withClassification({
|
|
488
|
+
outcome: "blocked",
|
|
489
|
+
stage: "coordination",
|
|
490
|
+
message: `Upgrade coordination could not be acquired: ${error.message}`,
|
|
491
|
+
action: "Wait for the current maintenance operation to finish, then retry."
|
|
492
|
+
}, classification);
|
|
493
|
+
}
|
|
494
|
+
const externallyQuiesced = options.controllerLifecycle === "externally-quiesced";
|
|
495
|
+
let controllerWasRunning = false;
|
|
496
|
+
let controllerStopConfirmed = false;
|
|
497
|
+
let controllerIdentity;
|
|
498
|
+
let committed = false;
|
|
499
|
+
let result;
|
|
500
|
+
let unexpected;
|
|
501
|
+
try {
|
|
502
|
+
if (!externallyQuiesced) {
|
|
503
|
+
const controllerStatus = options.controllerStatus
|
|
504
|
+
?? ((targetHome) => defaultControllerStatus(targetHome, options.controllerOptions));
|
|
505
|
+
let status;
|
|
506
|
+
try {
|
|
507
|
+
status = await controllerStatus(home);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
result = withClassification(controllerLifecycleBlocker("Controller status could not be verified", error), classification);
|
|
511
|
+
}
|
|
512
|
+
if (result === undefined && !isControllerLifecycleStatus(status)) {
|
|
513
|
+
result = withClassification(controllerLifecycleBlocker("Controller status was malformed", new Error("expected a boolean running field")), classification);
|
|
514
|
+
}
|
|
515
|
+
if (result === undefined && status.running && !isControllerLaunchIdentity(status.identity)) {
|
|
516
|
+
result = withClassification(controllerLifecycleBlocker("Controller launch identity could not be authenticated", new Error("executable/argv/version identity is unavailable")), classification);
|
|
517
|
+
}
|
|
518
|
+
if (result === undefined && status.running && !isPositivePid(status.pid)) {
|
|
519
|
+
result = withClassification(controllerLifecycleBlocker("Controller PID could not be authenticated", new Error("a positive status PID is unavailable for fenced stop")), classification);
|
|
520
|
+
}
|
|
521
|
+
if (result === undefined) {
|
|
522
|
+
controllerWasRunning = status.running;
|
|
523
|
+
controllerIdentity = status.running ? status.identity : undefined;
|
|
524
|
+
controllerStopConfirmed = !controllerWasRunning;
|
|
525
|
+
}
|
|
526
|
+
if (result === undefined && controllerWasRunning) {
|
|
527
|
+
const stopController = options.stopController
|
|
528
|
+
?? ((targetHome, expectedPid) => defaultStopController(targetHome, expectedPid, options.controllerOptions));
|
|
529
|
+
try {
|
|
530
|
+
const expectedPid = status.pid;
|
|
531
|
+
const stopped = await stopController(home, expectedPid);
|
|
532
|
+
if (!confirmedControllerStopped(stopped, expectedPid)) {
|
|
533
|
+
result = withClassification(controllerLifecycleBlocker("Controller stop did not confirm a drained process", new Error(`stop did not confirm captured PID ${expectedPid} with stopped:true`)), classification);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
controllerStopConfirmed = true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
result = withClassification(controllerLifecycleBlocker("Controller stop/drain failed", error), classification);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
controllerStopConfirmed = true;
|
|
546
|
+
}
|
|
547
|
+
// The first inventory is a cheap/read-only preflight. Re-read after the
|
|
548
|
+
// Controller has fully drained so a lifecycle request that committed while
|
|
549
|
+
// the fence was being placed cannot slip through on stale evidence.
|
|
550
|
+
if (result === undefined) {
|
|
551
|
+
const finalInventory = await readOfflineInventory(options, home);
|
|
552
|
+
if (finalInventory.total > 0) {
|
|
553
|
+
result = withClassification(offlineInventoryBlocker(finalInventory, true), classification);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (result === undefined) {
|
|
557
|
+
try {
|
|
558
|
+
result = withUpgradeCoordinationLock(home, () => {
|
|
559
|
+
const quiesce = verifyQuiesced(home, callerPid);
|
|
560
|
+
if (quiesce !== null)
|
|
561
|
+
return withClassification(quiesce, classification);
|
|
562
|
+
const db = new Database(join(home, COMMITTED_DATABASE_FILENAME));
|
|
563
|
+
try {
|
|
564
|
+
db.pragma("journal_mode = WAL");
|
|
565
|
+
db.pragma("synchronous = FULL");
|
|
566
|
+
db.pragma("foreign_keys = ON");
|
|
567
|
+
db.pragma("busy_timeout = 5000");
|
|
568
|
+
const apply = db.transaction(() => {
|
|
569
|
+
// BEGIN IMMEDIATE waits for every older SQLite writer. With the
|
|
570
|
+
// upgrade fence still held, the state read here is the final
|
|
571
|
+
// authoritative durable gate for the same schema transaction.
|
|
572
|
+
const durableInventory = inspectSqliteDurableUpgradeInventory(home, db);
|
|
573
|
+
if (durableInventory.total > 0) {
|
|
574
|
+
return withClassification(offlineInventoryBlocker(durableInventory, true), classification);
|
|
575
|
+
}
|
|
576
|
+
migrateSqliteSchema(db, { mode: "apply" });
|
|
577
|
+
return {
|
|
578
|
+
outcome: "upgraded",
|
|
579
|
+
classification,
|
|
580
|
+
migrationMode: "in-place",
|
|
581
|
+
report: sqliteInPlaceReport(options.latest, migration, "execute", now)
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
const applied = apply.immediate();
|
|
585
|
+
if (applied.outcome === "upgraded")
|
|
586
|
+
committed = true;
|
|
587
|
+
return applied;
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
db.close();
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
unexpected = error;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
finally {
|
|
600
|
+
try {
|
|
601
|
+
releaseFence();
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
if (unexpected === undefined)
|
|
605
|
+
unexpected = error;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (unexpected !== undefined) {
|
|
609
|
+
if (committed) {
|
|
610
|
+
return withClassification({
|
|
611
|
+
outcome: "blocked",
|
|
612
|
+
stage: "post-verify",
|
|
613
|
+
message: `SQLite migration committed but completion failed: ${messageOf(unexpected)}`,
|
|
614
|
+
action: "Do not restore the old Controller. Re-run this Yui version; the SQLite migration ledger is the commit record.",
|
|
615
|
+
storageCommitted: true
|
|
616
|
+
}, classification);
|
|
617
|
+
}
|
|
618
|
+
if (controllerWasRunning && controllerStopConfirmed) {
|
|
619
|
+
await restoreController(home, options, unexpected, controllerIdentity);
|
|
620
|
+
}
|
|
621
|
+
throw unexpected;
|
|
622
|
+
}
|
|
623
|
+
if (result === undefined)
|
|
624
|
+
throw new Error("SQLite in-place upgrade did not produce a result.");
|
|
625
|
+
if (result.outcome === "upgraded" && !externallyQuiesced && controllerWasRunning) {
|
|
626
|
+
try {
|
|
627
|
+
await (options.startController
|
|
628
|
+
?? ((targetHome) => defaultStartController(targetHome, options.controllerOptions)))(home);
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
return withClassification({
|
|
632
|
+
outcome: "blocked",
|
|
633
|
+
stage: "post-verify",
|
|
634
|
+
message: `SQLite migration committed but the replacement Controller did not start: ${messageOf(error)}`,
|
|
635
|
+
action: "Do not restore the old Controller. Start the current Yui Controller; the SQLite migration ledger already committed.",
|
|
636
|
+
storageCommitted: true
|
|
637
|
+
}, classification);
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
if (result.outcome === "blocked"
|
|
642
|
+
&& !committed
|
|
643
|
+
&& !externallyQuiesced
|
|
644
|
+
&& controllerWasRunning
|
|
645
|
+
&& controllerStopConfirmed) {
|
|
646
|
+
await restoreController(home, options, new Error(result.message), controllerIdentity);
|
|
647
|
+
}
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
function sqliteInPlaceReport(latest, migration, mode, now) {
|
|
651
|
+
const detail = `SQLite schema ${migration.currentVersion}->${migration.targetVersion}; `
|
|
652
|
+
+ `pending versions ${migration.pendingVersions.join(", ")}`;
|
|
653
|
+
if (mode === "dry-run") {
|
|
654
|
+
return {
|
|
655
|
+
outcome: "dry-run",
|
|
656
|
+
mode,
|
|
657
|
+
source: latest,
|
|
658
|
+
target: latest,
|
|
659
|
+
steps: [],
|
|
660
|
+
effects: [],
|
|
661
|
+
derived: { rebuiltEffects: [] },
|
|
662
|
+
validation: {
|
|
663
|
+
checks: [{
|
|
664
|
+
name: "SQLite staged migration and loader gate",
|
|
665
|
+
outcome: "passed",
|
|
666
|
+
detail: `${detail}; snapshot migrated and reopened by the current SQLite loader`
|
|
667
|
+
}]
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
outcome: "migrated",
|
|
673
|
+
mode,
|
|
674
|
+
source: latest,
|
|
675
|
+
target: latest,
|
|
676
|
+
steps: [],
|
|
677
|
+
effects: [],
|
|
678
|
+
derived: { rebuiltEffects: [] },
|
|
679
|
+
validation: {
|
|
680
|
+
checks: [{ name: "SQLite migration transaction", outcome: "passed", detail }]
|
|
681
|
+
},
|
|
682
|
+
switch: {
|
|
683
|
+
status: "switched",
|
|
684
|
+
detail: "SQLite schema migrated in place; no database copy or rebuild was created."
|
|
685
|
+
},
|
|
686
|
+
completedAt: now().toISOString()
|
|
687
|
+
};
|
|
688
|
+
}
|
|
360
689
|
/** Dry run: validate through the staged gate, then discard; never switch. */
|
|
361
690
|
function dryRun(options, classification, target) {
|
|
362
691
|
// Refuse to reuse a stale staging directory from an interrupted run.
|
|
@@ -390,11 +719,7 @@ async function execute(options, classification, target, callerPid, now) {
|
|
|
390
719
|
// the other upgrader's fence.
|
|
391
720
|
let releaseFence;
|
|
392
721
|
try {
|
|
393
|
-
releaseFence =
|
|
394
|
-
reason: "storage upgrade in progress",
|
|
395
|
-
createdAt: now().toISOString(),
|
|
396
|
-
ownerPid: callerPid
|
|
397
|
-
});
|
|
722
|
+
releaseFence = acquireUpgradeAdmission(options, "storage upgrade in progress", now, callerPid);
|
|
398
723
|
}
|
|
399
724
|
catch (error) {
|
|
400
725
|
if (!(error instanceof UpgradeFenceError))
|
|
@@ -615,11 +940,7 @@ async function executePseudoLayout7Repair(options, classification, now) {
|
|
|
615
940
|
const callerPid = options.callerPid ?? process.pid;
|
|
616
941
|
let releaseFence;
|
|
617
942
|
try {
|
|
618
|
-
releaseFence =
|
|
619
|
-
reason: "storage repair in progress",
|
|
620
|
-
createdAt: now().toISOString(),
|
|
621
|
-
ownerPid: callerPid
|
|
622
|
-
});
|
|
943
|
+
releaseFence = acquireUpgradeAdmission(options, "storage repair in progress", now, callerPid);
|
|
623
944
|
}
|
|
624
945
|
catch (error) {
|
|
625
946
|
if (!(error instanceof UpgradeFenceError))
|
|
@@ -4,6 +4,7 @@ import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
|
|
|
4
4
|
import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
5
5
|
import { reviewFindingLedgerWriteFailedFromEvents } from "../review/reviewFindingLedger.js";
|
|
6
6
|
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
|
|
7
|
+
import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
7
8
|
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
8
9
|
const ACTIVE_JOB_STATUSES = new Set([
|
|
9
10
|
"queued",
|
|
@@ -22,7 +23,8 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
22
23
|
const advisories = [];
|
|
23
24
|
const { task } = facts;
|
|
24
25
|
const findingsGate = options.findingsGate ?? true;
|
|
25
|
-
const
|
|
26
|
+
const taskFinalReviewContract = resolveRecordedTaskFinalReviewContract(task.id, facts.workItems, facts.reviewRounds, facts.taskFinalReviewContractEvents)?.effective;
|
|
27
|
+
const taskFinalReviewRequired = taskFinalReviewContract !== undefined;
|
|
26
28
|
// A pending/running Task-final Review must be resumed or blocked first.
|
|
27
29
|
for (const round of facts.reviewRounds) {
|
|
28
30
|
if ((round.scope ?? "work-item") !== "task")
|
|
@@ -43,11 +45,13 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
43
45
|
// escalations remain audit evidence, but a later completed full Review (or
|
|
44
46
|
// accepted delta) supersedes them instead of blocking completion forever.
|
|
45
47
|
const latestCompletedTaskReview = facts.reviewRounds
|
|
46
|
-
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
49
|
+
&& (taskFinalReviewContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalReviewContract))
|
|
50
|
+
&& isSemanticReviewRound(round, {
|
|
51
|
+
listAgentRuns: () => facts.agentRuns,
|
|
52
|
+
listReviewFindings: () => facts.reviewFindings,
|
|
53
|
+
listEvents: () => facts.events
|
|
54
|
+
})))
|
|
51
55
|
.slice()
|
|
52
56
|
.sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
53
57
|
|| left.id.localeCompare(right.id, undefined, { numeric: true })))
|