@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.
- package/ARCHITECTURE.md +27 -12
- package/README.md +85 -61
- package/dist/cli/commandCatalog.js +6 -6
- package/dist/cli/updateCommand.js +17 -9
- package/dist/cli/updateOrchestrator.js +81 -15
- package/dist/cli/updatePorts.js +72 -10
- package/dist/cli/upgradeCommand.js +104 -19
- package/dist/cli.js +2 -2
- package/dist/commands/agentCommands.js +13 -6
- package/dist/commands/controllerCommands.js +1 -1
- package/dist/commands/globalRoleCommands.js +11 -3
- package/dist/commands/roleConfiguration.js +7 -0
- package/dist/commands/roleRuntimeGuard.js +30 -0
- package/dist/commands/taskCommands.js +10 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +4 -4
- package/dist/controller/runtime.js +11 -25
- package/dist/controller/runtimeLaunchCoordinator.js +9 -30
- package/dist/controller/sessionNotify.js +5 -0
- package/dist/core/controllerServer.js +5 -5
- package/dist/doctor/doctor.js +37 -14
- package/dist/executor/agentExecutor.js +8 -11
- package/dist/executor/effectiveLaunch.js +34 -17
- package/dist/executor/fileRoleLaunchPlanner.js +11 -8
- package/dist/observability/runtimeIdentity.js +48 -50
- package/dist/release/runtimeRelease.js +9 -1
- package/dist/runtime/agentHost.js +7 -0
- package/dist/runtime/codexInteractiveHost.js +191 -0
- package/dist/runtime/exactControlPlane.js +20 -29
- package/dist/runtime/structuredProviderHost.js +35 -0
- package/dist/runtime/tmuxAdapters.js +51 -9
- package/dist/scheduler/activeRoleTurnDelivery.js +4 -4
- package/dist/scheduler/leaderWakeupProcessor.js +3 -4
- package/dist/storage/currentTaskStore.js +6 -4
- package/dist/storage/sqliteSchema.js +134 -59
- package/dist/storage/sqliteStore.js +7 -5
- package/dist/storage/storageSchema.js +92 -223
- package/dist/storage/storageVersions.js +12 -16
- package/dist/storage/upgrade/upgradeOrchestrator.js +224 -62
- package/dist/tmux/tmuxManager.js +43 -28
- package/dist/version.js +3 -3
- package/docs/task-local-identity.md +9 -9
- package/i18n/README.zh-CN.md +33 -17
- package/package.json +1 -1
- package/dist/storage/upgrade/recordVersions.js +0 -82
package/dist/cli/updatePorts.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* `npm install --global --prefix <tmp>`, so the live global install is never
|
|
8
8
|
* touched until the binary-activation step. Preflight invokes the STAGED binary's
|
|
9
9
|
* internal `yui upgrade --update-preflight` contract so the target version
|
|
10
|
-
* proves that the Home
|
|
11
|
-
* parent stops the exact old Controller, the binary is promoted
|
|
12
|
-
*
|
|
10
|
+
* proves that the Home is current or has a complete supported migration path.
|
|
11
|
+
* After the parent stops the exact old Controller, the binary is promoted,
|
|
12
|
+
* required migrations run through that same staged artifact, and the activated
|
|
13
|
+
* binary verifies the resulting current Home.
|
|
13
14
|
*
|
|
14
15
|
* Two hardening guarantees this module enforces:
|
|
15
16
|
*
|
|
@@ -146,13 +147,26 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
146
147
|
const result = run("npm", ["install", "--global", spec], { cwd: process.cwd(), env: environment, shell: false, stdio: "inherit" });
|
|
147
148
|
assertSpawnOk(result, "activate the new binary");
|
|
148
149
|
},
|
|
150
|
+
migrateStorage(staged, home) {
|
|
151
|
+
const result = run(staged.binaryPath, ["--json", "upgrade", "--update-apply"], {
|
|
152
|
+
cwd: process.cwd(),
|
|
153
|
+
env: {
|
|
154
|
+
...environment,
|
|
155
|
+
YUI_HOME: home,
|
|
156
|
+
YUI_UPDATE_HANDOVER_OWNER_PID: String(process.pid)
|
|
157
|
+
},
|
|
158
|
+
shell: false
|
|
159
|
+
});
|
|
160
|
+
return interpretStorageMigration(result);
|
|
161
|
+
},
|
|
149
162
|
verify(staged, home) {
|
|
150
163
|
// Verify the ACTUALLY-ACTIVATED global binary, not the staging path (P1-3).
|
|
151
164
|
const activeBinary = resolveGlobalBinary(environment, run);
|
|
152
165
|
if (activeBinary === null || !existsSync(activeBinary)) {
|
|
153
166
|
throw runtimeError("Post-update health check failed: could not locate the activated global `yui` binary.");
|
|
154
167
|
}
|
|
155
|
-
// 1) Health check the
|
|
168
|
+
// 1) Health check the current (possibly just migrated) Home through the
|
|
169
|
+
// activated binary.
|
|
156
170
|
// POST-VERIFY PARSES THE MACHINE-READABLE RESULT FIRST, THEN THE EXIT STATUS
|
|
157
171
|
// (R2-F2). `yui --json doctor` deliberately sets a non-zero exit when storage
|
|
158
172
|
// is unhealthy, so interpreting the exit status before the envelope would
|
|
@@ -784,21 +798,69 @@ function interpretPreflight(result) {
|
|
|
784
798
|
...(data.sceneUnchanged === true ? { sceneUnchanged: true } : {})
|
|
785
799
|
};
|
|
786
800
|
}
|
|
787
|
-
/** Accept
|
|
801
|
+
/** Accept either an exact current Home or a complete supported migration path. */
|
|
788
802
|
function parseUpdatePreflightResult(data) {
|
|
789
|
-
if (data.status !== "already-current"
|
|
803
|
+
if ((data.status !== "already-current" && data.status !== "migration-ready")
|
|
790
804
|
|| !Number.isSafeInteger(data.stepCount)
|
|
791
|
-
|| data.stepCount !== 0
|
|
792
805
|
|| !Array.isArray(data.steps)
|
|
793
|
-
|| data.steps.length !==
|
|
806
|
+
|| data.steps.length !== data.stepCount)
|
|
794
807
|
return null;
|
|
795
808
|
const homeClassification = data.classification;
|
|
796
809
|
if (!isRecord(homeClassification) || !isRecord(homeClassification.classification))
|
|
797
810
|
return null;
|
|
798
811
|
const classification = homeClassification.classification;
|
|
799
|
-
if (
|
|
812
|
+
if (data.status === "already-current") {
|
|
813
|
+
if (data.stepCount !== 0
|
|
814
|
+
|| classification.verdict !== "USABLE"
|
|
815
|
+
|| classification.status !== "current")
|
|
816
|
+
return null;
|
|
817
|
+
return { status: "already-current", stepCount: 0 };
|
|
818
|
+
}
|
|
819
|
+
if (data.stepCount <= 0
|
|
820
|
+
|| classification.verdict !== "MIGRATABLE"
|
|
821
|
+
|| classification.status !== "migration-ready"
|
|
822
|
+
|| !data.steps.every(isStorageMigrationStep))
|
|
800
823
|
return null;
|
|
801
|
-
return { status: "
|
|
824
|
+
return { status: "migration-ready", stepCount: data.stepCount };
|
|
825
|
+
}
|
|
826
|
+
function interpretStorageMigration(result) {
|
|
827
|
+
const data = parseSuccessEnvelopeData(result);
|
|
828
|
+
if (data === null) {
|
|
829
|
+
throw runtimeError("The staged binary did not return a successful storage migration result "
|
|
830
|
+
+ `(exit ${result.status ?? "null"}${result.signal === null ? "" : `, signal ${result.signal}`}).`);
|
|
831
|
+
}
|
|
832
|
+
if (data.outcome === "blocked" || data.outcome === "failed") {
|
|
833
|
+
const message = typeof data.message === "string"
|
|
834
|
+
? data.message
|
|
835
|
+
: "The staged binary refused the storage migration.";
|
|
836
|
+
const action = typeof data.action === "string" ? ` Action: ${data.action}` : "";
|
|
837
|
+
const backup = typeof data.backupPath === "string"
|
|
838
|
+
? ` Backup: ${data.backupPath}`
|
|
839
|
+
: "";
|
|
840
|
+
throw runtimeError(`${message}${action}${backup}`);
|
|
841
|
+
}
|
|
842
|
+
if (result.status !== 0) {
|
|
843
|
+
throw runtimeError(`The staged binary returned outcome=${String(data.outcome)} but exited `
|
|
844
|
+
+ `with status ${result.status ?? "null"}.`);
|
|
845
|
+
}
|
|
846
|
+
if (data.outcome === "already-current")
|
|
847
|
+
return {};
|
|
848
|
+
if (data.outcome !== "upgraded" || !isRecord(data.report)) {
|
|
849
|
+
throw runtimeError(`The staged binary returned unexpected storage migration outcome=${String(data.outcome)}.`);
|
|
850
|
+
}
|
|
851
|
+
const backupPath = data.report.backupPath;
|
|
852
|
+
if (typeof backupPath !== "string" || backupPath.length === 0) {
|
|
853
|
+
throw runtimeError("The staged binary did not report the required storage backup path.");
|
|
854
|
+
}
|
|
855
|
+
return { backupPath };
|
|
856
|
+
}
|
|
857
|
+
function isStorageMigrationStep(value) {
|
|
858
|
+
return isRecord(value)
|
|
859
|
+
&& Number.isSafeInteger(value.fromVersion)
|
|
860
|
+
&& Number.isSafeInteger(value.toVersion)
|
|
861
|
+
&& value.toVersion === value.fromVersion + 1
|
|
862
|
+
&& typeof value.name === "string"
|
|
863
|
+
&& value.name.length > 0;
|
|
802
864
|
}
|
|
803
865
|
function parseUpdateBlockers(value) {
|
|
804
866
|
if (value === undefined)
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
/** `yui upgrade`
|
|
2
|
-
import { latestStorageVersionState } from "../storage/upgrade/recordVersions.js";
|
|
1
|
+
/** `yui upgrade` plans or applies the supported linear storage migration chain. */
|
|
3
2
|
import { runStorageUpgrade } from "../storage/upgrade/upgradeOrchestrator.js";
|
|
4
|
-
import {
|
|
3
|
+
import { ensureFileTaskController, stopFileTaskController } from "../controller/clientRuntime.js";
|
|
4
|
+
import { runtimeError, usageError } from "../errors/cliError.js";
|
|
5
|
+
import { acquireHandoverLock, isForeignHandoverLockHeld, isHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
5
6
|
/**
|
|
6
7
|
* Run the upgrade command. Parses the public `[--dry-run]` form plus the staged
|
|
7
8
|
* updater's internal `--update-preflight` form, then returns rendered text,
|
|
8
9
|
* structured data, and an exit code (0 for a safe result, 5 for a blocker).
|
|
9
10
|
*/
|
|
10
|
-
export async function runUpgradeCommand(args, home) {
|
|
11
|
+
export async function runUpgradeCommand(args, home, environment = process.env) {
|
|
11
12
|
const mode = parseUpgradeArgs(args);
|
|
12
|
-
const result =
|
|
13
|
-
home,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const result = mode === "execute"
|
|
14
|
+
? await runInteractiveUpgrade(home, environment)
|
|
15
|
+
: mode === "update-apply"
|
|
16
|
+
? await runUpdateOwnedUpgrade(home, environment)
|
|
17
|
+
: await runStorageUpgrade({ home, mode });
|
|
17
18
|
return {
|
|
18
19
|
output: renderUpgradeResult(result, mode),
|
|
19
20
|
data: result,
|
|
20
|
-
exitCode: result.outcome === "blocked" ? 5 : 0
|
|
21
|
+
exitCode: result.outcome === "blocked" || result.outcome === "failed" ? 5 : 0
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
function parseUpgradeArgs(args) {
|
|
@@ -26,37 +27,121 @@ function parseUpgradeArgs(args) {
|
|
|
26
27
|
if (args.length === 1 && args[0] === "--dry-run")
|
|
27
28
|
return "dry-run";
|
|
28
29
|
// Intentionally omitted from public command help: this is the machine contract
|
|
29
|
-
// used by a staged `yui update`, not a replacement for user-facing dry-
|
|
30
|
+
// used by a staged `yui update`, not a replacement for user-facing dry-run.
|
|
30
31
|
if (args.length === 1 && args[0] === "--update-preflight")
|
|
31
32
|
return "update-preflight";
|
|
33
|
+
if (args.length === 1 && args[0] === "--update-apply")
|
|
34
|
+
return "update-apply";
|
|
32
35
|
throw usageError("Upgrade usage: yui upgrade [--dry-run]");
|
|
33
36
|
}
|
|
37
|
+
async function runInteractiveUpgrade(home, environment) {
|
|
38
|
+
const preflight = await runStorageUpgrade({ home, mode: "update-preflight" });
|
|
39
|
+
if (preflight.outcome !== "update-preflight"
|
|
40
|
+
|| preflight.status === "already-current") {
|
|
41
|
+
return runStorageUpgrade({ home, mode: "execute" });
|
|
42
|
+
}
|
|
43
|
+
const handover = acquireHandoverLock(home);
|
|
44
|
+
let controllerWasRunning = false;
|
|
45
|
+
try {
|
|
46
|
+
const stopped = await stopFileTaskController(home, {
|
|
47
|
+
environment,
|
|
48
|
+
handoverOwnerPid: process.pid
|
|
49
|
+
});
|
|
50
|
+
controllerWasRunning = stopped.stopped;
|
|
51
|
+
const result = await runStorageUpgrade({ home, mode: "execute" });
|
|
52
|
+
if (controllerWasRunning
|
|
53
|
+
&& (result.outcome === "upgraded" || result.outcome === "already-current")) {
|
|
54
|
+
try {
|
|
55
|
+
await ensureFileTaskController(home, {
|
|
56
|
+
environment,
|
|
57
|
+
handoverOwnerPid: process.pid
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw runtimeError(`Storage reached version ${result.report.targetVersion}, but the current `
|
|
62
|
+
+ `Controller could not restart: ${messageOf(error)} Backup: `
|
|
63
|
+
+ `${result.report.backupPath ?? "none"}. Keep the Home quiesced, inspect `
|
|
64
|
+
+ "Controller ownership, and start only the current Yui Controller.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
handover.release();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function runUpdateOwnedUpgrade(home, environment) {
|
|
74
|
+
const ownerText = environment.YUI_UPDATE_HANDOVER_OWNER_PID;
|
|
75
|
+
const ownerPid = ownerText === undefined ? Number.NaN : Number(ownerText);
|
|
76
|
+
if (!Number.isSafeInteger(ownerPid)
|
|
77
|
+
|| ownerPid < 1
|
|
78
|
+
|| process.ppid !== ownerPid
|
|
79
|
+
|| !isHandoverLockHeld(home)
|
|
80
|
+
|| isForeignHandoverLockHeld(home, ownerPid)) {
|
|
81
|
+
throw runtimeError("The internal update migration requires its direct parent to own the live "
|
|
82
|
+
+ "Controller handover lock.");
|
|
83
|
+
}
|
|
84
|
+
return runStorageUpgrade({ home, mode: "execute" });
|
|
85
|
+
}
|
|
34
86
|
/** Render an {@link UpgradeResult} as concise, CLI-style text. */
|
|
35
87
|
export function renderUpgradeResult(result, mode) {
|
|
36
88
|
const header = versionHeader(result);
|
|
37
89
|
switch (result.outcome) {
|
|
38
90
|
case "already-current":
|
|
39
91
|
return `${header}\nStorage is already at the current version; nothing to upgrade.`;
|
|
92
|
+
case "upgrade-plan":
|
|
93
|
+
return [
|
|
94
|
+
header,
|
|
95
|
+
`Upgrade plan: ${renderSteps(result.report.steps)}. Storage was not modified.`
|
|
96
|
+
].join("\n");
|
|
97
|
+
case "upgraded":
|
|
98
|
+
return [
|
|
99
|
+
header,
|
|
100
|
+
`Storage upgraded through ${renderSteps(result.report.steps)}.`,
|
|
101
|
+
`Backup: ${result.report.backupPath ?? "none"}`
|
|
102
|
+
].join("\n");
|
|
40
103
|
case "update-preflight":
|
|
41
104
|
return `${header}\nUpdate preflight: ${result.status} (${result.stepCount} steps). Storage was not modified.`;
|
|
42
105
|
case "blocked": {
|
|
43
106
|
return [
|
|
44
107
|
header,
|
|
45
|
-
`${mode === "dry-run"
|
|
108
|
+
`${mode === "dry-run"
|
|
109
|
+
? "Dry run"
|
|
110
|
+
: mode === "update-preflight"
|
|
111
|
+
? "Update preflight"
|
|
112
|
+
: "Upgrade"} blocked at ${result.stage}: ${result.message}`,
|
|
46
113
|
`Action: ${result.action}`,
|
|
47
114
|
"The authoritative Home is unchanged."
|
|
48
115
|
].join("\n");
|
|
49
116
|
}
|
|
117
|
+
case "failed":
|
|
118
|
+
return [
|
|
119
|
+
header,
|
|
120
|
+
`Upgrade failed at ${result.stage}: ${result.message}`,
|
|
121
|
+
`Action: ${result.action}`,
|
|
122
|
+
`Backup: ${result.backupPath ?? "none"}`,
|
|
123
|
+
result.sceneUnchanged
|
|
124
|
+
? "The authoritative Home was restored."
|
|
125
|
+
: "The authoritative Home may have changed; keep it quiesced."
|
|
126
|
+
].join("\n");
|
|
50
127
|
}
|
|
51
128
|
}
|
|
52
129
|
function versionHeader(result) {
|
|
53
130
|
const classification = result.classification;
|
|
54
|
-
const
|
|
55
|
-
|
|
131
|
+
const storage = classification.storageVersion === undefined
|
|
132
|
+
? "unknown"
|
|
133
|
+
: String(classification.storageVersion);
|
|
56
134
|
const verdict = classification.classification.verdict;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
135
|
+
return `Storage: ${verdict} version=${storage}/${classification.currentStorageVersion} `
|
|
136
|
+
+ `minimum=${classification.minimumSupportedStorageVersion}`;
|
|
137
|
+
}
|
|
138
|
+
function renderSteps(steps) {
|
|
139
|
+
if (steps.length === 0)
|
|
140
|
+
return "no migrations";
|
|
141
|
+
return steps
|
|
142
|
+
.map(({ fromVersion, toVersion, name }) => `${fromVersion}->${toVersion} ${name}`)
|
|
143
|
+
.join(", ");
|
|
144
|
+
}
|
|
145
|
+
function messageOf(error) {
|
|
146
|
+
return error instanceof Error ? error.message : String(error);
|
|
62
147
|
}
|
package/dist/cli.js
CHANGED
|
@@ -2092,10 +2092,10 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
|
|
|
2092
2092
|
throw usageError("Operator is not configured. Run yui setup first.");
|
|
2093
2093
|
const sessionSet = store.getGlobalRoleSessionSet(role.name);
|
|
2094
2094
|
const active = sessionSet?.sessions[sessionSet.activeAgentId];
|
|
2095
|
-
const paneRunning = tmux.
|
|
2095
|
+
const paneRunning = tmux.probeRoleStatus("operator", "operator") === "running";
|
|
2096
2096
|
if (paneRunning && active === undefined) {
|
|
2097
2097
|
throw usageError("Operator is running but its native session has not been recorded yet. "
|
|
2098
|
-
+ "
|
|
2098
|
+
+ "Record the exact native session before switching sessions.");
|
|
2099
2099
|
}
|
|
2100
2100
|
if (control.action === "resume"
|
|
2101
2101
|
&& paneRunning
|
|
@@ -2,6 +2,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
2
2
|
import { agentNotFound, usageError } from "../errors/cliError.js";
|
|
3
3
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
4
4
|
import { createConfiguredAgent, validateConfiguredAgent } from "../agent/agent.js";
|
|
5
|
+
import { LIVE_SESSION_ACKNOWLEDGEMENT_OPTION } from "./roleRuntimeGuard.js";
|
|
5
6
|
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
6
7
|
const SUPPORTED_ADAPTERS = Object.freeze(["codex", "claude"]);
|
|
7
8
|
export function runAgentCommand(args, store) {
|
|
@@ -72,7 +73,8 @@ function updateAgent(args, store) {
|
|
|
72
73
|
const [rawId, ...tail] = args;
|
|
73
74
|
const id = agentId(rawId);
|
|
74
75
|
const parsed = parseAgentOptions(tail, "update");
|
|
75
|
-
if (parsed.seen.size === 0
|
|
76
|
+
if (parsed.seen.size === 0
|
|
77
|
+
|| [...parsed.seen].every((option) => option === LIVE_SESSION_ACKNOWLEDGEMENT_OPTION)) {
|
|
76
78
|
throw usageError("Agent update requires at least one operational option.");
|
|
77
79
|
}
|
|
78
80
|
if (parsed.has("--arg") && parsed.has("--clear-args")) {
|
|
@@ -113,10 +115,13 @@ function updateAgent(args, store) {
|
|
|
113
115
|
+ "Wait for lifecycle reconciliation to finish before changing Agent launch settings.");
|
|
114
116
|
}
|
|
115
117
|
const liveSession = findNonStoppedSessionReference(tx, id);
|
|
116
|
-
if (liveSession !== null) {
|
|
117
|
-
throw usageError(
|
|
118
|
-
+ `
|
|
119
|
-
+ "
|
|
118
|
+
if (liveSession !== null && !parsed.has(LIVE_SESSION_ACKNOWLEDGEMENT_OPTION)) {
|
|
119
|
+
throw usageError(`${describeReference(liveSession)} runs a live native session (${liveSession.status}) on `
|
|
120
|
+
+ `Agent ${id}, so this change applies to its next Host activation instead of the `
|
|
121
|
+
+ "running one.\n"
|
|
122
|
+
+ `Re-run with ${LIVE_SESSION_ACKNOWLEDGEMENT_OPTION} to record the change and keep that `
|
|
123
|
+
+ "session.\n"
|
|
124
|
+
+ "Stop the affected Role session first to apply it to a fresh session instead.");
|
|
120
125
|
}
|
|
121
126
|
if (changes.adapter) {
|
|
122
127
|
const profile = findAgentProfileReference(tx, id);
|
|
@@ -273,7 +278,9 @@ function parseAgentOptions(args, mode) {
|
|
|
273
278
|
["--arg", { repeatable: true, allowOptionLikeValue: true }],
|
|
274
279
|
["--env", { repeatable: true, allowOptionLikeValue: false }]
|
|
275
280
|
]);
|
|
276
|
-
const flags = mode === "update"
|
|
281
|
+
const flags = mode === "update"
|
|
282
|
+
? new Set(["--clear-args", "--clear-env", LIVE_SESSION_ACKNOWLEDGEMENT_OPTION])
|
|
283
|
+
: new Set();
|
|
277
284
|
const seen = new Set();
|
|
278
285
|
const values = new Map();
|
|
279
286
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -201,7 +201,7 @@ export function renderRuntimeIdentitySection(input) {
|
|
|
201
201
|
` Entry digest ${build.entryDigest}`,
|
|
202
202
|
` Source commit ${build.sourceCommit}`,
|
|
203
203
|
` Node ${build.nodeVersion} (${build.platform})`,
|
|
204
|
-
` Storage
|
|
204
|
+
` Storage version ${storage.storageVersion} (status ${storage.storageStatus}, minimum ${storage.minimumStorageVersion}) · backend ${storage.configuredBackend} · worker ${storage.workerEnabled ? "on" : "off"}`,
|
|
205
205
|
` Store files state.json ${storage.physicalStateJson.present ? "present" : "absent"} · yui.db ${storage.physicalDatabase.present ? "present" : "absent"}${storage.physicalDatabase.wal ? " +WAL" : ""}${storage.physicalDatabase.present && storage.physicalDatabase.health !== UNSUPPORTED ? ` (${storage.physicalDatabase.health})` : ""}`
|
|
206
206
|
];
|
|
207
207
|
for (const finding of storage.findings) {
|
|
@@ -5,9 +5,9 @@ import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
|
5
5
|
import { activeRoleSummary, renderRoleDetails } from "../output/rolePresentation.js";
|
|
6
6
|
import { activeRoleAgentBinding, createGlobalRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateGlobalRole } from "../role/role.js";
|
|
7
7
|
import { isSystemRoleName, SYSTEM_ROLE_NAMES, systemRoleDescription } from "../role/systemRoles.js";
|
|
8
|
-
import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfileFrom, roleProfilePatch } from "./roleConfiguration.js";
|
|
8
|
+
import { hasAgentConfigOptions, hasNoRoleMutation, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfileFrom, roleProfilePatch } from "./roleConfiguration.js";
|
|
9
9
|
import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
|
|
10
|
-
import { assertRoleRuntimeMutationAllowed } from "./roleRuntimeGuard.js";
|
|
10
|
+
import { assertLiveRoleSessionAcknowledged, assertRoleRuntimeMutationAllowed, LIVE_SESSION_ACKNOWLEDGEMENT_OPTION } from "./roleRuntimeGuard.js";
|
|
11
11
|
export function runGlobalRoleCommand(args, store, options = {}) {
|
|
12
12
|
const [command, ...rest] = args;
|
|
13
13
|
switch (command) {
|
|
@@ -175,7 +175,7 @@ function updateRole(args, store, options) {
|
|
|
175
175
|
if (parsed.has("--workspace") && trimmed(parsed.one("--workspace")) === undefined) {
|
|
176
176
|
throw usageError("--workspace is required.");
|
|
177
177
|
}
|
|
178
|
-
if (
|
|
178
|
+
if (hasNoRoleMutation(parsed)) {
|
|
179
179
|
throw usageError("At least one role update option is required.");
|
|
180
180
|
}
|
|
181
181
|
const workspace = trimmed(parsed.one("--workspace"));
|
|
@@ -188,6 +188,14 @@ function updateRole(args, store, options) {
|
|
|
188
188
|
scope: "global",
|
|
189
189
|
roleName: role.name
|
|
190
190
|
}, "desired launch configuration update");
|
|
191
|
+
assertLiveRoleSessionAcknowledged({
|
|
192
|
+
sessions: tx.getGlobalRoleSessionSet(role.name),
|
|
193
|
+
roleName: role.name,
|
|
194
|
+
desiredRevision: role.launchRevision,
|
|
195
|
+
acknowledged: parsed.has(LIVE_SESSION_ACKNOWLEDGEMENT_OPTION),
|
|
196
|
+
stopCommand: "yui session stop --all",
|
|
197
|
+
endsSession: workspace !== undefined && workspace !== role.workspace
|
|
198
|
+
});
|
|
191
199
|
}
|
|
192
200
|
let bindings = role.agentBindings;
|
|
193
201
|
if (changesAgentConfig) {
|
|
@@ -42,6 +42,12 @@ const AGENT_CONFIG_OPTIONS = new Set([
|
|
|
42
42
|
...AGENT_REPEATABLE_OPTIONS.map(([option]) => option),
|
|
43
43
|
...AGENT_CLEAR_OPTIONS.map(([option]) => option)
|
|
44
44
|
]);
|
|
45
|
+
/** Options that select a target or acknowledge a fact instead of changing the Role. */
|
|
46
|
+
const NON_MUTATING_OPTIONS = ["--agent", "--yes"];
|
|
47
|
+
/** Whether the parsed options change nothing about the Role itself. */
|
|
48
|
+
export function hasNoRoleMutation(parsed) {
|
|
49
|
+
return [...parsed.seen].every((option) => NON_MUTATING_OPTIONS.includes(option));
|
|
50
|
+
}
|
|
45
51
|
export function roleOptionSpecs(input) {
|
|
46
52
|
const agentValueOptions = input.agentOptions === "execution"
|
|
47
53
|
? AGENT_VALUE_OPTIONS.filter(([option]) => option === "--model" || option === "--effort")
|
|
@@ -57,6 +63,7 @@ export function roleOptionSpecs(input) {
|
|
|
57
63
|
return new Map([
|
|
58
64
|
...(input.includeAgent === true ? [["--agent", "value"]] : []),
|
|
59
65
|
...(input.includeWorkspace === true ? [["--workspace", "value"]] : []),
|
|
66
|
+
...(input.update ? [["--yes", "flag"]] : []),
|
|
60
67
|
...PROFILE_OPTIONS,
|
|
61
68
|
...agentValueOptions,
|
|
62
69
|
...agentRepeatableOptions,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { activeLiveRoleAgentSession } from "../executor/agentExecutor.js";
|
|
2
3
|
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
3
4
|
/**
|
|
4
5
|
* Runtime launch reservation and cleanup are ownership obligations for a Role
|
|
@@ -10,3 +11,32 @@ export function assertRoleRuntimeMutationAllowed(store, owner, action) {
|
|
|
10
11
|
return;
|
|
11
12
|
throw usageError(`Role ${action} is blocked while a runtime lifecycle transition is pending or processing.`);
|
|
12
13
|
}
|
|
14
|
+
export const LIVE_SESSION_ACKNOWLEDGEMENT_OPTION = "--yes";
|
|
15
|
+
/**
|
|
16
|
+
* A live native Session keeps the launch configuration it started with. Changing
|
|
17
|
+
* the desired configuration is therefore a decision about the next Host
|
|
18
|
+
* activation, and the Agent making the change is the one that knows whether the
|
|
19
|
+
* running Session should keep going or be replaced. Report that fact once, here,
|
|
20
|
+
* instead of refusing the Turn that later resumes the Session.
|
|
21
|
+
*/
|
|
22
|
+
export function assertLiveRoleSessionAcknowledged(input) {
|
|
23
|
+
if (input.acknowledged)
|
|
24
|
+
return;
|
|
25
|
+
const session = activeLiveRoleAgentSession(input.sessions);
|
|
26
|
+
if (session === null)
|
|
27
|
+
return;
|
|
28
|
+
throw usageError([
|
|
29
|
+
input.endsSession === true
|
|
30
|
+
? `Role ${input.roleName} has a live native Session that cannot continue after this `
|
|
31
|
+
+ "change; its next Turn will need a new Session."
|
|
32
|
+
: `Role ${input.roleName} has a live native Session, so this change applies to its `
|
|
33
|
+
+ "next Host activation instead of the running one.",
|
|
34
|
+
` Agent: ${session.agentId} (${session.adapterId})`,
|
|
35
|
+
` Native Session: ${session.nativeSessionId}`,
|
|
36
|
+
` Host activation: ${session.runtimeGenerationId ?? "not recorded"}`,
|
|
37
|
+
` Session launched from desired revision r${session.effective.sourceDesiredRevision}; `
|
|
38
|
+
+ `current desired revision r${input.desiredRevision}`,
|
|
39
|
+
`Re-run with ${LIVE_SESSION_ACKNOWLEDGEMENT_OPTION} to record the change and keep the Session.`,
|
|
40
|
+
`Run \`${input.stopCommand}\` first to apply it to a fresh Session instead.`
|
|
41
|
+
].join("\n"));
|
|
42
|
+
}
|
|
@@ -50,9 +50,9 @@ import { projectWorkItemExecution } from "../execution/workItemExecutionProjecti
|
|
|
50
50
|
import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
51
51
|
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractResolution.js";
|
|
52
52
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
53
|
-
import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
|
|
53
|
+
import { hasAgentConfigOptions, hasNoRoleMutation, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
|
|
54
54
|
import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
|
|
55
|
-
import { assertRoleRuntimeMutationAllowed } from "./roleRuntimeGuard.js";
|
|
55
|
+
import { assertLiveRoleSessionAcknowledged, assertRoleRuntimeMutationAllowed, LIVE_SESSION_ACKNOWLEDGEMENT_OPTION } from "./roleRuntimeGuard.js";
|
|
56
56
|
import { runTaskContextCommand } from "./taskContextCommand.js";
|
|
57
57
|
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
58
58
|
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
@@ -1643,7 +1643,7 @@ function updateTaskRole(args, store, options) {
|
|
|
1643
1643
|
if (parsed.has("--agent") && (parsed.one("--agent")?.trim().length ?? 0) === 0) {
|
|
1644
1644
|
throw usageError("--agent is required.", usage);
|
|
1645
1645
|
}
|
|
1646
|
-
if (
|
|
1646
|
+
if (hasNoRoleMutation(parsed)) {
|
|
1647
1647
|
throw usageError("At least one role update option is required.", usage);
|
|
1648
1648
|
}
|
|
1649
1649
|
const now = clock(options);
|
|
@@ -1660,6 +1660,13 @@ function updateTaskRole(args, store, options) {
|
|
|
1660
1660
|
taskId: task.id,
|
|
1661
1661
|
roleName: role.name
|
|
1662
1662
|
}, "desired launch configuration update");
|
|
1663
|
+
assertLiveRoleSessionAcknowledged({
|
|
1664
|
+
sessions: tx.getTaskRoleSessionSet(task.id, role.name),
|
|
1665
|
+
roleName: role.name,
|
|
1666
|
+
desiredRevision: role.launchRevision,
|
|
1667
|
+
acknowledged: parsed.has(LIVE_SESSION_ACKNOWLEDGEMENT_OPTION),
|
|
1668
|
+
stopCommand: `yui task role session stop ${task.id} ${role.name} --reason "<decision>"`
|
|
1669
|
+
});
|
|
1663
1670
|
}
|
|
1664
1671
|
const profileId = parsed.one("--profile");
|
|
1665
1672
|
const agentProfile = profileId === undefined
|
|
@@ -9,7 +9,7 @@ import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
|
|
|
9
9
|
import { createTaskWake, fallbackWakeCursor, latestTaskWake } from "../scheduler/taskWake.js";
|
|
10
10
|
import { answerInputRequest } from "../input/inputRequest.js";
|
|
11
11
|
import { activeRoleAgentBinding } from "../role/role.js";
|
|
12
|
-
import { effectiveLaunchWithTaskMainWorkspace,
|
|
12
|
+
import { effectiveLaunchWithTaskMainWorkspace, roleSessionMayContinue, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
13
13
|
import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
|
|
14
14
|
import { appendTurnInput, createTurn } from "../turn/turn.js";
|
|
15
15
|
import { transportAgentResult } from "../domain/agentResultTransport.js";
|
|
@@ -2603,7 +2603,7 @@ function recordTaskRuntimeNativeSession(store, input, now) {
|
|
|
2603
2603
|
const effective = input.effective === undefined
|
|
2604
2604
|
? resolvedEffective
|
|
2605
2605
|
: validateEffectiveLaunchSnapshot(input.effective);
|
|
2606
|
-
if (!
|
|
2606
|
+
if (!roleSessionMayContinue(resolvedEffective, effective)) {
|
|
2607
2607
|
throw new Error("Reserved native Session effective launch changed before persistence.");
|
|
2608
2608
|
}
|
|
2609
2609
|
if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
|
|
@@ -2636,7 +2636,7 @@ function recordGlobalRuntimeNativeSession(store, input, now) {
|
|
|
2636
2636
|
const effective = input.effective === undefined
|
|
2637
2637
|
? resolvedEffective
|
|
2638
2638
|
: validateEffectiveLaunchSnapshot(input.effective);
|
|
2639
|
-
if (!
|
|
2639
|
+
if (!roleSessionMayContinue(resolvedEffective, effective)) {
|
|
2640
2640
|
throw new Error("Reserved global native Session effective launch changed before persistence.");
|
|
2641
2641
|
}
|
|
2642
2642
|
if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
|
|
@@ -2719,7 +2719,7 @@ function taskSessionEffective(store, taskId, roleName, agentId, existing) {
|
|
|
2719
2719
|
throw new Error(`Native Session registration does not match the effective Turn Agent: ${taskId}/${roleName}.`);
|
|
2720
2720
|
}
|
|
2721
2721
|
if (existing !== undefined) {
|
|
2722
|
-
if (!
|
|
2722
|
+
if (!roleSessionMayContinue(existing.effective, active.effective)) {
|
|
2723
2723
|
throw new Error(`Native Session effective launch does not match the active Turn: ${taskId}/${roleName}.`);
|
|
2724
2724
|
}
|
|
2725
2725
|
return existing.effective;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveRuntimeHealth, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
2
|
import { resolve } from "node:path";
|
|
4
3
|
import { isDeepStrictEqual } from "node:util";
|
|
5
4
|
import { controllerSocketPath } from "../core/controllerEndpoint.js";
|
|
@@ -7,9 +6,9 @@ import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_M
|
|
|
7
6
|
import { hasRuntimeCleanupObligation, runtimeLifecycleSignalKey, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
8
7
|
import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
|
|
9
8
|
import { activeLiveRoleAgentSession, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
|
|
10
|
-
import {
|
|
9
|
+
import { roleSessionMayContinue, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
11
10
|
import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } from "../executor/agentConfigurationCatalog.js";
|
|
12
|
-
import { isTaskOwnedWorkspace,
|
|
11
|
+
import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
13
12
|
import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
|
|
14
13
|
import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
15
14
|
import { SqliteTaskStore } from "../storage/sqliteStore.js";
|
|
@@ -216,7 +215,6 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
216
215
|
assertCurrent: (request) => {
|
|
217
216
|
assertRuntimeLaunchRequestCurrent(store, request);
|
|
218
217
|
},
|
|
219
|
-
launchFingerprint: (request) => (runtimeLaunchFingerprint(store, request)),
|
|
220
218
|
onCleanupRequired: signalRuntimeCleanup,
|
|
221
219
|
runtimeIsolation
|
|
222
220
|
});
|
|
@@ -501,7 +499,6 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
501
499
|
assertCurrent: (request) => {
|
|
502
500
|
assertRuntimeLaunchRequestCurrent(store, request);
|
|
503
501
|
},
|
|
504
|
-
launchFingerprint: (request) => (runtimeLaunchFingerprint(store, request)),
|
|
505
502
|
onCleanupRequired
|
|
506
503
|
});
|
|
507
504
|
const lifecycleTails = new Map();
|
|
@@ -862,11 +859,15 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
862
859
|
|| session.nativeSessionId !== request.nativeSessionId) {
|
|
863
860
|
throw new Error(`Native session changed: ${request.owner.roleName}.`);
|
|
864
861
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
if (
|
|
869
|
-
|
|
862
|
+
// The Role's durable Session record is the only authority for which Host
|
|
863
|
+
// activation may be restored. Targeting anything else would revive a
|
|
864
|
+
// historical activation.
|
|
865
|
+
if (request.hostActivationId !== undefined
|
|
866
|
+
&& session.runtimeGenerationId !== request.hostActivationId) {
|
|
867
|
+
throw new Error(`Session restore does not target the Role's current Host activation: ${request.owner.roleName}.`);
|
|
868
|
+
}
|
|
869
|
+
if (!roleSessionMayContinue(session.effective, request.effective)) {
|
|
870
|
+
throw new Error(`Native session cannot continue under this launch: ${request.owner.roleName}.`);
|
|
870
871
|
}
|
|
871
872
|
}
|
|
872
873
|
}
|
|
@@ -888,21 +889,6 @@ function currentDesiredEffective(store, request, role) {
|
|
|
888
889
|
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
889
890
|
});
|
|
890
891
|
}
|
|
891
|
-
function runtimeLaunchFingerprint(store, request) {
|
|
892
|
-
const agent = store.getConfiguredAgent(request.effective.agentId);
|
|
893
|
-
if (agent === null) {
|
|
894
|
-
throw new Error(`Agent no longer exists: ${request.effective.agentId}.`);
|
|
895
|
-
}
|
|
896
|
-
return createHash("sha256").update(JSON.stringify([
|
|
897
|
-
request.owner,
|
|
898
|
-
request.effective,
|
|
899
|
-
request.managedWorkspace === undefined
|
|
900
|
-
? undefined
|
|
901
|
-
: managedWorkspaceIdentity(request.managedWorkspace),
|
|
902
|
-
request.runtimePolicy,
|
|
903
|
-
agent
|
|
904
|
-
])).digest("hex");
|
|
905
|
-
}
|
|
906
892
|
function currentDesiredManagedWorkspace(store, taskId, roleName) {
|
|
907
893
|
const item = store.listWorkItems(taskId).find((candidate) => (candidate.assignee === roleName
|
|
908
894
|
&& !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
|