omp-conductor 0.8.0 → 0.9.0

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/src/upgrade.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { backupTimestamp, copyToUniqueBackup } from "./backups.ts";
2
3
  import { inspectBriefLayout, type BriefLayout } from "./brief-upgrade.ts";
3
4
  import { setPaused, statusSnapshot } from "./daemon.ts";
4
5
  import { fleetLayers, telegramStateDir, type DispatchLayer, type FleetLayers } from "./fleet.ts";
5
6
  import { livingDaemon, restartDaemon } from "./lifecycle.ts";
6
- import { configPath, findProject, loadConfig, resolveCaps, writeConfigRaw } from "./config.ts";
7
+ import { configBackupDir, configPath, findProject, loadConfig, resolveCaps, writeConfigRaw } from "./config.ts";
7
8
  import { renderBriefForProject } from "./setup.ts";
8
9
  import {
9
10
  STAGED_SERVICE_NAME,
@@ -327,6 +328,7 @@ async function rollbackUpgrade(
327
328
  herdrReloadStarted: boolean,
328
329
  daemonReloadStarted: boolean,
329
330
  configBefore?: string,
331
+ preUpgradeBackup?: string,
330
332
  ): Promise<void> {
331
333
  const failures: string[] = [];
332
334
 
@@ -344,12 +346,23 @@ async function rollbackUpgrade(
344
346
  const path = configPath();
345
347
  if (readFileSync(path, "utf8") !== configBefore) {
346
348
  deps.log("rollback: conductor config.json");
349
+ // Name the durable snapshot: the in-memory restore only runs while
350
+ // this process is alive, and an upgrade that died mid-flight leaves
351
+ // the operator needing exactly these bytes from disk.
352
+ deps.log(
353
+ preUpgradeBackup === undefined
354
+ ? "rollback: config.json changed during the upgrade; no durable snapshot was persisted"
355
+ : `rollback: config.json changed during the upgrade; pre-upgrade snapshot at ${preUpgradeBackup}`,
356
+ );
347
357
  // Exact bytes. `writeConfigFile(JSON.parse(...))` returns canonically
348
358
  // formatted output, which restores the keys but not the file.
349
359
  writeConfigRaw(configBefore);
350
360
  }
351
361
  } catch (err) {
352
- failures.push(`could not restore config.json: ${err instanceof Error ? err.message : String(err)}`);
362
+ failures.push(
363
+ `could not restore config.json: ${err instanceof Error ? err.message : String(err)}` +
364
+ (preUpgradeBackup === undefined ? "" : `; pre-upgrade snapshot at ${preUpgradeBackup}`),
365
+ );
353
366
  }
354
367
  }
355
368
  const restore = async (label: string, command: string, args: readonly string[]): Promise<void> => {
@@ -496,13 +509,23 @@ export async function upgradeConductor(
496
509
  // Snapshotted before anything is installed, because the new daemon starts
497
510
  // during this transaction and may migrate the file — see the restore in
498
511
  // `rollbackUpgrade`. Read as bytes, not through the loader: the point is to
499
- // put back exactly what was there, dialect and all.
512
+ // put back exactly what was there, dialect and all. The same bytes are also
513
+ // persisted under state, so a process death between here and the rollback
514
+ // cannot take the only pre-upgrade copy with it.
500
515
  let configBefore: string | undefined;
516
+ let preUpgradeBackup: string | undefined;
501
517
  try {
502
518
  configBefore = readFileSync(configPath(), "utf8");
519
+ preUpgradeBackup = copyToUniqueBackup(
520
+ configPath(),
521
+ configBackupDir(),
522
+ `config.json.pre-upgrade-${backupTimestamp()}`,
523
+ );
524
+ deps.log(`config backed up to ${preUpgradeBackup}`);
503
525
  } catch {
504
526
  // No readable config is not a reason to refuse an upgrade; there is simply
505
- // nothing to put back.
527
+ // nothing to put back. A snapshot that failed to persist is not fatal
528
+ // either — the in-memory restore below still covers a clean rollback.
506
529
  }
507
530
 
508
531
  if (brief.kind === "missing") throw new Error("no ORCHESTRATOR.md exists for the configured project");
@@ -608,6 +631,7 @@ export async function upgradeConductor(
608
631
  herdrReloadStarted,
609
632
  daemonReloadStarted,
610
633
  configBefore,
634
+ preUpgradeBackup,
611
635
  );
612
636
  } catch (rollbackErr) {
613
637
  const rollback = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);