omp-conductor 0.19.4 → 0.19.6
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/package.json +1 -1
- package/src/cli.ts +2 -0
- package/src/command-help.ts +18 -8
- package/src/command-manifest.ts +22 -5
- package/src/commands/context.ts +1 -0
- package/src/commands/report.ts +63 -0
- package/src/commands/restore-db.ts +1 -1
- package/src/commands/resume.ts +5 -0
- package/src/commands/snapshot-db.ts +67 -0
- package/src/commands/upgrade-install.ts +1 -1
- package/src/commands/upgrade.ts +2 -2
- package/src/commands/verb.ts +1 -0
- package/src/commands/watch.ts +14 -2
- package/src/config.ts +17 -0
- package/src/daemon.ts +54 -8
- package/src/dashboard/controls.ts +3 -0
- package/src/doctor.ts +36 -3
- package/src/fleet.ts +51 -3
- package/src/orchestrator-tick.ts +14 -3
- package/src/setup-host.ts +10 -10
- package/src/setup-wizard.ts +1 -1
- package/src/status-render.ts +21 -0
- package/src/store.ts +296 -22
- package/src/types.ts +32 -3
- package/src/upgrade-journal.ts +37 -1
- package/src/upgrade-verify.ts +272 -37
- package/src/upgrade.ts +324 -23
- package/src/verbs/actions.ts +6 -1
- package/src/verbs/server.ts +54 -20
- package/systemd/omp-conductor-recover.sh +55 -7
- package/systemd/recover-unit-test.sh +99 -6
package/src/upgrade.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import { backupTimestamp, copyToUniqueBackup } from "./backups.ts";
|
|
4
5
|
import { inspectBriefLayout, type BriefLayout } from "./brief-upgrade.ts";
|
|
5
6
|
import { pauseInstance, setPaused, statusSnapshot, type AdmissionAckRecord } from "./daemon.ts";
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
upgradeJournalPath,
|
|
25
26
|
type UpgradeCheck,
|
|
26
27
|
type UpgradeJournalEntry,
|
|
28
|
+
type InstallSnapshotManagement,
|
|
27
29
|
} from "./upgrade-journal.ts";
|
|
28
30
|
import {
|
|
29
31
|
enqueueUpgradeReport,
|
|
@@ -129,6 +131,8 @@ export interface UpgradeDeps {
|
|
|
129
131
|
*/
|
|
130
132
|
pauseState(project?: string): { source: string; reason?: string; since: number } | undefined;
|
|
131
133
|
setPaused(value: boolean, project?: string): void;
|
|
134
|
+
/** Host-global recovery fence for host-wide install surfaces. */
|
|
135
|
+
setGlobalRecoveryPaused(value: boolean): void;
|
|
132
136
|
restartDaemon(): Promise<void>;
|
|
133
137
|
sleep(ms: number): Promise<void>;
|
|
134
138
|
/**
|
|
@@ -205,6 +209,14 @@ export const DEFAULT_DEPS: UpgradeDeps = {
|
|
|
205
209
|
pauseState: (project) => pauseInstance(project),
|
|
206
210
|
setPaused: (v, project) =>
|
|
207
211
|
setPaused(v, { source: "upgrade", reason: "upgrade, draining" }, project),
|
|
212
|
+
setGlobalRecoveryPaused: (v) => {
|
|
213
|
+
const current = pauseInstance();
|
|
214
|
+
if (v && current === undefined) {
|
|
215
|
+
setPaused(true, { source: "upgrade-recovery", reason: "upgrade recovery verification required" });
|
|
216
|
+
} else if (!v && current?.source === "upgrade-recovery") {
|
|
217
|
+
setPaused(false);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
208
220
|
restartDaemon: async () => {
|
|
209
221
|
await restartDaemon({});
|
|
210
222
|
},
|
|
@@ -269,7 +281,7 @@ function parseRegistry(raw: string): { version: string; gitHead: string } {
|
|
|
269
281
|
interface ReleaseIdentityPlan {
|
|
270
282
|
version: string;
|
|
271
283
|
gitHead: string;
|
|
272
|
-
/** What
|
|
284
|
+
/** What the discovered CLI root / `omp plugin install` are given for this identity. */
|
|
273
285
|
packageSpec: string;
|
|
274
286
|
/** Named checks already run to earn this identity, journalled as evidence. */
|
|
275
287
|
checks: UpgradeCheck[];
|
|
@@ -360,7 +372,12 @@ async function resolveIdentity(deps: UpgradeDeps, options: UpgradeOptions): Prom
|
|
|
360
372
|
return { version, gitHead: sha, packageSpec: `github:${PACKAGE_SOURCE}#${sha}`, checks, bootstrap: true };
|
|
361
373
|
}
|
|
362
374
|
|
|
363
|
-
|
|
375
|
+
interface OmpPluginEntry {
|
|
376
|
+
version: string;
|
|
377
|
+
path?: string;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function ompPluginInventory(raw: string): unknown[] {
|
|
364
381
|
let parsed: unknown;
|
|
365
382
|
try {
|
|
366
383
|
parsed = JSON.parse(raw);
|
|
@@ -372,15 +389,51 @@ function ompPluginVersion(raw: string): string | undefined {
|
|
|
372
389
|
}
|
|
373
390
|
const npm = Reflect.get(parsed, "npm");
|
|
374
391
|
if (!Array.isArray(npm)) throw new Error("omp plugin list returned no npm inventory");
|
|
375
|
-
|
|
392
|
+
return npm;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function ompPluginEntry(raw: string): OmpPluginEntry | undefined {
|
|
396
|
+
const plugin = ompPluginInventory(raw).find(
|
|
376
397
|
(entry) => entry !== null && typeof entry === "object" && Reflect.get(entry, "name") === PACKAGE,
|
|
377
398
|
);
|
|
378
|
-
if (plugin === undefined) return undefined;
|
|
399
|
+
if (plugin === undefined || plugin === null || typeof plugin !== "object") return undefined;
|
|
379
400
|
const version = Reflect.get(plugin, "version");
|
|
380
401
|
if (typeof version !== "string" || version.length === 0) {
|
|
381
402
|
throw new Error("installed omp-conductor plugin has no version");
|
|
382
403
|
}
|
|
383
|
-
|
|
404
|
+
const path = Reflect.get(plugin, "path");
|
|
405
|
+
return {
|
|
406
|
+
version,
|
|
407
|
+
...(typeof path === "string" && path.length > 0 ? { path } : {}),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function ompPluginVersion(raw: string): string | undefined {
|
|
412
|
+
return ompPluginEntry(raw)?.version;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function nodeModulesRoot(path: string): string | undefined {
|
|
416
|
+
const marker = "/node_modules/";
|
|
417
|
+
const at = path.lastIndexOf(marker);
|
|
418
|
+
return at <= 0 ? undefined : path.slice(0, at);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function defaultOmpPluginRoot(env: NodeJS.ProcessEnv): string {
|
|
422
|
+
const configured = env["OMP_CODING_AGENT_DIR"];
|
|
423
|
+
if (configured !== undefined && configured.length > 0) return join(configured, "plugins");
|
|
424
|
+
return join(env["HOME"] ?? homedir(), ".omp", "plugins");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function ompPluginInstallRoot(raw: string, env: NodeJS.ProcessEnv): string {
|
|
428
|
+
const inventory = ompPluginInventory(raw);
|
|
429
|
+
const ownPath = ompPluginEntry(raw)?.path;
|
|
430
|
+
const fallbackPath = inventory
|
|
431
|
+
.map((entry) =>
|
|
432
|
+
entry !== null && typeof entry === "object" ? Reflect.get(entry, "path") : undefined,
|
|
433
|
+
)
|
|
434
|
+
.find((path): path is string => typeof path === "string" && path.length > 0);
|
|
435
|
+
const root = nodeModulesRoot(ownPath ?? fallbackPath ?? "");
|
|
436
|
+
return root ?? defaultOmpPluginRoot(env);
|
|
384
437
|
}
|
|
385
438
|
|
|
386
439
|
function herdrPluginSource(raw: string): string | undefined {
|
|
@@ -393,12 +446,143 @@ function herdrPluginSource(raw: string): string | undefined {
|
|
|
393
446
|
return source;
|
|
394
447
|
}
|
|
395
448
|
|
|
396
|
-
|
|
397
|
-
|
|
449
|
+
function defaultHerdrInstallRoot(env: NodeJS.ProcessEnv): string {
|
|
450
|
+
const config =
|
|
451
|
+
env["HERDR_CONFIG_DIR"] ??
|
|
452
|
+
join(env["XDG_CONFIG_HOME"] ?? join(env["HOME"] ?? homedir(), ".config"), "herdr");
|
|
453
|
+
return join(config, "plugins", "github");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function herdrPluginManagement(
|
|
457
|
+
raw: string,
|
|
458
|
+
env: NodeJS.ProcessEnv,
|
|
459
|
+
): InstallSnapshotManagement["herdr"] {
|
|
460
|
+
let parsed: unknown;
|
|
461
|
+
try {
|
|
462
|
+
parsed = JSON.parse(raw);
|
|
463
|
+
} catch {
|
|
464
|
+
throw new Error("herdr plugin list returned invalid JSON");
|
|
465
|
+
}
|
|
466
|
+
const result =
|
|
467
|
+
parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
468
|
+
? Reflect.get(parsed, "result")
|
|
469
|
+
: undefined;
|
|
470
|
+
const plugins =
|
|
471
|
+
result !== null && typeof result === "object" && !Array.isArray(result)
|
|
472
|
+
? Reflect.get(result, "plugins")
|
|
473
|
+
: undefined;
|
|
474
|
+
if (!Array.isArray(plugins)) throw new Error("herdr plugin list returned no plugin inventory");
|
|
475
|
+
const plugin = plugins.find(
|
|
476
|
+
(entry) => entry !== null && typeof entry === "object" && Reflect.get(entry, "plugin_id") === HERDR_PLUGIN,
|
|
477
|
+
);
|
|
478
|
+
const root =
|
|
479
|
+
plugin !== undefined && typeof plugin === "object"
|
|
480
|
+
? Reflect.get(plugin, "plugin_root")
|
|
481
|
+
: undefined;
|
|
482
|
+
const source =
|
|
483
|
+
plugin !== undefined && typeof plugin === "object"
|
|
484
|
+
? Reflect.get(plugin, "source")
|
|
485
|
+
: undefined;
|
|
486
|
+
const managedPath =
|
|
487
|
+
source !== null && typeof source === "object" && !Array.isArray(source)
|
|
488
|
+
? Reflect.get(source, "managed_path")
|
|
489
|
+
: undefined;
|
|
490
|
+
const marker = "/plugins/github/";
|
|
491
|
+
const markerAt = typeof root === "string" ? root.indexOf(marker) : -1;
|
|
492
|
+
const installRoot =
|
|
493
|
+
typeof managedPath === "string" && managedPath.length > 0
|
|
494
|
+
? dirname(managedPath)
|
|
495
|
+
: markerAt >= 0 && typeof root === "string"
|
|
496
|
+
? root.slice(0, markerAt + marker.length - 1)
|
|
497
|
+
: defaultHerdrInstallRoot(env);
|
|
498
|
+
return {
|
|
499
|
+
...(typeof root === "string" && root.length > 0 ? { root } : {}),
|
|
500
|
+
installRoot,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function cliInstallRoot(target: string): string {
|
|
505
|
+
const marker = `/node_modules/${PACKAGE}/`;
|
|
506
|
+
const at = target.lastIndexOf(marker);
|
|
507
|
+
if (at <= 0) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`installed omp-conductor CLI target ${target} is not inside a discoverable node_modules/${PACKAGE} root`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return target.slice(0, at);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Discover the exact managed roots the transaction will mutate (#1019). */
|
|
516
|
+
export async function discoverInstallManagement(
|
|
517
|
+
deps: SurfaceProbeDeps,
|
|
518
|
+
): Promise<InstallSnapshotManagement> {
|
|
519
|
+
const session = resolveHerdrSession(deps.env);
|
|
520
|
+
const executable = (await mustRun(deps, "which", ["omp-conductor"])).stdout.trim();
|
|
521
|
+
if (executable.length === 0) throw new Error("which omp-conductor returned no executable");
|
|
522
|
+
const [resolved, omp, herdr] = await Promise.all([
|
|
523
|
+
mustRun(deps, "readlink", ["-f", executable]),
|
|
524
|
+
mustRun(deps, "omp", ["plugin", "list", "--json"]),
|
|
525
|
+
mustRun(deps, "herdr", ["--session", session, "plugin", "list", "--json"]),
|
|
526
|
+
]);
|
|
527
|
+
const target = resolved.stdout.trim();
|
|
528
|
+
if (target.length === 0) throw new Error(`cannot resolve installed CLI ${executable}`);
|
|
529
|
+
const ompRoot = ompPluginInstallRoot(omp.stdout, deps.env);
|
|
530
|
+
return {
|
|
531
|
+
cli: { executable, target, root: cliInstallRoot(target) },
|
|
532
|
+
omp: { root: ompRoot, lock: join(ompRoot, "omp-plugins.lock.json") },
|
|
533
|
+
herdr: herdrPluginManagement(herdr.stdout, deps.env),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function preflightInstall(
|
|
538
|
+
deps: UpgradeDeps,
|
|
539
|
+
management: InstallSnapshotManagement,
|
|
540
|
+
packageSpec: string,
|
|
541
|
+
): Promise<void> {
|
|
542
|
+
const writable: { label: string; path: string }[] = [
|
|
543
|
+
{ label: "CLI package root", path: management.cli.root },
|
|
544
|
+
{ label: "CLI package manifest", path: join(management.cli.root, "package.json") },
|
|
545
|
+
{ label: "CLI node_modules", path: join(management.cli.root, "node_modules") },
|
|
546
|
+
{ label: "OMP plugin root", path: management.omp.root },
|
|
547
|
+
{ label: "Herdr managed-plugin root", path: management.herdr.installRoot },
|
|
548
|
+
];
|
|
549
|
+
if (existsSync(management.omp.lock)) {
|
|
550
|
+
writable.push({ label: "OMP plugin lock", path: management.omp.lock });
|
|
551
|
+
}
|
|
552
|
+
for (const target of writable) {
|
|
553
|
+
const checked = await deps.run("test", ["-w", target.path]);
|
|
554
|
+
if (checked.code !== 0) {
|
|
555
|
+
const detail = checked.stderr.trim() || checked.stdout.trim() || "missing or not writable";
|
|
556
|
+
throw new Error(
|
|
557
|
+
`upgrade preflight refused: ${target.label} ${target.path} is unusable (${detail}); ` +
|
|
558
|
+
"nothing has been installed or paused",
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
await mustRun(deps, "bun", [
|
|
564
|
+
"add",
|
|
565
|
+
"--cwd",
|
|
566
|
+
management.cli.root,
|
|
567
|
+
"--exact",
|
|
568
|
+
"--dry-run",
|
|
569
|
+
packageSpec,
|
|
570
|
+
]);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`upgrade preflight refused: the CLI install command is unusable at ${management.cli.root} ` +
|
|
574
|
+
`(${err instanceof Error ? err.message : String(err)}); nothing has been installed or paused`,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** The three installed identities one host carries: the discovered CLI package
|
|
580
|
+
* tree, the omp plugin, and the herdr recovery plugin's pin. */
|
|
398
581
|
export interface InstalledSurfaces {
|
|
399
582
|
cliVersion: string;
|
|
400
583
|
ompVersion?: string;
|
|
401
584
|
herdrSource?: string;
|
|
585
|
+
management?: InstallSnapshotManagement;
|
|
402
586
|
}
|
|
403
587
|
|
|
404
588
|
/** What reading the three surfaces needs — deliberately narrower than
|
|
@@ -417,6 +601,7 @@ export async function inspectSurfaces(
|
|
|
417
601
|
deps: SurfaceProbeDeps,
|
|
418
602
|
/** `readHerdr: false` skips the herdr session probe and plugin read
|
|
419
603
|
* entirely, so a host with no herdr on PATH can still be asked what its
|
|
604
|
+
|
|
420
605
|
* CLI and omp plugin are (#904). The upgrade transaction always reads all
|
|
421
606
|
* three — a release pins every surface. */
|
|
422
607
|
opts: { readHerdr?: boolean } = {},
|
|
@@ -445,6 +630,56 @@ export async function inspectSurfaces(
|
|
|
445
630
|
};
|
|
446
631
|
}
|
|
447
632
|
|
|
633
|
+
function packageVersionAt(deps: UpgradeDeps, manifest: string): string {
|
|
634
|
+
let parsed: unknown;
|
|
635
|
+
try {
|
|
636
|
+
parsed = JSON.parse(deps.readFile(manifest));
|
|
637
|
+
} catch (err) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
`cannot read restored package manifest ${manifest}: ${err instanceof Error ? err.message : String(err)}`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
const version =
|
|
643
|
+
parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
644
|
+
? Reflect.get(parsed, "version")
|
|
645
|
+
: undefined;
|
|
646
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
647
|
+
throw new Error(`restored package manifest ${manifest} has no version`);
|
|
648
|
+
}
|
|
649
|
+
return version;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Verify the snapshot's recorded destinations directly, before PATH-based probes. */
|
|
653
|
+
function verifyRestoredManagement(deps: UpgradeDeps, previous: InstalledSurfaces): void {
|
|
654
|
+
const management = previous.management;
|
|
655
|
+
if (management === undefined) return;
|
|
656
|
+
const cliManifest = join(management.cli.root, "node_modules", PACKAGE, "package.json");
|
|
657
|
+
const cliVersion = packageVersionAt(deps, cliManifest);
|
|
658
|
+
if (cliVersion !== previous.cliVersion) {
|
|
659
|
+
throw new Error(
|
|
660
|
+
`restored CLI root ${management.cli.root} has ${cliVersion}, expected ${previous.cliVersion}`,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
if (previous.ompVersion !== undefined) {
|
|
664
|
+
const ompManifest = join(management.omp.root, "node_modules", PACKAGE, "package.json");
|
|
665
|
+
const ompVersion = packageVersionAt(deps, ompManifest);
|
|
666
|
+
if (ompVersion !== previous.ompVersion) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
`restored OMP plugin root ${management.omp.root} has ${ompVersion}, expected ${previous.ompVersion}`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (previous.herdrSource !== undefined) {
|
|
673
|
+
if (management.herdr.root === undefined) {
|
|
674
|
+
throw new Error("restored Herdr plugin had no recorded root to verify");
|
|
675
|
+
}
|
|
676
|
+
const manifest = join(management.herdr.root, "herdr-plugin.toml");
|
|
677
|
+
if (deps.readFile(manifest).trim().length === 0) {
|
|
678
|
+
throw new Error(`restored Herdr plugin manifest ${manifest} is empty`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
448
683
|
export function expectedHerdrSource(gitHead: string): string {
|
|
449
684
|
return `github:${HERDR_SOURCE}@${gitHead}`;
|
|
450
685
|
}
|
|
@@ -492,7 +727,7 @@ function previousHerdrInstall(source: string): readonly [string, readonly string
|
|
|
492
727
|
* that represents them.
|
|
493
728
|
*
|
|
494
729
|
* Everything an upgrade or a draining restart replaces is host-wide: the
|
|
495
|
-
*
|
|
730
|
+
* discovered CLI package tree, the omp plugin, the Herdr plugin, and the one
|
|
496
731
|
* daemon. That daemon serves *every* configured project — the generated unit's
|
|
497
732
|
* ExecStart carries no `--project` — so its restart lands on all of them at
|
|
498
733
|
* once. The scope therefore names every project whose workers must drain and
|
|
@@ -1092,13 +1327,22 @@ export async function rollbackUpgrade(
|
|
|
1092
1327
|
// Exact post-rollback identity verification decides whether absence is safe.
|
|
1093
1328
|
}
|
|
1094
1329
|
};
|
|
1095
|
-
|
|
1096
1330
|
if (installTouched) {
|
|
1097
|
-
|
|
1098
|
-
"
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1331
|
+
if (previous.management === undefined) {
|
|
1332
|
+
await restore("legacy Bun-global omp-conductor CLI", "bun", [
|
|
1333
|
+
"add",
|
|
1334
|
+
"-g",
|
|
1335
|
+
`${PACKAGE}@${previous.cliVersion}`,
|
|
1336
|
+
]);
|
|
1337
|
+
} else {
|
|
1338
|
+
await restore(`omp-conductor CLI at ${previous.management.cli.root}`, "bun", [
|
|
1339
|
+
"add",
|
|
1340
|
+
"--cwd",
|
|
1341
|
+
previous.management.cli.root,
|
|
1342
|
+
"--exact",
|
|
1343
|
+
`${PACKAGE}@${previous.cliVersion}`,
|
|
1344
|
+
]);
|
|
1345
|
+
}
|
|
1102
1346
|
if (previous.ompVersion === undefined) {
|
|
1103
1347
|
await removeIfPresent("remove newly installed omp plugin", "omp", [
|
|
1104
1348
|
"plugin",
|
|
@@ -1182,6 +1426,7 @@ export async function rollbackUpgrade(
|
|
|
1182
1426
|
|
|
1183
1427
|
if (installTouched) {
|
|
1184
1428
|
try {
|
|
1429
|
+
verifyRestoredManagement(deps, previous);
|
|
1185
1430
|
const restored = await inspectSurfaces(deps);
|
|
1186
1431
|
if (
|
|
1187
1432
|
restored.cliVersion !== previous.cliVersion ||
|
|
@@ -1262,6 +1507,19 @@ export async function upgradeConductor(
|
|
|
1262
1507
|
// already passed (#908). Everything below reads the same pair either way.
|
|
1263
1508
|
const identity = await resolveIdentity(deps, options);
|
|
1264
1509
|
const release = { version: identity.version, gitHead: identity.gitHead };
|
|
1510
|
+
const requestedProjects = (deps.env["OMP_CONDUCTOR_INSTALL_PROJECTS"] ?? "")
|
|
1511
|
+
.split(",")
|
|
1512
|
+
.filter((name) => name.length > 0);
|
|
1513
|
+
const requestedHolder = deps.env["OMP_CONDUCTOR_INSTALL_HOLDER"];
|
|
1514
|
+
const installHolder =
|
|
1515
|
+
requestedHolder === "human" || requestedHolder === "orchestrator"
|
|
1516
|
+
? requestedHolder
|
|
1517
|
+
: undefined;
|
|
1518
|
+
const installProjects: readonly (string | undefined)[] =
|
|
1519
|
+
requestedProjects.length === 0 ? scope.selectors : requestedProjects;
|
|
1520
|
+
const installAttribution =
|
|
1521
|
+
`Host-global install holder: ${installHolder ?? "unknown"}; affected projects: ` +
|
|
1522
|
+
`${installProjects.map((selector) => selector ?? "(default)").join(", ")}.`;
|
|
1265
1523
|
if (identity.bootstrap) {
|
|
1266
1524
|
// The bootstrap's evidence lands in the journal before the first surface
|
|
1267
1525
|
// moves, so an interrupted bootstrap leaves a record of what was verified
|
|
@@ -1287,15 +1545,24 @@ export async function upgradeConductor(
|
|
|
1287
1545
|
version: release.version,
|
|
1288
1546
|
gitHead: release.gitHead,
|
|
1289
1547
|
unit: deps.env["OMP_CONDUCTOR_UNIT"],
|
|
1548
|
+
selectors: scope.selectors.map((selector) => selector ?? null),
|
|
1549
|
+
...(requestedProjects.length === 0 ? {} : { installProjects: requestedProjects }),
|
|
1550
|
+
...(installHolder === undefined ? {} : { installHolder }),
|
|
1290
1551
|
});
|
|
1291
1552
|
}
|
|
1292
1553
|
const initial = deps.layers(scope.pauseKey);
|
|
1293
|
-
const
|
|
1554
|
+
const observedSurfaces = await inspectSurfaces(deps);
|
|
1294
1555
|
const briefs: ScopedBrief[] = scope.selectors.map((selector) => ({
|
|
1295
1556
|
selector,
|
|
1296
1557
|
...deps.brief(selector),
|
|
1297
1558
|
}));
|
|
1298
|
-
const installNeeded = !surfacesCurrent(
|
|
1559
|
+
const installNeeded = !surfacesCurrent(observedSurfaces, release.version, release.gitHead);
|
|
1560
|
+
const management = installNeeded ? await discoverInstallManagement(deps) : undefined;
|
|
1561
|
+
if (management !== undefined) await preflightInstall(deps, management, identity.packageSpec);
|
|
1562
|
+
const surfaces: InstalledSurfaces = {
|
|
1563
|
+
...observedSurfaces,
|
|
1564
|
+
...(management === undefined ? {} : { management }),
|
|
1565
|
+
};
|
|
1299
1566
|
// The host-unit baseline, read while the OLD package is still the one
|
|
1300
1567
|
// rendering (#905). A destination already differing from the old render was
|
|
1301
1568
|
// edited outside conductor, so the reconcile after the install must not
|
|
@@ -1394,6 +1661,7 @@ export async function upgradeConductor(
|
|
|
1394
1661
|
cliVersion: surfaces.cliVersion,
|
|
1395
1662
|
ompVersion: surfaces.ompVersion,
|
|
1396
1663
|
herdrSource: surfaces.herdrSource,
|
|
1664
|
+
...(surfaces.management === undefined ? {} : { management: surfaces.management }),
|
|
1397
1665
|
},
|
|
1398
1666
|
...(preUpgradeBackup === undefined ? {} : { configBackup: preUpgradeBackup }),
|
|
1399
1667
|
pauseKey: scope.pauseKey,
|
|
@@ -1413,7 +1681,14 @@ export async function upgradeConductor(
|
|
|
1413
1681
|
deps.log("safety: pausing new issue claims");
|
|
1414
1682
|
deps.setPaused(true, scope.pauseKey);
|
|
1415
1683
|
}
|
|
1416
|
-
journal({
|
|
1684
|
+
journal({
|
|
1685
|
+
kind: "phase",
|
|
1686
|
+
phase: "paused",
|
|
1687
|
+
surface: "dispatch",
|
|
1688
|
+
ok: true,
|
|
1689
|
+
version: release.version,
|
|
1690
|
+
pauseSince: deps.pauseState(scope.pauseKey)?.since,
|
|
1691
|
+
});
|
|
1417
1692
|
deps.log("drain: waiting for live omp worker sessions");
|
|
1418
1693
|
try {
|
|
1419
1694
|
await waitForDrain(deps, scope);
|
|
@@ -1436,9 +1711,18 @@ export async function upgradeConductor(
|
|
|
1436
1711
|
const reloadStarted = Date.now();
|
|
1437
1712
|
try {
|
|
1438
1713
|
if (installNeeded) {
|
|
1714
|
+
if (management === undefined) {
|
|
1715
|
+
throw new Error("upgrade invariant failed: install needed without discovered surface management");
|
|
1716
|
+
}
|
|
1439
1717
|
installTouched = true;
|
|
1440
|
-
deps.log(`install 1/3:
|
|
1441
|
-
await mustRun(deps, "bun", [
|
|
1718
|
+
deps.log(`install 1/3: omp-conductor CLI at ${management.cli.root} → ${release.version}`);
|
|
1719
|
+
await mustRun(deps, "bun", [
|
|
1720
|
+
"add",
|
|
1721
|
+
"--cwd",
|
|
1722
|
+
management.cli.root,
|
|
1723
|
+
"--exact",
|
|
1724
|
+
identity.packageSpec,
|
|
1725
|
+
]);
|
|
1442
1726
|
journal({ kind: "phase", phase: "install", surface: "cli", ok: true, version: release.version, gitHead: release.gitHead });
|
|
1443
1727
|
deps.log(`install 2/3: omp plugin omp-conductor → ${release.version}`);
|
|
1444
1728
|
await mustRun(deps, "omp", ["plugin", "install", identity.packageSpec]);
|
|
@@ -1595,6 +1879,7 @@ export async function upgradeConductor(
|
|
|
1595
1879
|
} catch {
|
|
1596
1880
|
deps.setPaused(true, scope.pauseKey);
|
|
1597
1881
|
}
|
|
1882
|
+
deps.setGlobalRecoveryPaused(true);
|
|
1598
1883
|
const failure = err instanceof Error ? err.message : String(err);
|
|
1599
1884
|
journal({
|
|
1600
1885
|
kind: "phase",
|
|
@@ -1623,10 +1908,11 @@ export async function upgradeConductor(
|
|
|
1623
1908
|
journal({ kind: "outcome", phase: "rolled-back", ok: false, version: release.version, detail: failure });
|
|
1624
1909
|
enqueueUpgradeReport(
|
|
1625
1910
|
"tier2",
|
|
1626
|
-
reportProject(
|
|
1911
|
+
reportProject(installProjects),
|
|
1627
1912
|
`fleet upgrade to omp-conductor@${release.version} failed and was rolled back`,
|
|
1628
1913
|
[
|
|
1629
1914
|
`Reason: ${failure}`,
|
|
1915
|
+
installAttribution,
|
|
1630
1916
|
`Restored: cli=${surfaces.cliVersion}, omp=${surfaces.ompVersion ?? "missing"}, ` +
|
|
1631
1917
|
`herdr=${surfaces.herdrSource ?? "missing"}`,
|
|
1632
1918
|
...(preUpgradeBackup === undefined ? [] : [`Pre-upgrade config snapshot: ${preUpgradeBackup}`]),
|
|
@@ -1639,10 +1925,11 @@ export async function upgradeConductor(
|
|
|
1639
1925
|
journal({ kind: "outcome", phase: "rollback-failed", ok: false, version: release.version, detail: rollback });
|
|
1640
1926
|
enqueueUpgradeReport(
|
|
1641
1927
|
"tier2",
|
|
1642
|
-
reportProject(
|
|
1928
|
+
reportProject(installProjects),
|
|
1643
1929
|
`upgrade to omp-conductor@${release.version} failed and ITS ROLLBACK ALSO FAILED`,
|
|
1644
1930
|
[
|
|
1645
1931
|
`Failure: ${failure}`,
|
|
1932
|
+
installAttribution,
|
|
1646
1933
|
`Rollback failure: ${rollback}`,
|
|
1647
1934
|
"The fleet may be at mixed versions; do not resume dispatch.",
|
|
1648
1935
|
"Restore manually from the journal at " + upgradeJournalPath(stateDir()) + ".",
|
|
@@ -1748,6 +2035,9 @@ export async function rollbackFromJournal(
|
|
|
1748
2035
|
cliVersion: snapshot.previous.cliVersion,
|
|
1749
2036
|
ompVersion: snapshot.previous.ompVersion,
|
|
1750
2037
|
herdrSource: snapshot.previous.herdrSource,
|
|
2038
|
+
...(snapshot.previous.management === undefined
|
|
2039
|
+
? {}
|
|
2040
|
+
: { management: snapshot.previous.management }),
|
|
1751
2041
|
};
|
|
1752
2042
|
let configBefore: string | undefined;
|
|
1753
2043
|
if (request.configBackup !== undefined && existsSync(request.configBackup)) {
|
|
@@ -1779,6 +2069,10 @@ export async function rollbackFromJournal(
|
|
|
1779
2069
|
configBefore,
|
|
1780
2070
|
request.configBackup,
|
|
1781
2071
|
);
|
|
2072
|
+
// The restored bytes remain host-wide unverified state until the returning
|
|
2073
|
+
// daemon reads all three surfaces. A project-scoped request cannot leave its
|
|
2074
|
+
// siblings admitting in that interval.
|
|
2075
|
+
deps.setGlobalRecoveryPaused(true);
|
|
1782
2076
|
|
|
1783
2077
|
const restoredVersion = previous.cliVersion;
|
|
1784
2078
|
appendJournal(root, {
|
|
@@ -1791,10 +2085,17 @@ export async function rollbackFromJournal(
|
|
|
1791
2085
|
});
|
|
1792
2086
|
enqueueUpgradeReport(
|
|
1793
2087
|
"tier2",
|
|
1794
|
-
reportProject(request.selectors),
|
|
2088
|
+
reportProject(request.installProjects ?? request.selectors),
|
|
1795
2089
|
`fleet restored to omp-conductor@${restoredVersion} after the failed upgrade to ${request.version}`,
|
|
1796
2090
|
[
|
|
1797
2091
|
"The transient rollback unit restored every surface from the durable snapshot;",
|
|
2092
|
+
`Host-global install holder: ${request.installHolder ?? "unknown"}; affected projects: ${
|
|
2093
|
+
(request.installProjects ?? request.selectors).length === 0
|
|
2094
|
+
? "all configured projects"
|
|
2095
|
+
: (request.installProjects ?? request.selectors)
|
|
2096
|
+
.map((selector) => selector ?? "(default)")
|
|
2097
|
+
.join(", ")
|
|
2098
|
+
}.`,
|
|
1798
2099
|
"dispatch stays paused until the first tick after these restarts verifies the old version.",
|
|
1799
2100
|
`Pre-upgrade config snapshot: ${request.configBackup ?? "none recorded"}`,
|
|
1800
2101
|
],
|
package/src/verbs/actions.ts
CHANGED
|
@@ -1008,7 +1008,12 @@ export function githubVerbActions(
|
|
|
1008
1008
|
return { code: ran.ok ? 0 : 1, stdout: ran.stdout, stderr: ran.stderr };
|
|
1009
1009
|
},
|
|
1010
1010
|
env(),
|
|
1011
|
-
{
|
|
1011
|
+
{
|
|
1012
|
+
kind: "install",
|
|
1013
|
+
version: execution.version,
|
|
1014
|
+
holder: execution.holder,
|
|
1015
|
+
projects: execution.projects,
|
|
1016
|
+
},
|
|
1012
1017
|
);
|
|
1013
1018
|
if (!launched.ok) return { ok: false, stderr: launched.stderr };
|
|
1014
1019
|
return { ok: true, detail: launched.unit };
|