@zq-silk/yui 0.12.0 → 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/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 +57 -24
- 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
|
@@ -34,14 +34,17 @@
|
|
|
34
34
|
* authoritative input byte-for-byte unchanged.
|
|
35
35
|
*/
|
|
36
36
|
import { spawn } from "node:child_process";
|
|
37
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
37
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
38
|
+
import { tmpdir } from "node:os";
|
|
38
39
|
import { dirname, join } from "node:path";
|
|
40
|
+
import Database from "better-sqlite3";
|
|
39
41
|
import { describeReport, runMigration } from "../migration/index.js";
|
|
40
42
|
import { validateCompatibleFileTaskStore } from "../compatibleTaskStore.js";
|
|
41
43
|
import { stopFileTaskController, ensureFileTaskController, ensureFileTaskControllerIdentity } from "../../controller/clientRuntime.js";
|
|
42
44
|
import { callController } from "../../core/controllerClient.js";
|
|
43
45
|
import { FileTaskStore, STORAGE_STATE_FILE, withStorageWriteLock } from "../taskStore.js";
|
|
44
46
|
import { SqliteTaskStore } from "../sqliteStore.js";
|
|
47
|
+
import { migrateSqliteSchema } from "../sqliteSchema.js";
|
|
45
48
|
import { clearUpgradeFence, placeUpgradeFence, readUpgradeFence, UpgradeFenceError } from "../upgradeFence.js";
|
|
46
49
|
import { withUpgradeCoordinationLock } from "../upgradeCoordination.js";
|
|
47
50
|
import { clearUpgradeReceipt, writeUpgradeReceipt, upgradeReceiptPath } from "./upgradeReceipt.js";
|
|
@@ -52,7 +55,7 @@ import { repairPseudoLayout7 } from "./pseudoLayoutRepair.js";
|
|
|
52
55
|
import { createSqliteMigrationTarget } from "./sqliteMigrationTarget.js";
|
|
53
56
|
import { createSqliteRecordMigrationTarget } from "./sqliteRecordMigrationTarget.js";
|
|
54
57
|
import { COMMITTED_DATABASE_FILENAME } from "./sqliteStateMigration.js";
|
|
55
|
-
import { inspectOfflineUpgradeInventory } from "./offlineUpgradeInventory.js";
|
|
58
|
+
import { inspectOfflineUpgradeInventory, inspectSqliteDurableUpgradeInventory } from "./offlineUpgradeInventory.js";
|
|
56
59
|
/**
|
|
57
60
|
* Run the storage upgrade for one Home. Never throws for an expected blocker;
|
|
58
61
|
* it returns a structured `blocked` result instead. It only throws on a truly
|
|
@@ -95,6 +98,9 @@ export async function runStorageUpgrade(options) {
|
|
|
95
98
|
action: blocker.action
|
|
96
99
|
}, classification);
|
|
97
100
|
}
|
|
101
|
+
if (classification.sqliteMigration !== undefined) {
|
|
102
|
+
return runSqliteInPlaceUpgrade(options, classification, now, callerPid);
|
|
103
|
+
}
|
|
98
104
|
// A pseudo-layout-7 Home (manifest 7, no yui.db, readable state.json) needs
|
|
99
105
|
// the deterministic staged state.json→SQLite repair, not the version
|
|
100
106
|
// migration engine (Issue 01). The update preflight reports it as
|
|
@@ -357,6 +363,329 @@ function offlineInventoryBlocker(inventory, sceneUnchanged) {
|
|
|
357
363
|
...(sceneUnchanged ? { sceneUnchanged: true } : {})
|
|
358
364
|
};
|
|
359
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* A valid SQLite migration prefix advances the authoritative database in
|
|
368
|
+
* place. It still uses the offline runtime gate because the old Controller
|
|
369
|
+
* cannot safely resume after the schema transaction commits, but it never
|
|
370
|
+
* snapshots, copies, rebuilds, or swaps the database.
|
|
371
|
+
*/
|
|
372
|
+
async function runSqliteInPlaceUpgrade(options, classification, now, callerPid) {
|
|
373
|
+
const migration = classification.sqliteMigration;
|
|
374
|
+
const inventory = await readOfflineInventory(options, options.home);
|
|
375
|
+
if (inventory.total > 0) {
|
|
376
|
+
return withClassification(offlineInventoryBlocker(inventory, true), classification);
|
|
377
|
+
}
|
|
378
|
+
if (options.mode === "update-preflight") {
|
|
379
|
+
return {
|
|
380
|
+
outcome: "update-preflight",
|
|
381
|
+
status: "in-place-migration",
|
|
382
|
+
stepCount: migration.pendingVersions.length,
|
|
383
|
+
classification,
|
|
384
|
+
sqliteMigration: migration
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (options.mode === "dry-run") {
|
|
388
|
+
return validateSqliteInPlaceDryRun(options, classification, now);
|
|
389
|
+
}
|
|
390
|
+
return executeSqliteInPlaceUpgrade(options, classification, now, callerPid);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Exercise every pending SQLite migration against a consistent disposable
|
|
394
|
+
* snapshot, then open that snapshot through the current production loader.
|
|
395
|
+
* The authoritative database is opened read-only and is never migrated.
|
|
396
|
+
*/
|
|
397
|
+
async function validateSqliteInPlaceDryRun(options, classification, now) {
|
|
398
|
+
const migration = classification.sqliteMigration;
|
|
399
|
+
const stagingRoot = mkdtempSync(join(tmpdir(), "yui-sqlite-upgrade-dry-run-"));
|
|
400
|
+
const stagedDatabasePath = join(stagingRoot, COMMITTED_DATABASE_FILENAME);
|
|
401
|
+
let source;
|
|
402
|
+
let staged;
|
|
403
|
+
let loader;
|
|
404
|
+
try {
|
|
405
|
+
source = new Database(join(options.home, COMMITTED_DATABASE_FILENAME), {
|
|
406
|
+
readonly: true,
|
|
407
|
+
fileMustExist: true
|
|
408
|
+
});
|
|
409
|
+
source.pragma("busy_timeout = 5000");
|
|
410
|
+
await source.backup(stagedDatabasePath);
|
|
411
|
+
source.close();
|
|
412
|
+
source = undefined;
|
|
413
|
+
staged = new Database(stagedDatabasePath);
|
|
414
|
+
staged.pragma("journal_mode = WAL");
|
|
415
|
+
staged.pragma("synchronous = FULL");
|
|
416
|
+
staged.pragma("foreign_keys = ON");
|
|
417
|
+
const applied = migrateSqliteSchema(staged, { mode: "apply" }).applied;
|
|
418
|
+
if (applied.join(",") !== migration.pendingVersions.join(",")) {
|
|
419
|
+
throw new Error(`staged SQLite migration applied versions ${applied.join(",") || "none"}; `
|
|
420
|
+
+ `expected ${migration.pendingVersions.join(",")}`);
|
|
421
|
+
}
|
|
422
|
+
staged.close();
|
|
423
|
+
staged = undefined;
|
|
424
|
+
loader = new SqliteTaskStore(stagingRoot);
|
|
425
|
+
const quickCheck = loader.databaseHandle().pragma("quick_check", { simple: true });
|
|
426
|
+
if (quickCheck !== "ok") {
|
|
427
|
+
throw new Error(`staged SQLite quick_check returned ${String(quickCheck)}`);
|
|
428
|
+
}
|
|
429
|
+
loader.close();
|
|
430
|
+
loader = undefined;
|
|
431
|
+
return {
|
|
432
|
+
outcome: "dry-run",
|
|
433
|
+
classification,
|
|
434
|
+
report: sqliteInPlaceReport(options.latest, migration, "dry-run", now)
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
return withClassification({
|
|
439
|
+
outcome: "blocked",
|
|
440
|
+
stage: "validate",
|
|
441
|
+
message: `SQLite dry-run validation failed: ${messageOf(error)}`,
|
|
442
|
+
action: "The authoritative database was not changed. Resolve the reported migration or loader failure, then retry the dry run."
|
|
443
|
+
}, classification);
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
try {
|
|
447
|
+
loader?.close();
|
|
448
|
+
}
|
|
449
|
+
catch { /* best-effort disposable cleanup */ }
|
|
450
|
+
try {
|
|
451
|
+
staged?.close();
|
|
452
|
+
}
|
|
453
|
+
catch { /* best-effort disposable cleanup */ }
|
|
454
|
+
try {
|
|
455
|
+
source?.close();
|
|
456
|
+
}
|
|
457
|
+
catch { /* best-effort disposable cleanup */ }
|
|
458
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/** Acquire a local write fence or prove the exact parent-owned fence exists. */
|
|
462
|
+
function acquireUpgradeAdmission(options, reason, now, callerPid) {
|
|
463
|
+
const externalOwnerPid = options.externalUpgradeFenceOwnerPid;
|
|
464
|
+
if (externalOwnerPid !== undefined) {
|
|
465
|
+
const fence = readUpgradeFence(options.home);
|
|
466
|
+
if (!isPositivePid(externalOwnerPid) || fence?.ownerPid !== externalOwnerPid) {
|
|
467
|
+
throw new UpgradeFenceError(`expected parent-owned upgrade fence for PID ${String(externalOwnerPid)}`);
|
|
468
|
+
}
|
|
469
|
+
return () => { };
|
|
470
|
+
}
|
|
471
|
+
return placeUpgradeFence(options.home, {
|
|
472
|
+
reason,
|
|
473
|
+
createdAt: now().toISOString(),
|
|
474
|
+
ownerPid: callerPid
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
async function executeSqliteInPlaceUpgrade(options, classification, now, callerPid) {
|
|
478
|
+
const { home } = options;
|
|
479
|
+
const migration = classification.sqliteMigration;
|
|
480
|
+
let releaseFence;
|
|
481
|
+
try {
|
|
482
|
+
releaseFence = acquireUpgradeAdmission(options, "SQLite schema migration in progress", now, callerPid);
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
if (!(error instanceof UpgradeFenceError))
|
|
486
|
+
throw error;
|
|
487
|
+
return withClassification({
|
|
488
|
+
outcome: "blocked",
|
|
489
|
+
stage: "coordination",
|
|
490
|
+
message: `Upgrade coordination could not be acquired: ${error.message}`,
|
|
491
|
+
action: "Wait for the current maintenance operation to finish, then retry."
|
|
492
|
+
}, classification);
|
|
493
|
+
}
|
|
494
|
+
const externallyQuiesced = options.controllerLifecycle === "externally-quiesced";
|
|
495
|
+
let controllerWasRunning = false;
|
|
496
|
+
let controllerStopConfirmed = false;
|
|
497
|
+
let controllerIdentity;
|
|
498
|
+
let committed = false;
|
|
499
|
+
let result;
|
|
500
|
+
let unexpected;
|
|
501
|
+
try {
|
|
502
|
+
if (!externallyQuiesced) {
|
|
503
|
+
const controllerStatus = options.controllerStatus
|
|
504
|
+
?? ((targetHome) => defaultControllerStatus(targetHome, options.controllerOptions));
|
|
505
|
+
let status;
|
|
506
|
+
try {
|
|
507
|
+
status = await controllerStatus(home);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
result = withClassification(controllerLifecycleBlocker("Controller status could not be verified", error), classification);
|
|
511
|
+
}
|
|
512
|
+
if (result === undefined && !isControllerLifecycleStatus(status)) {
|
|
513
|
+
result = withClassification(controllerLifecycleBlocker("Controller status was malformed", new Error("expected a boolean running field")), classification);
|
|
514
|
+
}
|
|
515
|
+
if (result === undefined && status.running && !isControllerLaunchIdentity(status.identity)) {
|
|
516
|
+
result = withClassification(controllerLifecycleBlocker("Controller launch identity could not be authenticated", new Error("executable/argv/version identity is unavailable")), classification);
|
|
517
|
+
}
|
|
518
|
+
if (result === undefined && status.running && !isPositivePid(status.pid)) {
|
|
519
|
+
result = withClassification(controllerLifecycleBlocker("Controller PID could not be authenticated", new Error("a positive status PID is unavailable for fenced stop")), classification);
|
|
520
|
+
}
|
|
521
|
+
if (result === undefined) {
|
|
522
|
+
controllerWasRunning = status.running;
|
|
523
|
+
controllerIdentity = status.running ? status.identity : undefined;
|
|
524
|
+
controllerStopConfirmed = !controllerWasRunning;
|
|
525
|
+
}
|
|
526
|
+
if (result === undefined && controllerWasRunning) {
|
|
527
|
+
const stopController = options.stopController
|
|
528
|
+
?? ((targetHome, expectedPid) => defaultStopController(targetHome, expectedPid, options.controllerOptions));
|
|
529
|
+
try {
|
|
530
|
+
const expectedPid = status.pid;
|
|
531
|
+
const stopped = await stopController(home, expectedPid);
|
|
532
|
+
if (!confirmedControllerStopped(stopped, expectedPid)) {
|
|
533
|
+
result = withClassification(controllerLifecycleBlocker("Controller stop did not confirm a drained process", new Error(`stop did not confirm captured PID ${expectedPid} with stopped:true`)), classification);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
controllerStopConfirmed = true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
result = withClassification(controllerLifecycleBlocker("Controller stop/drain failed", error), classification);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
controllerStopConfirmed = true;
|
|
546
|
+
}
|
|
547
|
+
// The first inventory is a cheap/read-only preflight. Re-read after the
|
|
548
|
+
// Controller has fully drained so a lifecycle request that committed while
|
|
549
|
+
// the fence was being placed cannot slip through on stale evidence.
|
|
550
|
+
if (result === undefined) {
|
|
551
|
+
const finalInventory = await readOfflineInventory(options, home);
|
|
552
|
+
if (finalInventory.total > 0) {
|
|
553
|
+
result = withClassification(offlineInventoryBlocker(finalInventory, true), classification);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (result === undefined) {
|
|
557
|
+
try {
|
|
558
|
+
result = withUpgradeCoordinationLock(home, () => {
|
|
559
|
+
const quiesce = verifyQuiesced(home, callerPid);
|
|
560
|
+
if (quiesce !== null)
|
|
561
|
+
return withClassification(quiesce, classification);
|
|
562
|
+
const db = new Database(join(home, COMMITTED_DATABASE_FILENAME));
|
|
563
|
+
try {
|
|
564
|
+
db.pragma("journal_mode = WAL");
|
|
565
|
+
db.pragma("synchronous = FULL");
|
|
566
|
+
db.pragma("foreign_keys = ON");
|
|
567
|
+
db.pragma("busy_timeout = 5000");
|
|
568
|
+
const apply = db.transaction(() => {
|
|
569
|
+
// BEGIN IMMEDIATE waits for every older SQLite writer. With the
|
|
570
|
+
// upgrade fence still held, the state read here is the final
|
|
571
|
+
// authoritative durable gate for the same schema transaction.
|
|
572
|
+
const durableInventory = inspectSqliteDurableUpgradeInventory(home, db);
|
|
573
|
+
if (durableInventory.total > 0) {
|
|
574
|
+
return withClassification(offlineInventoryBlocker(durableInventory, true), classification);
|
|
575
|
+
}
|
|
576
|
+
migrateSqliteSchema(db, { mode: "apply" });
|
|
577
|
+
return {
|
|
578
|
+
outcome: "upgraded",
|
|
579
|
+
classification,
|
|
580
|
+
migrationMode: "in-place",
|
|
581
|
+
report: sqliteInPlaceReport(options.latest, migration, "execute", now)
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
const applied = apply.immediate();
|
|
585
|
+
if (applied.outcome === "upgraded")
|
|
586
|
+
committed = true;
|
|
587
|
+
return applied;
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
db.close();
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
unexpected = error;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
finally {
|
|
600
|
+
try {
|
|
601
|
+
releaseFence();
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
if (unexpected === undefined)
|
|
605
|
+
unexpected = error;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (unexpected !== undefined) {
|
|
609
|
+
if (committed) {
|
|
610
|
+
return withClassification({
|
|
611
|
+
outcome: "blocked",
|
|
612
|
+
stage: "post-verify",
|
|
613
|
+
message: `SQLite migration committed but completion failed: ${messageOf(unexpected)}`,
|
|
614
|
+
action: "Do not restore the old Controller. Re-run this Yui version; the SQLite migration ledger is the commit record.",
|
|
615
|
+
storageCommitted: true
|
|
616
|
+
}, classification);
|
|
617
|
+
}
|
|
618
|
+
if (controllerWasRunning && controllerStopConfirmed) {
|
|
619
|
+
await restoreController(home, options, unexpected, controllerIdentity);
|
|
620
|
+
}
|
|
621
|
+
throw unexpected;
|
|
622
|
+
}
|
|
623
|
+
if (result === undefined)
|
|
624
|
+
throw new Error("SQLite in-place upgrade did not produce a result.");
|
|
625
|
+
if (result.outcome === "upgraded" && !externallyQuiesced && controllerWasRunning) {
|
|
626
|
+
try {
|
|
627
|
+
await (options.startController
|
|
628
|
+
?? ((targetHome) => defaultStartController(targetHome, options.controllerOptions)))(home);
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
return withClassification({
|
|
632
|
+
outcome: "blocked",
|
|
633
|
+
stage: "post-verify",
|
|
634
|
+
message: `SQLite migration committed but the replacement Controller did not start: ${messageOf(error)}`,
|
|
635
|
+
action: "Do not restore the old Controller. Start the current Yui Controller; the SQLite migration ledger already committed.",
|
|
636
|
+
storageCommitted: true
|
|
637
|
+
}, classification);
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
if (result.outcome === "blocked"
|
|
642
|
+
&& !committed
|
|
643
|
+
&& !externallyQuiesced
|
|
644
|
+
&& controllerWasRunning
|
|
645
|
+
&& controllerStopConfirmed) {
|
|
646
|
+
await restoreController(home, options, new Error(result.message), controllerIdentity);
|
|
647
|
+
}
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
function sqliteInPlaceReport(latest, migration, mode, now) {
|
|
651
|
+
const detail = `SQLite schema ${migration.currentVersion}->${migration.targetVersion}; `
|
|
652
|
+
+ `pending versions ${migration.pendingVersions.join(", ")}`;
|
|
653
|
+
if (mode === "dry-run") {
|
|
654
|
+
return {
|
|
655
|
+
outcome: "dry-run",
|
|
656
|
+
mode,
|
|
657
|
+
source: latest,
|
|
658
|
+
target: latest,
|
|
659
|
+
steps: [],
|
|
660
|
+
effects: [],
|
|
661
|
+
derived: { rebuiltEffects: [] },
|
|
662
|
+
validation: {
|
|
663
|
+
checks: [{
|
|
664
|
+
name: "SQLite staged migration and loader gate",
|
|
665
|
+
outcome: "passed",
|
|
666
|
+
detail: `${detail}; snapshot migrated and reopened by the current SQLite loader`
|
|
667
|
+
}]
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
outcome: "migrated",
|
|
673
|
+
mode,
|
|
674
|
+
source: latest,
|
|
675
|
+
target: latest,
|
|
676
|
+
steps: [],
|
|
677
|
+
effects: [],
|
|
678
|
+
derived: { rebuiltEffects: [] },
|
|
679
|
+
validation: {
|
|
680
|
+
checks: [{ name: "SQLite migration transaction", outcome: "passed", detail }]
|
|
681
|
+
},
|
|
682
|
+
switch: {
|
|
683
|
+
status: "switched",
|
|
684
|
+
detail: "SQLite schema migrated in place; no database copy or rebuild was created."
|
|
685
|
+
},
|
|
686
|
+
completedAt: now().toISOString()
|
|
687
|
+
};
|
|
688
|
+
}
|
|
360
689
|
/** Dry run: validate through the staged gate, then discard; never switch. */
|
|
361
690
|
function dryRun(options, classification, target) {
|
|
362
691
|
// Refuse to reuse a stale staging directory from an interrupted run.
|
|
@@ -390,11 +719,7 @@ async function execute(options, classification, target, callerPid, now) {
|
|
|
390
719
|
// the other upgrader's fence.
|
|
391
720
|
let releaseFence;
|
|
392
721
|
try {
|
|
393
|
-
releaseFence =
|
|
394
|
-
reason: "storage upgrade in progress",
|
|
395
|
-
createdAt: now().toISOString(),
|
|
396
|
-
ownerPid: callerPid
|
|
397
|
-
});
|
|
722
|
+
releaseFence = acquireUpgradeAdmission(options, "storage upgrade in progress", now, callerPid);
|
|
398
723
|
}
|
|
399
724
|
catch (error) {
|
|
400
725
|
if (!(error instanceof UpgradeFenceError))
|
|
@@ -615,11 +940,7 @@ async function executePseudoLayout7Repair(options, classification, now) {
|
|
|
615
940
|
const callerPid = options.callerPid ?? process.pid;
|
|
616
941
|
let releaseFence;
|
|
617
942
|
try {
|
|
618
|
-
releaseFence =
|
|
619
|
-
reason: "storage repair in progress",
|
|
620
|
-
createdAt: now().toISOString(),
|
|
621
|
-
ownerPid: callerPid
|
|
622
|
-
});
|
|
943
|
+
releaseFence = acquireUpgradeAdmission(options, "storage repair in progress", now, callerPid);
|
|
623
944
|
}
|
|
624
945
|
catch (error) {
|
|
625
946
|
if (!(error instanceof UpgradeFenceError))
|
package/dist/task/nextAction.js
CHANGED
|
@@ -50,40 +50,6 @@ export function projectNextAction(facts) {
|
|
|
50
50
|
if (laneRecovery !== undefined) {
|
|
51
51
|
return buildExecutionLaneRecoveryAction(facts, laneRecovery);
|
|
52
52
|
}
|
|
53
|
-
// Quick Win (EXE-03): a resume Run that failed before durable Provider
|
|
54
|
-
// acceptance must not be retried against the same native Session. The
|
|
55
|
-
// authoritative next action is to replace the Session, not to retry the
|
|
56
|
-
// same delivery. The guard only applies while the failed resume Run is the
|
|
57
|
-
// *latest* Leader Run: once a newer Run exists (the fresh-Session launch),
|
|
58
|
-
// the historical failure is stale and must not keep recommending a Session
|
|
59
|
-
// replacement.
|
|
60
|
-
const latestLeaderRun = facts.leaderRuns.at(-1);
|
|
61
|
-
const failedResumeWithoutAcceptance = latestLeaderRun !== undefined
|
|
62
|
-
&& latestLeaderRun.mode === "resume"
|
|
63
|
-
&& latestLeaderRun.status === "failed"
|
|
64
|
-
&& latestLeaderRun.deliveredAt === undefined
|
|
65
|
-
? latestLeaderRun
|
|
66
|
-
: undefined;
|
|
67
|
-
if (failedResumeWithoutAcceptance !== undefined) {
|
|
68
|
-
return buildAction(facts, {
|
|
69
|
-
kind: "replace-leader-session",
|
|
70
|
-
reason: `Leader resume Run ${failedResumeWithoutAcceptance.id} failed before Provider acceptance; `
|
|
71
|
-
+ "the native Session is proven unusable for this delivery. Replace it with a fresh Session "
|
|
72
|
-
+ "after exact cleanup/reset.",
|
|
73
|
-
refs: [ref("agent-run", failedResumeWithoutAcceptance.id)],
|
|
74
|
-
preconditions: [
|
|
75
|
-
{ fact: "Resume Run failed without durable acceptance", satisfied: true, ref: ref("agent-run", failedResumeWithoutAcceptance.id) },
|
|
76
|
-
{ fact: "Old Session is cleaned up or reset before fresh launch", satisfied: false }
|
|
77
|
-
],
|
|
78
|
-
// The failed resume Run is already terminal, so `task run recover
|
|
79
|
-
// --action replace-session` (which requires an active Run) cannot act
|
|
80
|
-
// on it. The working recovery is to reset the Role's Session
|
|
81
|
-
// generation, then clear the Leader failure so the next wake launches
|
|
82
|
-
// a fresh Session (the failed-resume guard in the wakeup processor
|
|
83
|
-
// forces mode=new for the next launch).
|
|
84
|
-
recommendedCommand: `yui task role reset ${task.id} leader --reason "resume failed before acceptance" && yui jobs retry leader-recovery:${task.id}`
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
53
|
const activeLeader = facts.activeRuns.find((run) => run.roleName === "leader");
|
|
88
54
|
if (activeLeader !== undefined) {
|
|
89
55
|
return buildAction(facts, {
|
package/dist/web/webSnapshot.js
CHANGED
|
@@ -224,7 +224,9 @@ function projectWebRunRuntimeHealth(reader, taskId, run, events, now, policy) {
|
|
|
224
224
|
}
|
|
225
225
|
function latestStallField(events, runId, field) {
|
|
226
226
|
const stalled = events
|
|
227
|
-
.filter((event) => event.type === "run.stalled"
|
|
227
|
+
.filter((event) => event.type === "run.stalled"
|
|
228
|
+
&& event.payload.runId === runId
|
|
229
|
+
&& event.payload.status !== "diagnostic-only")
|
|
228
230
|
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
|
|
229
231
|
return stalled?.payload[field];
|
|
230
232
|
}
|
package/package.json
CHANGED
|
@@ -338,7 +338,9 @@ creating an explicit Task Role binding, also set and read back the required
|
|
|
338
338
|
model and effort instead of relying on CLI defaults.
|
|
339
339
|
Every managed reviewer must deliver through the current Run's exact
|
|
340
340
|
`--summary-file -` yield command; a final response alone is not a durable
|
|
341
|
-
handoff.
|
|
341
|
+
handoff. Read the completed result as one review batch and route all reported
|
|
342
|
+
findings together; do not manufacture another ReviewRound merely because one
|
|
343
|
+
finding was handled before the rest of the batch.
|
|
342
344
|
|
|
343
345
|
A direct or native-subagent WorkItem is roleless. A Task Role WorkItem must be
|
|
344
346
|
created with `--role <role>`; do not retrofit the Role later. Reuse a compatible
|
|
@@ -782,8 +784,9 @@ yui task complete <task-id> --summary "<outcome, validation, and remaining risks
|
|
|
782
784
|
```
|
|
783
785
|
|
|
784
786
|
Retire obsolete WorkItems with `yui task work retire <task>/<work> --summary
|
|
785
|
-
"..."`, optionally using `--replacement`. If the current
|
|
786
|
-
|
|
787
|
-
|
|
787
|
+
"..."`, optionally using `--replacement`. If the current Provider Conversation
|
|
788
|
+
cannot continue, request a bounded switch with
|
|
789
|
+
`yui task role session switch <task> <role> --reason "..."`; the current
|
|
790
|
+
Conversation remains authoritative until Yui safely binds the replacement. Archiving is a
|
|
788
791
|
separate global Operator lifecycle action. It performs the final Task-owned
|
|
789
792
|
runtime and clean-worktree teardown, including this Leader.
|
|
@@ -345,10 +345,10 @@ the workflow without claiming that version was delivered.
|
|
|
345
345
|
conflicts on the Leader's behalf.
|
|
346
346
|
- Reconcile a disappeared native Session with `task reconcile`; inspect the Run
|
|
347
347
|
before retrying a confirmed failure.
|
|
348
|
-
- If the current
|
|
349
|
-
`yui task role
|
|
350
|
-
|
|
351
|
-
|
|
348
|
+
- If the current Provider Conversation cannot continue, use
|
|
349
|
+
`yui task role session switch <task> <role> --reason "..."`. The request is
|
|
350
|
+
audited, and Yui keeps the old Conversation authoritative until the exact
|
|
351
|
+
replacement bind succeeds; never reconstruct identities from terminal text.
|
|
352
352
|
- Retry only an explicitly failed recovery Job.
|
|
353
353
|
- When a Leader first-progress advisory is reported, inspect its native
|
|
354
354
|
generations and absence of durable progress. It is cost evidence rather than
|
|
@@ -88,10 +88,13 @@ only its named resource, effect, and isolation boundary; never broaden it. A
|
|
|
88
88
|
real Agent may develop or review code, but that does not authorize a real
|
|
89
89
|
provider/model test.
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
Complete the assigned frozen-scope review before yielding. Accumulate all
|
|
92
|
+
reachable findings, verification gaps, checks actually run, and bounded next
|
|
93
|
+
actions, then submit them together in one Review Run result; do not yield as
|
|
94
|
+
soon as the first finding is discovered. A review result is evidence for Leader
|
|
95
|
+
judgment; it does not accept the WorkItem or complete the Task. Preserve the
|
|
96
|
+
ReviewRound record and explicitly clean its workspace after the round is
|
|
97
|
+
terminal.
|
|
95
98
|
|
|
96
99
|
For normal software delivery, follow the applicable Project Policy. The
|
|
97
100
|
Leader decides whether risk warrants one independent Task-final Review of the
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
2
|
-
import { resetTaskRoleSession } from "../executor/agentExecutor.js";
|
|
3
|
-
import { createTaskEvent } from "../event/taskEvent.js";
|
|
4
|
-
import { createTaskMessage } from "../message/message.js";
|
|
5
|
-
import { updateRoleStatus } from "../role/role.js";
|
|
6
|
-
import { enqueueOperatorEvent } from "../scheduler/operatorEvent.js";
|
|
7
|
-
import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
|
|
8
|
-
import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
9
|
-
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
10
|
-
import { updateWorkItemStatus, workItemOwnsUnresolvedExecutionLane } from "../workItem/workItem.js";
|
|
11
|
-
import { terminalizeExactTaskRun } from "./exactRunTerminalization.js";
|
|
12
|
-
/**
|
|
13
|
-
* Resets the current native generation using Yui's own persisted identities.
|
|
14
|
-
* The caller supplies intent only; the Controller verifies process cleanup.
|
|
15
|
-
*/
|
|
16
|
-
export function resetTaskRoleSessionGeneration(store, taskId, roleName, reason, now) {
|
|
17
|
-
const task = store.getTask(requiredIdentity(taskId, "Task id"));
|
|
18
|
-
if (task === null)
|
|
19
|
-
throw new Error(`Task not found: ${taskId}.`);
|
|
20
|
-
if (task.status !== "active")
|
|
21
|
-
throw new Error(`Task is not active: ${task.id}/${task.status}.`);
|
|
22
|
-
const normalizedRole = requiredIdentity(roleName, "Role name");
|
|
23
|
-
const role = store.getRole(task.id, normalizedRole);
|
|
24
|
-
if (role === null)
|
|
25
|
-
throw new Error(`Role not found: ${task.id}/${normalizedRole}.`);
|
|
26
|
-
const summary = `Reset native Session: ${requiredText(reason, "Reset reason")}`;
|
|
27
|
-
let sessions = store.getTaskRoleSessionSet(task.id, role.name);
|
|
28
|
-
const current = sessions?.sessions[sessions.activeAgentId];
|
|
29
|
-
const activeRun = store.getActiveAgentRun(task.id, role.name);
|
|
30
|
-
if (activeRun !== null) {
|
|
31
|
-
const receiptId = sessions?.inFlight?.runId === activeRun.id
|
|
32
|
-
? sessions.inFlight.receiptId
|
|
33
|
-
: formatAgentRunReceiptId(task.id, activeRun.id);
|
|
34
|
-
const terminal = terminalizeExactTaskRun(store, {
|
|
35
|
-
taskId: task.id,
|
|
36
|
-
roleName: role.name,
|
|
37
|
-
agentId: activeRun.effective.agentId,
|
|
38
|
-
runId: activeRun.id,
|
|
39
|
-
receiptId,
|
|
40
|
-
...(current === undefined ? {} : {
|
|
41
|
-
nativeSessionId: current.nativeSessionId,
|
|
42
|
-
...(current.launchId === undefined ? {} : { launchId: current.launchId })
|
|
43
|
-
}),
|
|
44
|
-
outcome: { status: "failed", summary }
|
|
45
|
-
}, now);
|
|
46
|
-
if (terminal.disposition !== "applied" || terminal.run === null) {
|
|
47
|
-
throw new Error(`Task Role reset lost its exact Run fence: ${terminal.reason ?? "obsolete"}.`);
|
|
48
|
-
}
|
|
49
|
-
if (activeRun.purpose === "execution" && activeRun.workItemId !== undefined) {
|
|
50
|
-
const item = store.getWorkItem(task.id, activeRun.workItemId);
|
|
51
|
-
if (item !== null
|
|
52
|
-
&& !["completed", "failed", "retired"].includes(item.status)
|
|
53
|
-
&& !workItemOwnsUnresolvedExecutionLane(item, activeRun.executionGroupId, activeRun.executionLaneId)) {
|
|
54
|
-
store.saveWorkItem(task.id, updateWorkItemStatus(item, "failed", now, summary));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
enqueueWork(store, runtimeLifecycleTarget({
|
|
59
|
-
scope: "task",
|
|
60
|
-
taskId: task.id,
|
|
61
|
-
roleName: role.name
|
|
62
|
-
}), RUNTIME_CLEANUP_REQUIRED_REASON, now, [{ type: "task", id: task.id }]);
|
|
63
|
-
sessions = store.getTaskRoleSessionSet(task.id, role.name);
|
|
64
|
-
if (sessions !== null)
|
|
65
|
-
store.saveTaskRoleSessionSet(resetTaskRoleSession(sessions, now));
|
|
66
|
-
const updatedRole = store.getRole(task.id, role.name);
|
|
67
|
-
store.saveRole(task.id, updateRoleStatus(updatedRole, role.name === "leader" ? "failed" : "idle", now));
|
|
68
|
-
const message = createTaskMessage(store.nextMessageId(task.id), task.id, summary, "system", { type: "system" }, now, activeRun === null ? {} : {
|
|
69
|
-
runId: activeRun.id,
|
|
70
|
-
...(activeRun.workItemId === undefined ? {} : { workItemId: activeRun.workItemId })
|
|
71
|
-
});
|
|
72
|
-
store.saveMessage(task.id, message);
|
|
73
|
-
const resetEvent = createTaskEvent(store.nextEventId(task.id), task.id, "runtime.role-session-reset", {
|
|
74
|
-
roleName: role.name,
|
|
75
|
-
reason: summary,
|
|
76
|
-
...(activeRun === null ? {} : { runId: activeRun.id }),
|
|
77
|
-
...(current?.nativeSessionId === undefined
|
|
78
|
-
? {}
|
|
79
|
-
: { nativeSessionId: current.nativeSessionId })
|
|
80
|
-
}, now);
|
|
81
|
-
store.saveEvent(task.id, resetEvent);
|
|
82
|
-
if (role.name === "leader") {
|
|
83
|
-
const nativeSessionId = current?.nativeSessionId ?? `reset-${task.id}`;
|
|
84
|
-
store.saveLeaderFailure(recordLeaderFailure(task.id, nativeSessionId, summary, now, store.getLeaderFailure(task.id)));
|
|
85
|
-
enqueueOperatorEvent(store, resetEvent, "leader-run-failed", now);
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
enqueueWork(store, { kind: "role", taskId: task.id, roleName: "leader" }, "role-run-failed", now, [
|
|
89
|
-
{ type: "message", taskId: task.id, id: message.id },
|
|
90
|
-
...(activeRun === null ? [] : [{
|
|
91
|
-
type: "run",
|
|
92
|
-
taskId: task.id,
|
|
93
|
-
id: activeRun.id
|
|
94
|
-
}])
|
|
95
|
-
]);
|
|
96
|
-
}
|
|
97
|
-
return {
|
|
98
|
-
taskId: task.id,
|
|
99
|
-
roleName: role.name,
|
|
100
|
-
run: activeRun === null ? null : store.getAgentRun(task.id, activeRun.id),
|
|
101
|
-
...(current === undefined ? {} : { nativeSessionId: current.nativeSessionId })
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
function requiredIdentity(value, label) {
|
|
105
|
-
const normalized = requiredText(value, label);
|
|
106
|
-
if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
|
|
107
|
-
|| /[\/\\\0]/u.test(normalized))
|
|
108
|
-
throw new Error(`${label} is invalid.`);
|
|
109
|
-
return normalized;
|
|
110
|
-
}
|
|
111
|
-
function requiredText(value, label) {
|
|
112
|
-
if (typeof value !== "string" || value.includes("\0"))
|
|
113
|
-
throw new Error(`${label} is invalid.`);
|
|
114
|
-
const normalized = value.trim();
|
|
115
|
-
if (normalized.length === 0)
|
|
116
|
-
throw new Error(`${label} is required.`);
|
|
117
|
-
return normalized;
|
|
118
|
-
}
|