@zq-silk/yui 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -9
- package/dist/cli/commandCatalog.js +11 -6
- 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 +51 -14
- package/dist/commands/configCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +2 -1
- package/dist/commands/taskCommands.js +80 -29
- package/dist/commands/taskContextCommand.js +6 -2
- package/dist/commands/taskRoleRuntimeStatus.js +31 -7
- package/dist/config/configCatalog.js +1 -1
- package/dist/controller/clientRuntime.js +38 -2
- package/dist/controller/controller.js +23 -15
- package/dist/controller/fileSchedulerStoreAdapter.js +248 -138
- package/dist/controller/runtime.js +56 -1
- package/dist/controller/runtimeHookRunFence.js +19 -4
- package/dist/controller/structuredProviderObservation.js +20 -3
- package/dist/core/controllerClient.js +20 -2
- package/dist/core/controllerServer.js +1 -0
- package/dist/executor/agentExecutor.js +48 -46
- package/dist/executor/fileRoleLaunchPlanner.js +94 -30
- package/dist/lifecycle/exactRunTerminalization.js +68 -3
- package/dist/observability/executionAudit.js +5 -0
- package/dist/release/runtimeRelease.js +20 -0
- package/dist/run/recoveryProjection.js +45 -6
- package/dist/runtime/agentHost.js +159 -85
- package/dist/runtime/conversationSwitch.js +277 -0
- package/dist/runtime/index.js +1 -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 +15 -0
- package/dist/runtime/runtimeProjection.js +6 -7
- package/dist/runtime/tmuxAdapters.js +4 -1
- package/dist/scheduler/activeRoleRunDelivery.js +31 -196
- package/dist/scheduler/leaderWakeupProcessor.js +39 -133
- package/dist/scheduler/roleRunStall.js +53 -17
- package/dist/storage/sqliteSchema.js +62 -26
- package/dist/storage/sqliteStore.js +23 -4
- package/dist/storage/upgrade/homeClassification.js +52 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
- package/dist/task/nextAction.js +0 -34
- package/dist/web/webSnapshot.js +3 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +7 -4
- package/skills/yui-operator/SKILL.md +4 -4
- package/skills/yui-reviewer/SKILL.md +7 -4
- package/dist/lifecycle/taskRoleSessionReset.js +0 -118
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}`,
|
package/dist/cli.js
CHANGED
|
@@ -301,19 +301,44 @@ export async function main() {
|
|
|
301
301
|
if (args[0] === "upgrade") {
|
|
302
302
|
// Mirror doctor/controller: needs a Home but self-manages the schema check,
|
|
303
303
|
// because upgrade must run against a non-current Home.
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
304
|
+
const externallyQuiesced = process.env.YUI_UPDATE_EXTERNALLY_QUIESCED === "1";
|
|
305
|
+
const ownsHandover = args.length === 1 && !externallyQuiesced;
|
|
306
|
+
const externalFenceOwner = externallyQuiesced
|
|
307
|
+
? process.env.YUI_UPDATE_HANDOVER_OWNER_PID
|
|
308
|
+
: undefined;
|
|
309
|
+
const externalUpgradeFenceOwnerPid = externalFenceOwner === undefined
|
|
310
|
+
? undefined
|
|
311
|
+
: Number(externalFenceOwner);
|
|
312
|
+
if (externalUpgradeFenceOwnerPid !== undefined
|
|
313
|
+
&& (!Number.isSafeInteger(externalUpgradeFenceOwnerPid)
|
|
314
|
+
|| externalUpgradeFenceOwnerPid < 1)) {
|
|
315
|
+
throw runtimeError("Update storage-fence owner PID is invalid.");
|
|
316
|
+
}
|
|
317
|
+
const handover = ownsHandover ? acquireHandoverLock(home) : undefined;
|
|
318
|
+
try {
|
|
319
|
+
const result = await runUpgradeCommand(args.slice(1), home, externallyQuiesced
|
|
320
|
+
? {
|
|
321
|
+
controllerLifecycle: "externally-quiesced",
|
|
322
|
+
...(externalUpgradeFenceOwnerPid === undefined
|
|
323
|
+
? {}
|
|
324
|
+
: { externalUpgradeFenceOwnerPid })
|
|
325
|
+
}
|
|
326
|
+
: {});
|
|
327
|
+
// Public execute upgrades leave the Home operational even when no
|
|
328
|
+
// Controller existed before the command. Dry-run and the staged updater's
|
|
329
|
+
// externally-quiesced preflight must remain read-only/lifecycle-neutral.
|
|
330
|
+
if (ownsHandover && result.exitCode === 0) {
|
|
331
|
+
await ensureFileTaskController(home, {
|
|
332
|
+
environment: process.env,
|
|
333
|
+
handoverOwnerPid: process.pid
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
process.exitCode = result.exitCode;
|
|
337
|
+
emit(result.output, false, result.data);
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
handover?.release();
|
|
314
341
|
}
|
|
315
|
-
process.exitCode = result.exitCode;
|
|
316
|
-
emit(result.output, false, result.data);
|
|
317
342
|
return;
|
|
318
343
|
}
|
|
319
344
|
if (args[0] === "internal") {
|
|
@@ -521,9 +546,21 @@ export async function main() {
|
|
|
521
546
|
}
|
|
522
547
|
validateCompatibleFileTaskStore(home);
|
|
523
548
|
const controllerMethod = method;
|
|
549
|
+
const updateHandoverOwner = process.env.YUI_UPDATE_HANDOVER_OWNER_PID;
|
|
550
|
+
const updateHandoverOwnerPid = updateHandoverOwner === undefined
|
|
551
|
+
? undefined
|
|
552
|
+
: Number(updateHandoverOwner);
|
|
553
|
+
if (updateHandoverOwnerPid !== undefined
|
|
554
|
+
&& (!Number.isSafeInteger(updateHandoverOwnerPid) || updateHandoverOwnerPid < 1)) {
|
|
555
|
+
throw runtimeError("Update Controller handover owner PID is invalid.");
|
|
556
|
+
}
|
|
557
|
+
const controllerOptions = {
|
|
558
|
+
environment: process.env,
|
|
559
|
+
...(updateHandoverOwnerPid === undefined ? {} : { handoverOwnerPid: updateHandoverOwnerPid })
|
|
560
|
+
};
|
|
524
561
|
const result = controllerMethod === "restart"
|
|
525
|
-
? await restartFileTaskController(home,
|
|
526
|
-
: await stopFileTaskController(home,
|
|
562
|
+
? await restartFileTaskController(home, controllerOptions)
|
|
563
|
+
: await stopFileTaskController(home, controllerOptions);
|
|
527
564
|
// The update lifecycle needs the authenticated replacement PID returned by
|
|
528
565
|
// restart/readiness. Keep stop's long-standing text envelope, while
|
|
529
566
|
// exposing restart's structured result alongside its human output.
|
|
@@ -455,7 +455,7 @@ const CONFIG_KEY_HANDLERS = [
|
|
|
455
455
|
const { runtimeHealth: _removed, ...rest } = config;
|
|
456
456
|
return rest;
|
|
457
457
|
});
|
|
458
|
-
return "Runtime health thresholds reset to quiet 300s / diagnostic
|
|
458
|
+
return "Runtime health thresholds reset to quiet 300s / diagnostic 900s / stall 1800s\n";
|
|
459
459
|
}
|
|
460
460
|
},
|
|
461
461
|
{
|
|
@@ -139,7 +139,8 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
|
|
|
139
139
|
}
|
|
140
140
|
if (report.sessions.status === "ok" && report.sessions.data !== undefined) {
|
|
141
141
|
const sessions = report.sessions.data;
|
|
142
|
-
lines.push("", `Sessions: ${sessions.generations} generations · ${sessions.broken} broken · ${sessions.stopped} stopped · ${sessions.other} other`, `Resets: ${sessions.resets} ·
|
|
142
|
+
lines.push("", `Sessions: ${sessions.generations} generations · ${sessions.broken} broken · ${sessions.stopped} stopped · ${sessions.other} other`, `Resets: ${sessions.resets} · Conversation switches ${sessions.conversationSwitches}`
|
|
143
|
+
+ ` · lifecycle events ${sessions.lifecycleEvents} · stop failures ${sessions.stopFailures}`, `Terminal by Run relation: ${sessions.terminalByRunRelation.postRunYielded} post-run-yielded`
|
|
143
144
|
+ ` · ${sessions.terminalByRunRelation.runFailed} run-failed`
|
|
144
145
|
+ ` · ${sessions.terminalByRunRelation.activeRun} active-run`
|
|
145
146
|
+ ` · ${sessions.terminalByRunRelation.noRun} no-run`);
|
|
@@ -9,8 +9,9 @@ import { createTaskRecordRetirement, isTaskRecordRetired, taskRecordRetirement }
|
|
|
9
9
|
import { isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
|
|
10
10
|
import { readCommandText } from "./textInput.js";
|
|
11
11
|
import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
|
|
12
|
-
import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace,
|
|
12
|
+
import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
13
13
|
import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
|
|
14
|
+
import { CONVERSATION_SWITCH_REQUESTED_EVENT, CONVERSATION_SWITCH_RESOLVED_EVENT, projectConversationSwitch, providerConversationGeneration, roleSessionDispatchModeWithConversationSwitch } from "../runtime/conversationSwitch.js";
|
|
14
15
|
import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
15
16
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
16
17
|
import { formatTimestamp } from "../output/timePresentation.js";
|
|
@@ -18,7 +19,6 @@ import { renderRoleDetails } from "../output/rolePresentation.js";
|
|
|
18
19
|
import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js";
|
|
19
20
|
import { cancelInputRequest } from "../input/inputRequest.js";
|
|
20
21
|
import { recoverExactAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
|
|
21
|
-
import { resetTaskRoleSessionGeneration } from "../lifecycle/taskRoleSessionReset.js";
|
|
22
22
|
import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
|
|
23
23
|
import { agentRunDeliveryReceiptId, createAgentRun, withAgentRunContextSnapshot } from "../run/agentRun.js";
|
|
24
24
|
import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
|
|
@@ -1373,8 +1373,8 @@ function taskRoleCommand(args, store, options) {
|
|
|
1373
1373
|
return output(bindTaskRole(rest, store, options));
|
|
1374
1374
|
if (command === "unbind")
|
|
1375
1375
|
return output(unbindTaskRole(rest, store, options));
|
|
1376
|
-
if (command === "
|
|
1377
|
-
return
|
|
1376
|
+
if (command === "session")
|
|
1377
|
+
return taskRoleSessionCommand(rest, store, options);
|
|
1378
1378
|
if (command === "view")
|
|
1379
1379
|
return viewTaskRole(rest, store);
|
|
1380
1380
|
if (command === "takeover")
|
|
@@ -1385,29 +1385,78 @@ function taskRoleCommand(args, store, options) {
|
|
|
1385
1385
|
? "Task role command is required."
|
|
1386
1386
|
: `Unknown command: task role ${command}`);
|
|
1387
1387
|
}
|
|
1388
|
-
function
|
|
1389
|
-
const
|
|
1388
|
+
function taskRoleSessionCommand(args, store, options) {
|
|
1389
|
+
const [command, ...rest] = args;
|
|
1390
|
+
if (command === "switch")
|
|
1391
|
+
return switchTaskRoleSession(rest, store, options);
|
|
1392
|
+
throw usageError(command === undefined
|
|
1393
|
+
? "Task role session command is required."
|
|
1394
|
+
: `Unknown command: task role session ${command}`);
|
|
1395
|
+
}
|
|
1396
|
+
function switchTaskRoleSession(args, store, options) {
|
|
1397
|
+
const usage = "Task role session switch usage: yui task role session switch <task> <role> --reason <text>.";
|
|
1390
1398
|
const parsed = parseTail(args, new Set(["--reason"]), usage);
|
|
1391
1399
|
exactPositionals(parsed.positionals, 2, usage);
|
|
1392
|
-
const reason = requiredOption(parsed.options, "--reason");
|
|
1400
|
+
const reason = truncateEventNote(requiredOption(parsed.options, "--reason"));
|
|
1393
1401
|
const now = clock(options);
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1402
|
+
const result = store.transaction((tx) => {
|
|
1403
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
1404
|
+
if (task.status !== "active") {
|
|
1405
|
+
throw usageError(inactiveTaskMessage(task, "switching a Provider Conversation"), usage);
|
|
1406
|
+
}
|
|
1407
|
+
const role = requireRole(tx, task.id, parsed.positionals[1]);
|
|
1408
|
+
const requestedBy = taskActor(options, task.id);
|
|
1409
|
+
const leaderRunId = requestedBy === "leader"
|
|
1410
|
+
? taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome)
|
|
1411
|
+
: undefined;
|
|
1412
|
+
if (requestedBy === "leader" && leaderRunId === undefined) {
|
|
1413
|
+
throw usageError("Leader Conversation switching requires the exact current-Turn assertion.", usage);
|
|
1414
|
+
}
|
|
1415
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1416
|
+
const generation = providerConversationGeneration(sessions);
|
|
1417
|
+
if (sessions === null || generation === null) {
|
|
1418
|
+
throw usageError(`Task Role has no current Provider Conversation: ${task.id}/${role.name}.`, usage);
|
|
1419
|
+
}
|
|
1420
|
+
const events = tx.listEvents(task.id);
|
|
1421
|
+
const previous = projectConversationSwitch(events, role.name, sessions);
|
|
1422
|
+
if (previous?.status === "pending"
|
|
1423
|
+
&& previous.generation === generation
|
|
1424
|
+
&& previous.requestedBy === requestedBy
|
|
1425
|
+
&& previous.reason === reason) {
|
|
1426
|
+
return { taskId: task.id, roleName: role.name, request: previous, replayed: true };
|
|
1427
|
+
}
|
|
1428
|
+
if (previous !== null && previous.resolvedAt === undefined) {
|
|
1429
|
+
tx.saveEvent(task.id, createTaskEvent(tx.nextEventId(task.id), task.id, CONVERSATION_SWITCH_RESOLVED_EVENT, {
|
|
1430
|
+
requestId: previous.requestId,
|
|
1431
|
+
roleName: role.name,
|
|
1432
|
+
generation: previous.generation,
|
|
1433
|
+
status: "obsolete"
|
|
1434
|
+
}, now));
|
|
1435
|
+
}
|
|
1436
|
+
const requestId = tx.nextEventId(task.id);
|
|
1437
|
+
const event = createTaskEvent(requestId, task.id, CONVERSATION_SWITCH_REQUESTED_EVENT, {
|
|
1438
|
+
requestId,
|
|
1439
|
+
roleName: role.name,
|
|
1440
|
+
generation,
|
|
1441
|
+
requestedBy,
|
|
1442
|
+
reason,
|
|
1443
|
+
...(leaderRunId === undefined ? {} : { leaderRunId })
|
|
1444
|
+
}, now);
|
|
1445
|
+
tx.saveEvent(task.id, event);
|
|
1446
|
+
return {
|
|
1447
|
+
taskId: task.id,
|
|
1448
|
+
roleName: role.name,
|
|
1449
|
+
request: projectConversationSwitch([...events, event], role.name, sessions),
|
|
1450
|
+
replayed: false
|
|
1451
|
+
};
|
|
1452
|
+
});
|
|
1453
|
+
options.runtime?.notifyStateChanged(result.taskId);
|
|
1454
|
+
options.runtime?.notifyMailboxChanged?.({
|
|
1455
|
+
kind: "role",
|
|
1403
1456
|
taskId: result.taskId,
|
|
1404
1457
|
roleName: result.roleName
|
|
1405
1458
|
});
|
|
1406
|
-
|
|
1407
|
-
notifyMailbox(options.runtime, result.roleName === LEADER_ROLE
|
|
1408
|
-
? { kind: "operator" }
|
|
1409
|
-
: leaderMailbox(result.taskId), result.taskId);
|
|
1410
|
-
return output(`Reset Task Role Session ${result.taskId}/${result.roleName}; runtime cleanup is pending.\n`, result);
|
|
1459
|
+
return output(`${result.replayed ? "Reused" : "Recorded"} pending Provider Conversation switch ${result.request.requestId} for ${result.taskId}/${result.roleName}. The current Conversation remains authoritative until safe replacement binding.\n`, result);
|
|
1411
1460
|
}
|
|
1412
1461
|
function addTaskRole(args, store, options) {
|
|
1413
1462
|
const usage = "Task role add usage: yui task role add <task> <name> [Role and Agent settings].";
|
|
@@ -2337,7 +2386,7 @@ function dispatchWork(args, store, options) {
|
|
|
2337
2386
|
workItemWriteProjectIds: item.writeProjectIds
|
|
2338
2387
|
});
|
|
2339
2388
|
const sessions = tx.getTaskRoleSessionSet(task.id, plan.role.name);
|
|
2340
|
-
const dispatchMode =
|
|
2389
|
+
const dispatchMode = taskRoleDispatchMode(tx, task.id, plan.role.name, sessions, effective.agentId, effective);
|
|
2341
2390
|
const laneWorkspace = laneManagedWorkspace === undefined
|
|
2342
2391
|
? undefined
|
|
2343
2392
|
: {
|
|
@@ -4576,7 +4625,7 @@ function retryRun(args, store, options) {
|
|
|
4576
4625
|
contextSnapshotRef: contextSnapshotRef(retrySnapshot),
|
|
4577
4626
|
deltaRefIds: contextSnapshotDeltaRefIds(tx, retrySnapshot)
|
|
4578
4627
|
});
|
|
4579
|
-
const created = createAgentRun(runId, task.id, role.name,
|
|
4628
|
+
const created = createAgentRun(runId, task.id, role.name, taskRoleDispatchMode(tx, task.id, role.name, sessions, effective.agentId, effective), assignment, now, {
|
|
4580
4629
|
...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId }),
|
|
4581
4630
|
...(runningGroup === undefined ? {} : {
|
|
4582
4631
|
executionGroupId: runningGroup.id,
|
|
@@ -5125,11 +5174,11 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
5125
5174
|
}
|
|
5126
5175
|
/**
|
|
5127
5176
|
* Records one explicit Leader recovery decision against an exact live Run.
|
|
5128
|
-
*
|
|
5129
|
-
*
|
|
5177
|
+
* Diagnose and retry remain same-Run requests: this command never changes a
|
|
5178
|
+
* native Conversation generation.
|
|
5130
5179
|
*/
|
|
5131
5180
|
function recoverRun(args, store, options) {
|
|
5132
|
-
const usage = "Task run recover usage: yui task run recover <task>/<run> --action <diagnose|retry|
|
|
5181
|
+
const usage = "Task run recover usage: yui task run recover <task>/<run> --action <diagnose|retry|terminate> (--expected-progress-at <timestamp>|--from-next-action <fingerprint>) --provider-acceptance <accepted|rejected|ambiguous> --reason <text> [--agent-id <id>] [--adapter-id <id>] [--native-session-id <id>] [--launch-id <id>].";
|
|
5133
5182
|
const parsed = parseTail(args, new Set([
|
|
5134
5183
|
"--action",
|
|
5135
5184
|
"--expected-progress-at",
|
|
@@ -6153,7 +6202,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
6153
6202
|
effective,
|
|
6154
6203
|
workspace: laneWorkspace
|
|
6155
6204
|
}, now);
|
|
6156
|
-
createdRuns.push(createAgentRun(runId, taskId, laneReviewer.name,
|
|
6205
|
+
createdRuns.push(createAgentRun(runId, taskId, laneReviewer.name, taskRoleDispatchMode(tx, taskId, laneReviewer.name, sessions, effective.agentId, effective), assignment, now, {
|
|
6157
6206
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
6158
6207
|
purpose: "review",
|
|
6159
6208
|
reviewRoundId: round.id,
|
|
@@ -6519,6 +6568,9 @@ function assertTaskOpen(task) {
|
|
|
6519
6568
|
function taskActor(options, taskId) {
|
|
6520
6569
|
return resolveTaskActor(options.environment, taskId);
|
|
6521
6570
|
}
|
|
6571
|
+
function taskRoleDispatchMode(store, taskId, roleName, sessions, agentId, effective) {
|
|
6572
|
+
return roleSessionDispatchModeWithConversationSwitch(sessions, store.listEvents(taskId), store.getWorkMailbox({ kind: "role", taskId, roleName }), roleName, agentId, effective);
|
|
6573
|
+
}
|
|
6522
6574
|
function inactiveTaskMessage(task, action) {
|
|
6523
6575
|
if (task.status === "draft") {
|
|
6524
6576
|
return `Task ${task.id} is a Draft; activate it before ${action}.`;
|
|
@@ -6549,10 +6601,9 @@ function parseWorkStatus(value) {
|
|
|
6549
6601
|
function parseRecoveryAction(value, usage) {
|
|
6550
6602
|
if (value === "diagnose"
|
|
6551
6603
|
|| value === "retry"
|
|
6552
|
-
|| value === "replace-session"
|
|
6553
6604
|
|| value === "terminate")
|
|
6554
6605
|
return value;
|
|
6555
|
-
throw usageError("--action must be diagnose, retry,
|
|
6606
|
+
throw usageError("--action must be diagnose, retry, or terminate.", usage);
|
|
6556
6607
|
}
|
|
6557
6608
|
function parseProviderAcceptance(value, usage) {
|
|
6558
6609
|
if (value === "accepted" || value === "rejected" || value === "ambiguous")
|
|
@@ -269,7 +269,9 @@ export function runTaskContextCommand(args, store, currentTaskReviewCandidate =
|
|
|
269
269
|
: [` Runtime source at creation: ${creation.payload.runtimeSource}`]),
|
|
270
270
|
...(recovery === undefined ? [] : [
|
|
271
271
|
` Runtime cleanup: ${recovery.runtimeCleanupPending ? "pending" : "none"}`,
|
|
272
|
-
` Fresh launch: ${recovery.freshLaunchAllowed
|
|
272
|
+
` Fresh launch: ${recovery.freshLaunchAllowed
|
|
273
|
+
? "allowed"
|
|
274
|
+
: `blocked (${recovery.freshLaunchBlockers.join(", ")})`}`
|
|
273
275
|
])
|
|
274
276
|
];
|
|
275
277
|
})),
|
|
@@ -420,7 +422,9 @@ function renderCoordinationMailbox(mailbox) {
|
|
|
420
422
|
}
|
|
421
423
|
function latestStallKind(events, runId) {
|
|
422
424
|
const event = [...events]
|
|
423
|
-
.filter((candidate) => candidate.type === "run.stalled"
|
|
425
|
+
.filter((candidate) => candidate.type === "run.stalled"
|
|
426
|
+
&& candidate.payload.runId === runId
|
|
427
|
+
&& candidate.payload.status !== "diagnostic-only")
|
|
424
428
|
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
|
|
425
429
|
return event?.payload.kind ?? "workflow-not-progressing";
|
|
426
430
|
}
|