@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
package/dist/cli/updatePorts.js
CHANGED
|
@@ -34,10 +34,13 @@ import { accessSync, constants, existsSync, mkdtempSync, readFileSync, realpathS
|
|
|
34
34
|
import { tmpdir } from "node:os";
|
|
35
35
|
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
36
36
|
import { fileURLToPath } from "node:url";
|
|
37
|
+
import Database from "better-sqlite3";
|
|
37
38
|
import { runtimeError } from "../errors/cliError.js";
|
|
38
39
|
import { isConcreteVersion } from "../domain/validation.js";
|
|
39
40
|
import { STORAGE_DOCTOR_CHECK_NAMES } from "../doctor/doctor.js";
|
|
41
|
+
import { acquireHandoverLock } from "../release/runtimeRelease.js";
|
|
40
42
|
import { inspectStorageSchema } from "../storage/storageSchema.js";
|
|
43
|
+
import { placeUpgradeFence } from "../storage/upgradeFence.js";
|
|
41
44
|
import { correlateUpgradeReceipt } from "../storage/upgrade/upgradeOrchestrator.js";
|
|
42
45
|
import { readSwitchProgress } from "../storage/upgrade/switchProgress.js";
|
|
43
46
|
const PACKAGE_NAME = "@zq-silk/yui";
|
|
@@ -91,8 +94,29 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
91
94
|
// deliberately not persisted as a retry or recovery protocol.
|
|
92
95
|
let verifiedActivatedBinary;
|
|
93
96
|
let verifiedActivatedVersion;
|
|
97
|
+
let storageFenceOwnerPid;
|
|
94
98
|
const stopReplacementController = (home, pid) => (stopReplacementControllerForUpdate(home, pid, environment, run));
|
|
95
99
|
return {
|
|
100
|
+
beginControllerHandover(home) {
|
|
101
|
+
return acquireHandoverLock(home).release;
|
|
102
|
+
},
|
|
103
|
+
beginStorageWriteFence(home) {
|
|
104
|
+
const release = placeUpgradeFence(home, {
|
|
105
|
+
reason: "update storage activation in progress",
|
|
106
|
+
createdAt: new Date().toISOString(),
|
|
107
|
+
ownerPid: process.pid
|
|
108
|
+
});
|
|
109
|
+
storageFenceOwnerPid = process.pid;
|
|
110
|
+
let released = false;
|
|
111
|
+
return () => {
|
|
112
|
+
if (released)
|
|
113
|
+
return;
|
|
114
|
+
release();
|
|
115
|
+
released = true;
|
|
116
|
+
if (storageFenceOwnerPid === process.pid)
|
|
117
|
+
storageFenceOwnerPid = undefined;
|
|
118
|
+
};
|
|
119
|
+
},
|
|
96
120
|
stage(version) {
|
|
97
121
|
// A caller that names a version (the release workflow, which freezes the
|
|
98
122
|
// exact version in its plan) installs THAT version — never a moving
|
|
@@ -151,7 +175,10 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
151
175
|
env: {
|
|
152
176
|
...environment,
|
|
153
177
|
YUI_HOME: home,
|
|
154
|
-
YUI_UPDATE_EXTERNALLY_QUIESCED: "1"
|
|
178
|
+
YUI_UPDATE_EXTERNALLY_QUIESCED: "1",
|
|
179
|
+
...(storageFenceOwnerPid === undefined
|
|
180
|
+
? {}
|
|
181
|
+
: { YUI_UPDATE_HANDOVER_OWNER_PID: String(storageFenceOwnerPid) })
|
|
155
182
|
},
|
|
156
183
|
shell: false
|
|
157
184
|
});
|
|
@@ -219,6 +246,7 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
219
246
|
// on-disk schema and report `switched: false` so the caller re-probes the
|
|
220
247
|
// real state instead of giving a recovery instruction from a stale receipt.
|
|
221
248
|
const schema = inspectStorageSchema(home);
|
|
249
|
+
const sqliteSchemaHead = inspectSqliteSchemaHead(home);
|
|
222
250
|
// A crash mid-switch leaves a durable progress marker. A marker of ANY phase
|
|
223
251
|
// — `backing-up`, `promoting`, or `interrupted` — is only actionable as an
|
|
224
252
|
// interrupted switch when the FILESYSTEM still corroborates it: the backup
|
|
@@ -243,6 +271,7 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
243
271
|
switched: false,
|
|
244
272
|
interrupted: true,
|
|
245
273
|
schemaCurrent: false,
|
|
274
|
+
...(sqliteSchemaHead === undefined ? {} : { sqliteSchemaHead }),
|
|
246
275
|
...(progress.backupPath === undefined ? {} : { backupPath: progress.backupPath })
|
|
247
276
|
};
|
|
248
277
|
}
|
|
@@ -252,12 +281,17 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
252
281
|
}
|
|
253
282
|
const correlation = correlateUpgradeReceipt(home);
|
|
254
283
|
if (!correlation.corresponds) {
|
|
255
|
-
return {
|
|
284
|
+
return {
|
|
285
|
+
switched: false,
|
|
286
|
+
schemaCurrent: schema.status === "current",
|
|
287
|
+
...(sqliteSchemaHead === undefined ? {} : { sqliteSchemaHead })
|
|
288
|
+
};
|
|
256
289
|
}
|
|
257
290
|
const receipt = correlation.receipt;
|
|
258
291
|
return {
|
|
259
292
|
switched: true,
|
|
260
293
|
schemaCurrent: schema.status === "current",
|
|
294
|
+
...(sqliteSchemaHead === undefined ? {} : { sqliteSchemaHead }),
|
|
261
295
|
...(receipt.backupPath === undefined ? {} : { backupPath: receipt.backupPath })
|
|
262
296
|
};
|
|
263
297
|
},
|
|
@@ -291,6 +325,39 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
291
325
|
}
|
|
292
326
|
};
|
|
293
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Read the generic durable SQLite ledger without asking the old parent binary
|
|
330
|
+
* to understand the staged release's migration registry. The staged preflight
|
|
331
|
+
* supplies the validated source and target heads used to interpret this value.
|
|
332
|
+
*/
|
|
333
|
+
function inspectSqliteSchemaHead(home) {
|
|
334
|
+
const path = join(home, "yui.db");
|
|
335
|
+
if (!existsSync(path))
|
|
336
|
+
return undefined;
|
|
337
|
+
const db = new Database(path, { readonly: true, fileMustExist: true });
|
|
338
|
+
try {
|
|
339
|
+
db.pragma("query_only = ON");
|
|
340
|
+
const rows = db.prepare("SELECT version, checksum FROM schema_migrations ORDER BY version").all();
|
|
341
|
+
if (rows.length === 0)
|
|
342
|
+
throw new Error("SQLite migration ledger is empty.");
|
|
343
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
344
|
+
const row = rows[index];
|
|
345
|
+
if (row.version !== index + 1
|
|
346
|
+
|| typeof row.checksum !== "string"
|
|
347
|
+
|| row.checksum.length === 0) {
|
|
348
|
+
throw new Error("SQLite migration ledger is not a contiguous checksummed prefix.");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const head = rows.at(-1);
|
|
352
|
+
return {
|
|
353
|
+
version: head.version,
|
|
354
|
+
checksum: head.checksum
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
db.close();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
294
361
|
const UPDATE_CLI_PATH = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
295
362
|
const UPDATE_CLIENT_RUNTIME_PATH = fileURLToPath(new URL("../controller/clientRuntime.js", import.meta.url));
|
|
296
363
|
const UPDATE_CONTROLLER_RECONCILIATION_PATH = fileURLToPath(new URL("../controller/updateReconciliation.js", import.meta.url));
|
|
@@ -581,7 +648,17 @@ function runControllerCommand(home, environment, spawn, method, cliBinary) {
|
|
|
581
648
|
const args = cliBinary === undefined
|
|
582
649
|
? [UPDATE_CLI_PATH, "--json", "controller", method]
|
|
583
650
|
: ["--json", "controller", method];
|
|
584
|
-
const result = spawn(command, args, {
|
|
651
|
+
const result = spawn(command, args, {
|
|
652
|
+
cwd: process.cwd(),
|
|
653
|
+
env: {
|
|
654
|
+
...environment,
|
|
655
|
+
YUI_HOME: home,
|
|
656
|
+
// This exact lifecycle child is part of the update process that owns
|
|
657
|
+
// the handover lock. Managed Sessions never receive this bypass.
|
|
658
|
+
YUI_UPDATE_HANDOVER_OWNER_PID: String(process.pid)
|
|
659
|
+
},
|
|
660
|
+
shell: false
|
|
661
|
+
});
|
|
585
662
|
if (result.error !== undefined || result.status !== 0) {
|
|
586
663
|
const error = new Error(`Controller ${method} failed (exit ${result.status ?? "null"}).`);
|
|
587
664
|
const code = controllerErrorCodeFromResult(result);
|
|
@@ -828,11 +905,14 @@ function interpretPreflight(result) {
|
|
|
828
905
|
...(data.sceneUnchanged === true ? { sceneUnchanged: true } : {})
|
|
829
906
|
};
|
|
830
907
|
}
|
|
831
|
-
/** Strictly parse the
|
|
908
|
+
/** Strictly parse the green states of the internal update preflight. */
|
|
832
909
|
function parseUpdatePreflightResult(data) {
|
|
833
910
|
const status = data.status;
|
|
834
911
|
const stepCount = data.stepCount;
|
|
835
|
-
if ((status !== "already-current"
|
|
912
|
+
if ((status !== "already-current"
|
|
913
|
+
&& status !== "compatible"
|
|
914
|
+
&& status !== "in-place-migration"
|
|
915
|
+
&& status !== "migration-required")
|
|
836
916
|
|| !Number.isSafeInteger(stepCount)
|
|
837
917
|
|| stepCount < 0) {
|
|
838
918
|
return null;
|
|
@@ -857,11 +937,57 @@ function parseUpdatePreflightResult(data) {
|
|
|
857
937
|
return { status };
|
|
858
938
|
const evidence = status === "compatible"
|
|
859
939
|
? `${stepCount} compatible step(s) classified and the compatible source validated in memory`
|
|
860
|
-
:
|
|
940
|
+
: status === "in-place-migration"
|
|
941
|
+
? `${stepCount} SQLite migration step(s) classified for one in-place transaction and the offline runtime inventory confirmed clear`
|
|
942
|
+
: `${stepCount} offline migration step(s) classified and the offline runtime inventory confirmed clear`;
|
|
943
|
+
const summary = `${evidence}. `
|
|
944
|
+
+ "No staged Home or staged-output loader validation was performed during update preflight.";
|
|
945
|
+
if (status === "in-place-migration") {
|
|
946
|
+
const sqliteMigration = parseSqliteMigrationBoundary(data.sqliteMigration, stepCount);
|
|
947
|
+
if (sqliteMigration === null)
|
|
948
|
+
return null;
|
|
949
|
+
return {
|
|
950
|
+
status,
|
|
951
|
+
summary,
|
|
952
|
+
sqliteMigration
|
|
953
|
+
};
|
|
954
|
+
}
|
|
861
955
|
return {
|
|
862
956
|
status,
|
|
863
|
-
summary
|
|
864
|
-
|
|
957
|
+
summary
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function parseSqliteMigrationBoundary(value, stepCount) {
|
|
961
|
+
if (!isRecord(value))
|
|
962
|
+
return null;
|
|
963
|
+
const currentVersion = value.currentVersion;
|
|
964
|
+
const currentChecksum = value.currentChecksum;
|
|
965
|
+
const targetVersion = value.targetVersion;
|
|
966
|
+
const targetChecksum = value.targetChecksum;
|
|
967
|
+
const pendingVersions = value.pendingVersions;
|
|
968
|
+
if (!Number.isSafeInteger(currentVersion)
|
|
969
|
+
|| currentVersion < 1
|
|
970
|
+
|| typeof currentChecksum !== "string"
|
|
971
|
+
|| currentChecksum.length === 0
|
|
972
|
+
|| !Number.isSafeInteger(targetVersion)
|
|
973
|
+
|| targetVersion <= currentVersion
|
|
974
|
+
|| typeof targetChecksum !== "string"
|
|
975
|
+
|| targetChecksum.length === 0
|
|
976
|
+
|| !Array.isArray(pendingVersions)
|
|
977
|
+
|| pendingVersions.length !== stepCount
|
|
978
|
+
|| targetVersion !== currentVersion + stepCount
|
|
979
|
+
|| pendingVersions.some((version, index) => !Number.isSafeInteger(version) || version !== currentVersion + index + 1)) {
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
current: {
|
|
984
|
+
version: currentVersion,
|
|
985
|
+
checksum: currentChecksum
|
|
986
|
+
},
|
|
987
|
+
target: {
|
|
988
|
+
version: targetVersion,
|
|
989
|
+
checksum: targetChecksum
|
|
990
|
+
}
|
|
865
991
|
};
|
|
866
992
|
}
|
|
867
993
|
function parseUpdateBlockers(value) {
|
|
@@ -933,6 +1059,9 @@ function interpretActivation(result) {
|
|
|
933
1059
|
if (outcome === "already-current")
|
|
934
1060
|
return { status: "already-current" };
|
|
935
1061
|
if (outcome === "upgraded") {
|
|
1062
|
+
if (data.migrationMode === "in-place" && data.backupPath === undefined) {
|
|
1063
|
+
return { status: "migrated-in-place" };
|
|
1064
|
+
}
|
|
936
1065
|
const backupPath = data.backupPath;
|
|
937
1066
|
if (typeof backupPath !== "string"
|
|
938
1067
|
|| backupPath.length === 0
|
|
@@ -35,6 +35,9 @@ export async function runUpgradeCommand(args, home, options = {}) {
|
|
|
35
35
|
...(options.controllerLifecycle === undefined
|
|
36
36
|
? {}
|
|
37
37
|
: { controllerLifecycle: options.controllerLifecycle }),
|
|
38
|
+
...(options.externalUpgradeFenceOwnerPid === undefined
|
|
39
|
+
? {}
|
|
40
|
+
: { externalUpgradeFenceOwnerPid: options.externalUpgradeFenceOwnerPid }),
|
|
38
41
|
...(options.now === undefined ? {} : { now: options.now })
|
|
39
42
|
});
|
|
40
43
|
return {
|
|
@@ -68,28 +71,35 @@ export function renderUpgradeResult(result, mode) {
|
|
|
68
71
|
? "four-state classification"
|
|
69
72
|
: result.status === "compatible"
|
|
70
73
|
? "four-state classification plus compatible-source validation"
|
|
71
|
-
:
|
|
74
|
+
: result.status === "in-place-migration"
|
|
75
|
+
? "SQLite ledger classification plus a clear offline runtime inventory"
|
|
76
|
+
: "four-state classification plus a clear offline runtime inventory";
|
|
72
77
|
return `${header}\nUpdate preflight: ${result.status} (${result.stepCount} step(s)); `
|
|
73
78
|
+ `${evidence}. No staged Home was created, `
|
|
74
79
|
+ "no staged-output loader validation was performed, and storage was not switched.";
|
|
75
80
|
}
|
|
76
81
|
case "dry-run": {
|
|
77
|
-
const steps = result.
|
|
82
|
+
const steps = result.classification.sqliteMigration?.pendingVersions.length
|
|
83
|
+
?? (result.report.outcome === "dry-run" ? result.report.steps.length : 0);
|
|
78
84
|
return `${header}\nDry run: validated ${steps} migration step(s) through the loader gate. `
|
|
79
85
|
+ "Staged output discarded; storage was not switched.";
|
|
80
86
|
}
|
|
81
87
|
case "upgraded":
|
|
82
|
-
return
|
|
83
|
-
|
|
88
|
+
return result.migrationMode === "in-place"
|
|
89
|
+
? `${header}\nUpgraded SQLite in place in one transaction; no database rebuild or backup copy was created.`
|
|
90
|
+
: `${header}\nUpgraded storage. Original Home backed up at `
|
|
91
|
+
+ `${result.backupPath ?? "(unspecified)"}.`;
|
|
84
92
|
case "blocked": {
|
|
85
93
|
// Most blockers guarantee the source is untouched. A partial switch or a
|
|
86
94
|
// post-switch ambiguity explicitly carries the committed boundary and
|
|
87
95
|
// named recovery evidence; never print a false unchanged claim there.
|
|
88
|
-
const unchangedNote = result.
|
|
89
|
-
? "The
|
|
90
|
-
: result.
|
|
91
|
-
? "The switch
|
|
92
|
-
:
|
|
96
|
+
const unchangedNote = result.storageCommitted === true
|
|
97
|
+
? "The SQLite transaction committed in place; the old Controller was not restored (see Action)."
|
|
98
|
+
: result.switchCommitted === true
|
|
99
|
+
? "The storage switch committed, but post-switch completion is ambiguous; the old Controller was not restored (see Action and recovery evidence)."
|
|
100
|
+
: result.stage === "switch-ambiguous"
|
|
101
|
+
? "The switch did not complete and could not be rolled back; the authoritative Home is NOT intact (see Action)."
|
|
102
|
+
: "Storage was not switched; the authoritative Home is unchanged.";
|
|
93
103
|
return [
|
|
94
104
|
header,
|
|
95
105
|
`${mode === "dry-run" ? "Dry run" : mode === "update-preflight" ? "Update preflight" : "Upgrade"} blocked at ${result.stage}: ${result.message}`,
|