@vtxmacro/cli 2026.8.39 → 2026.8.40

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.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/bin/vtx.js +646 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,6 +15,11 @@ The VTX host verifies that native runtime's version and digest before use; it
15
15
  does not borrow a Codex binary from PATH or an editor extension.
16
16
  For Windows-login auto-start, install and configure the CLI from native Windows;
17
17
  a CLI installed only inside WSL cannot start the WSL virtual machine at login.
18
+ An npm update does not replace code already loaded by a running durable service.
19
+ After updating, check `vtx inference-host service status --json`. If the service
20
+ was already desired-running, run `vtx inference-host service stop` followed by
21
+ `vtx inference-host service start`, then verify status and logs. Leave an
22
+ intentionally stopped service stopped until you want to run it.
18
23
 
19
24
  ## Configure
20
25
 
@@ -86,6 +91,21 @@ distinct. Use `service uninstall --instance <name>` to remove one worker;
86
91
  unqualified `service uninstall` removes the whole supervisor. The legacy
87
92
  unqualified host commands continue to target `default`.
88
93
 
94
+ On native Windows, if service status or logs show that guarded recovery is
95
+ blocking one installed subscription, recover that exact instance without rebooting:
96
+
97
+ ```bash
98
+ vtx inference-host service recover --instance <name> --force-recovery --json
99
+ ```
100
+
101
+ Recovery is intentionally noninteractive and requires both `--instance` and
102
+ `--force-recovery`; `--json` changes only the output format. The shared
103
+ supervisor briefly quiesces so every installed worker can stop cooperatively,
104
+ then VTX restores its previous desired state and the same peer subscriptions.
105
+ The command never kills an arbitrary process, logs either account out, changes
106
+ VTX profile settings, or controls a Trader. If any package-owned VTX automation
107
+ is still live, recovery fails closed and preserves its evidence.
108
+
89
109
  For each VTX profile and lane, **Subscription #1** is tried first and later
90
110
  compatible subscriptions follow in the saved Account cascade order.
91
111
  Quota/credits exhaustion, unusable authentication, and exhausted recoverable
package/bin/vtx.js CHANGED
@@ -38,7 +38,7 @@ var init_agent_cli_release = __esm({
38
38
  "agent-cli-release.json"() {
39
39
  agent_cli_release_default = {
40
40
  package_name: "@vtxmacro/cli",
41
- package_version: "2026.8.39",
41
+ package_version: "2026.8.40",
42
42
  codex_package_name: "@openai/codex",
43
43
  codex_version: "0.147.0",
44
44
  platforms: {
@@ -21473,10 +21473,10 @@ import {
21473
21473
  realpath as realpath3,
21474
21474
  rm as rm3
21475
21475
  } from "node:fs/promises";
21476
- import { randomBytes as randomBytes3 } from "node:crypto";
21476
+ import { createHash as createHash4, randomBytes as randomBytes3 } from "node:crypto";
21477
21477
  import { tmpdir as tmpdir2 } from "node:os";
21478
21478
  import { isAbsolute as isAbsolute3, join as join4, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
21479
- var MAX_PROMPT_BYTES2, CODEX_MODEL_NAME_PATTERN, CODEX_REASONING_EFFORT_PATTERN, tokenUsageReceiptSchema, turnResultReceiptSchema, terminalReceiptSchema, recoveryCheckpointSchema, recoveryFileSchema, FileCodexAttemptRecoveryStore, utf8Bytes, assertAttemptActive, tomlString, permissionConfig, ensureDedicatedCodexHome, createIsolatedCodexAttemptResources, createIsolatedCodexHostResources, validateAttemptInput, isStrictDescendant, assertRecoveryResourceScope, removeRecoveredThread, confirmGuardianTerminatedForRecovery, reconcileCodexAttemptRecovery, CodexSubscriptionAdapter;
21479
+ var MAX_PROMPT_BYTES2, CODEX_MODEL_NAME_PATTERN, CODEX_REASONING_EFFORT_PATTERN, codexRecoveryCheckpointProcessFingerprint, proveCodexSameBootQuiescence, tokenUsageReceiptSchema, turnResultReceiptSchema, terminalReceiptSchema, recoveryCheckpointSchema, recoveryFileSchema, FileCodexAttemptRecoveryStore, utf8Bytes, assertAttemptActive, tomlString, permissionConfig, ensureDedicatedCodexHome, createIsolatedCodexAttemptResources, createIsolatedCodexHostResources, validateAttemptInput, isStrictDescendant, assertRecoveryResourceScope, removeRecoveredThread, confirmGuardianTerminatedForRecovery, reconcileCodexAttemptRecovery, CodexSubscriptionAdapter;
21480
21480
  var init_codex_adapter = __esm({
21481
21481
  "lib/inference-host/codex-adapter.ts"() {
21482
21482
  "use strict";
@@ -21487,6 +21487,34 @@ var init_codex_adapter = __esm({
21487
21487
  MAX_PROMPT_BYTES2 = 3e5;
21488
21488
  CODEX_MODEL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
21489
21489
  CODEX_REASONING_EFFORT_PATTERN = /^[a-z][a-z0-9_-]{0,31}$/u;
21490
+ codexRecoveryCheckpointProcessFingerprint = (checkpoint) => createHash4("sha256").update(JSON.stringify({
21491
+ attemptId: checkpoint.attemptId,
21492
+ processToken: checkpoint.processToken,
21493
+ processReceiptPath: checkpoint.processReceiptPath,
21494
+ processState: checkpoint.processState,
21495
+ bootIdentity: checkpoint.bootIdentity ?? null,
21496
+ cleanupConfirmed: checkpoint.cleanupConfirmed
21497
+ })).digest("hex");
21498
+ proveCodexSameBootQuiescence = async (options) => {
21499
+ const readBootIdentity = options.readBootIdentity ?? readInferenceSystemBootIdentity;
21500
+ const bootIdentityBefore = await readBootIdentity();
21501
+ await options.confirmNoManagedCodexProcesses();
21502
+ const bootIdentityAfter = await readBootIdentity();
21503
+ if (bootIdentityAfter !== bootIdentityBefore) {
21504
+ throw new Error("System boot identity changed during Codex recovery process inspection.");
21505
+ }
21506
+ return {
21507
+ schemaVersion: "vtx_codex_same_boot_quiescence_v1",
21508
+ bootIdentity: bootIdentityAfter,
21509
+ observedAtMs: (options.now ?? Date.now)(),
21510
+ checkpointFingerprints: Object.fromEntries(
21511
+ Object.entries(options.attempts).map(([attemptId, checkpoint]) => [
21512
+ attemptId,
21513
+ codexRecoveryCheckpointProcessFingerprint(checkpoint)
21514
+ ])
21515
+ )
21516
+ };
21517
+ };
21490
21518
  tokenUsageReceiptSchema = external_exports.strictObject({
21491
21519
  inputTokens: external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
21492
21520
  cachedInputTokens: external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
@@ -21959,11 +21987,20 @@ var init_codex_adapter = __esm({
21959
21987
  throw new Error("Codex cross-boot recovery receipt ownership is unconfirmed.");
21960
21988
  }
21961
21989
  } else {
21962
- await confirmGuardianTerminatedForRecovery(
21963
- checkpoint,
21964
- guardian,
21965
- options.deadlineMs ?? 5e3
21966
- );
21990
+ const receipt = await readCodexGuardianReceipt(guardian.receiptPath);
21991
+ if (receipt === null && options.sameBootQuiescenceProof) {
21992
+ const proof = options.sameBootQuiescenceProof;
21993
+ const expectedFingerprint = proof.checkpointFingerprints[checkpoint.attemptId];
21994
+ if (proof.schemaVersion !== "vtx_codex_same_boot_quiescence_v1" || proof.bootIdentity !== currentBootIdentity || checkpoint.bootIdentity !== currentBootIdentity || !Number.isFinite(proof.observedAtMs) || proof.observedAtMs > Date.now() + 1e3 || Date.now() - proof.observedAtMs > 3e4 || expectedFingerprint !== codexRecoveryCheckpointProcessFingerprint(checkpoint)) {
21995
+ throw new Error("Codex same-boot recovery process fence is invalid or stale.");
21996
+ }
21997
+ } else {
21998
+ await confirmGuardianTerminatedForRecovery(
21999
+ checkpoint,
22000
+ guardian,
22001
+ options.deadlineMs ?? 5e3
22002
+ );
22003
+ }
21967
22004
  }
21968
22005
  assertRecoveryResourceScope(checkpoint);
21969
22006
  await removeRecoveredThread(checkpoint, options.codexHome);
@@ -22607,11 +22644,147 @@ var init_codex_adapter = __esm({
22607
22644
  }
22608
22645
  });
22609
22646
 
22647
+ // lib/inference-host/codex-recovery-process.ts
22648
+ import { spawn as spawn5 } from "node:child_process";
22649
+ var normalizedWindowsPath, classifyCodexRecoveryProcesses, WINDOWS_PROCESS_QUERY, queryWindowsProcesses, confirmNoVtxManagedCodexProcesses;
22650
+ var init_codex_recovery_process = __esm({
22651
+ "lib/inference-host/codex-recovery-process.ts"() {
22652
+ "use strict";
22653
+ normalizedWindowsPath = (value) => value.replaceAll("\\", "/").replaceAll(/\/+$/gu, "").toLowerCase();
22654
+ classifyCodexRecoveryProcesses = (processes, pinnedBinaryPath) => {
22655
+ const pinned = normalizedWindowsPath(pinnedBinaryPath);
22656
+ let managedProcessCount = 0;
22657
+ let ambiguousProcessCount = 0;
22658
+ for (const process3 of processes) {
22659
+ const name = process3.name.trim().toLowerCase();
22660
+ if (name === "codex.exe") {
22661
+ if (!process3.executable_path || !process3.command_line || !process3.creation_date) {
22662
+ ambiguousProcessCount += 1;
22663
+ continue;
22664
+ }
22665
+ const executable = normalizedWindowsPath(process3.executable_path);
22666
+ const packageOwned = executable === pinned || executable.includes("/node_modules/@vtxmacro/") && executable.includes("/node_modules/@openai/codex-win32-x64/") || executable.includes("/node_modules/@vtxmacro/.cli-") && executable.includes("/node_modules/@openai/codex-win32-x64/");
22667
+ if (packageOwned) managedProcessCount += 1;
22668
+ continue;
22669
+ }
22670
+ if (name !== "node.exe") continue;
22671
+ if (!process3.executable_path || !process3.command_line || !process3.creation_date) {
22672
+ ambiguousProcessCount += 1;
22673
+ continue;
22674
+ }
22675
+ if (process3.command_line.includes("--input-type=module") && process3.command_line.includes("vtx_codex_guardian_v1") && process3.command_line.includes("processToken") && process3.command_line.includes("receiptPath")) {
22676
+ managedProcessCount += 1;
22677
+ }
22678
+ }
22679
+ return {
22680
+ managed_process_count: managedProcessCount,
22681
+ ambiguous_process_count: ambiguousProcessCount
22682
+ };
22683
+ };
22684
+ WINDOWS_PROCESS_QUERY = String.raw`
22685
+ $ErrorActionPreference = 'Stop'
22686
+ $items = @(Get-CimInstance Win32_Process | Where-Object { $_.Name -in @('node.exe', 'codex.exe') } | ForEach-Object {
22687
+ [ordered]@{
22688
+ process_id = [int]$_.ProcessId
22689
+ name = [string]$_.Name
22690
+ executable_path = if ($null -eq $_.ExecutablePath) { $null } else { [string]$_.ExecutablePath }
22691
+ command_line = if ($null -eq $_.CommandLine) { $null } else { [string]$_.CommandLine }
22692
+ creation_date = if ($null -eq $_.CreationDate) { $null } else { [string]$_.CreationDate }
22693
+ }
22694
+ })
22695
+ [Console]::Out.Write(($items | ConvertTo-Json -Depth 3 -Compress))
22696
+ `;
22697
+ queryWindowsProcesses = async () => {
22698
+ const args = [
22699
+ "-NoLogo",
22700
+ "-NoProfile",
22701
+ "-NonInteractive",
22702
+ "-EncodedCommand",
22703
+ Buffer.from(WINDOWS_PROCESS_QUERY, "utf16le").toString("base64")
22704
+ ];
22705
+ const result2 = await new Promise(
22706
+ (resolvePromise, reject) => {
22707
+ const child = spawn5("powershell.exe", args, {
22708
+ windowsHide: true,
22709
+ stdio: ["ignore", "pipe", "pipe"]
22710
+ });
22711
+ let stdout = "";
22712
+ let stderr = "";
22713
+ child.stdout.on("data", (chunk) => {
22714
+ stdout = `${stdout}${chunk.toString("utf8")}`.slice(-4e6);
22715
+ });
22716
+ child.stderr.on("data", (chunk) => {
22717
+ stderr = `${stderr}${chunk.toString("utf8")}`.slice(-4096);
22718
+ });
22719
+ child.once("error", reject);
22720
+ child.once("close", (code) => {
22721
+ if (code !== 0) {
22722
+ reject(new Error("Windows process ownership inventory failed."));
22723
+ return;
22724
+ }
22725
+ resolvePromise({ exitCode: code ?? 1, stdout });
22726
+ });
22727
+ }
22728
+ );
22729
+ if (result2.exitCode !== 0) throw new Error("Windows process ownership inventory failed.");
22730
+ let parsed;
22731
+ try {
22732
+ parsed = result2.stdout.trim() ? JSON.parse(result2.stdout) : [];
22733
+ } catch {
22734
+ throw new Error("Windows process ownership inventory was invalid.");
22735
+ }
22736
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
22737
+ return entries.map((entry) => {
22738
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
22739
+ throw new Error("Windows process ownership inventory was invalid.");
22740
+ }
22741
+ const record2 = entry;
22742
+ if (!Number.isSafeInteger(record2.process_id) || Number(record2.process_id) < 1 || typeof record2.name !== "string" || record2.executable_path !== null && typeof record2.executable_path !== "string" || record2.command_line !== null && typeof record2.command_line !== "string" || record2.creation_date !== null && typeof record2.creation_date !== "string") {
22743
+ throw new Error("Windows process ownership inventory was invalid.");
22744
+ }
22745
+ return {
22746
+ process_id: Number(record2.process_id),
22747
+ name: record2.name,
22748
+ executable_path: record2.executable_path,
22749
+ command_line: record2.command_line,
22750
+ creation_date: record2.creation_date
22751
+ };
22752
+ });
22753
+ };
22754
+ confirmNoVtxManagedCodexProcesses = async (options) => {
22755
+ if ((options.platform ?? process.platform) !== "win32") {
22756
+ throw new Error("Same-boot service recovery is currently supported only on Windows.");
22757
+ }
22758
+ const queryProcesses = options.queryProcesses ?? queryWindowsProcesses;
22759
+ const sleep4 = options.sleep ?? (async (milliseconds) => {
22760
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
22761
+ });
22762
+ for (let observation = 0; observation < 2; observation += 1) {
22763
+ const classification = classifyCodexRecoveryProcesses(
22764
+ await queryProcesses(),
22765
+ options.pinnedBinaryPath
22766
+ );
22767
+ if (classification.ambiguous_process_count > 0) {
22768
+ throw new Error(
22769
+ "Codex process ownership could not be inspected completely; recovery evidence was preserved."
22770
+ );
22771
+ }
22772
+ if (classification.managed_process_count > 0) {
22773
+ throw new Error(
22774
+ "VTX-managed Codex automation is still running; recovery evidence was preserved."
22775
+ );
22776
+ }
22777
+ if (observation === 0) await sleep4(250);
22778
+ }
22779
+ };
22780
+ }
22781
+ });
22782
+
22610
22783
  // lib/inference-host/crypto.ts
22611
22784
  import {
22612
22785
  createCipheriv,
22613
22786
  createDecipheriv,
22614
- createHash as createHash4,
22787
+ createHash as createHash5,
22615
22788
  createPrivateKey,
22616
22789
  createPublicKey,
22617
22790
  diffieHellman,
@@ -22650,7 +22823,7 @@ var init_crypto = __esm({
22650
22823
  const aad = envelopeAadSchema.parse(input);
22651
22824
  return Buffer.from(JSON.stringify(canonicalize(aad)), "utf8");
22652
22825
  };
22653
- externalInferenceSha256 = (value) => createHash4("sha256").update(value).digest("hex");
22826
+ externalInferenceSha256 = (value) => createHash5("sha256").update(value).digest("hex");
22654
22827
  decodeCanonicalBase64Url = (value, fieldName, expectedBytes) => {
22655
22828
  if (!/^[A-Za-z0-9_-]+$/u.test(value)) {
22656
22829
  throw new ExternalInferenceEnvelopeError("invalid_encoding", `${fieldName} is invalid.`);
@@ -22749,7 +22922,7 @@ var init_crypto = __esm({
22749
22922
  return Buffer.from(hkdfSync(
22750
22923
  "sha256",
22751
22924
  root,
22752
- createHash4("sha256").update(aadBytes).digest(),
22925
+ createHash5("sha256").update(aadBytes).digest(),
22753
22926
  info,
22754
22927
  32
22755
22928
  ));
@@ -29163,7 +29336,7 @@ var require_ajv = __commonJS({
29163
29336
  });
29164
29337
 
29165
29338
  // lib/inference-host/runner.ts
29166
- import { createHash as createHash5 } from "node:crypto";
29339
+ import { createHash as createHash6 } from "node:crypto";
29167
29340
  function createDefaultInferenceHostRunnerDependencies(options) {
29168
29341
  const fetchImpl = options.fetchImpl ?? fetch;
29169
29342
  return {
@@ -29471,7 +29644,7 @@ var init_runner = __esm({
29471
29644
  attempts
29472
29645
  };
29473
29646
  };
29474
- sha256 = (value) => createHash5("sha256").update(value, "utf8").digest("hex");
29647
+ sha256 = (value) => createHash6("sha256").update(value, "utf8").digest("hex");
29475
29648
  stableOperationId = (kind, parts) => `${kind}_${sha256(JSON.stringify(parts)).slice(0, 48)}`;
29476
29649
  buildAttemptStartRequest = (claim, attemptId, startedAt) => attemptStartRequestSchema.parse({
29477
29650
  schema_version: "external_inference_attempt_start_v1",
@@ -31110,7 +31283,7 @@ var init_runner = __esm({
31110
31283
  });
31111
31284
 
31112
31285
  // lib/inference-host/service.ts
31113
- import { spawn as spawn5 } from "node:child_process";
31286
+ import { spawn as spawn6 } from "node:child_process";
31114
31287
  import { randomUUID } from "node:crypto";
31115
31288
  import { createWriteStream, readFileSync } from "node:fs";
31116
31289
  import { access as access3, chmod as chmod3, mkdir as mkdir3, readFile as readFile5, rm as rm4, writeFile as writeFile2 } from "node:fs/promises";
@@ -31156,7 +31329,7 @@ var init_service = __esm({
31156
31329
  plistEscape = xmlEscape;
31157
31330
  systemdQuote = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`;
31158
31331
  defaultRunCommand = async (command, args) => await new Promise((resolvePromise) => {
31159
- const child = spawn5(command, [...args], {
31332
+ const child = spawn6(command, [...args], {
31160
31333
  windowsHide: true,
31161
31334
  stdio: ["ignore", "pipe", "pipe"]
31162
31335
  });
@@ -31629,8 +31802,10 @@ WantedBy=default.target
31629
31802
  }
31630
31803
  throw new Error("Background service did not reach an active state within 10 seconds.");
31631
31804
  }
31632
- async restoreRunningSupervisor(manifest) {
31633
- const restoredManifest = assertManifest({
31805
+ async restoreRunningSupervisor(manifest, preserveGeneration = false, targetInstanceName) {
31806
+ const previousRuntime = await readRuntimeAcrossAtomicReplacement(this.runtimePath()).catch(() => null);
31807
+ const previousRuntimeUpdatedAt = previousRuntime?.manifest_generation === manifest.generation ? Date.parse(previousRuntime.updated_at) : Number.NaN;
31808
+ const restoredManifest = preserveGeneration ? manifest : assertManifest({
31634
31809
  ...manifest,
31635
31810
  generation: randomUUID(),
31636
31811
  installed_at: this.now().toISOString()
@@ -31658,10 +31833,15 @@ ${result2.stderr}`)) {
31658
31833
  }
31659
31834
  await this.waitForManagerActive();
31660
31835
  if (!this.confirmInitialReadiness) return await this.status();
31661
- const runtime = await this.waitForManifestApplied(restoredManifest);
31836
+ const runtime = await this.waitForManifestApplied(
31837
+ restoredManifest,
31838
+ targetInstanceName,
31839
+ this.reconcileWaitAttempts,
31840
+ Number.isFinite(previousRuntimeUpdatedAt) ? previousRuntimeUpdatedAt : void 0
31841
+ );
31662
31842
  return await this.status(runtime);
31663
31843
  }
31664
- async waitForManifestApplied(manifest, targetInstanceName, maxAttempts = this.reconcileWaitAttempts) {
31844
+ async waitForManifestApplied(manifest, targetInstanceName, maxAttempts = this.reconcileWaitAttempts, minimumRuntimeUpdatedAtExclusive) {
31665
31845
  let consecutiveReadyObservations = 0;
31666
31846
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
31667
31847
  let runtime = null;
@@ -31672,7 +31852,9 @@ ${result2.stderr}`)) {
31672
31852
  throw error48;
31673
31853
  }
31674
31854
  }
31675
- if (runtime?.manifest_generation === manifest.generation) {
31855
+ const runtimeUpdatedAt = runtime ? Date.parse(runtime.updated_at) : Number.NaN;
31856
+ const runtimeIsFresh = minimumRuntimeUpdatedAtExclusive === void 0 || Number.isFinite(runtimeUpdatedAt) && runtimeUpdatedAt > minimumRuntimeUpdatedAtExclusive;
31857
+ if (runtime?.manifest_generation === manifest.generation && runtimeIsFresh) {
31676
31858
  const configuredNames = manifest.workers.map((worker) => worker.instance_name).sort();
31677
31859
  const runtimeNames = runtime.workers.map((worker) => worker.instance_name).sort();
31678
31860
  const exactWorkerSet = JSON.stringify(configuredNames) === JSON.stringify(runtimeNames);
@@ -31934,7 +32116,71 @@ ${result2.stderr}`)) {
31934
32116
  async stop() {
31935
32117
  return await this.withControlLock(async () => await this.stopUnlocked());
31936
32118
  }
31937
- async stopUnlocked() {
32119
+ /**
32120
+ * Quiesce the complete installed worker boundary while one exact instance's
32121
+ * durable Codex recovery evidence is reconciled. The service process lock is
32122
+ * held for the callback, so neither the manager nor a manual run-internal can
32123
+ * recreate workers during the same-boot process inspection.
32124
+ */
32125
+ async recoverInstance(instanceName, operation, options = {}) {
32126
+ return await this.withControlLock(async () => {
32127
+ const manifest = await readInferenceHostServiceManifest(this.manifestPath());
32128
+ if (!manifest) throw new Error("Inference-host service is not installed.");
32129
+ if (!manifest.workers.some((worker) => worker.instance_name === instanceName)) {
32130
+ throw new Error(`Inference-host instance ${instanceName} is not installed.`);
32131
+ }
32132
+ if (options.expectedManifestGeneration && options.expectedManifestGeneration !== manifest.generation) {
32133
+ throw new Error("Inference-host service manifest changed during recovery.");
32134
+ }
32135
+ const recordedDesiredRunning = await readInferenceHostServiceDesired(this.desiredPath());
32136
+ const desiredRunning = options.restoreDesiredRunning ?? recordedDesiredRunning;
32137
+ const context = { manifest, desiredRunning };
32138
+ await options.prepare?.(context);
32139
+ const before = await this.status();
32140
+ if (recordedDesiredRunning || before.manager_active) {
32141
+ await this.stopUnlocked(true);
32142
+ } else {
32143
+ await writeDesired(this.desiredPath(), false, this.now());
32144
+ }
32145
+ let serviceFence = null;
32146
+ let result2;
32147
+ let operationError = null;
32148
+ try {
32149
+ serviceFence = await this.acquireProcessLock(
32150
+ `${this.config.supervisorProcessLockPath}.service`
32151
+ );
32152
+ const stopped = await this.status();
32153
+ if (stopped.manager_active) {
32154
+ throw new Error("Background service manager became active during recovery quiescence.");
32155
+ }
32156
+ result2 = await operation(context);
32157
+ } catch (error48) {
32158
+ operationError = error48;
32159
+ } finally {
32160
+ await serviceFence?.release().catch((error48) => {
32161
+ operationError = operationError ?? error48;
32162
+ });
32163
+ }
32164
+ let restoredStatus;
32165
+ try {
32166
+ restoredStatus = desiredRunning ? await this.restoreRunningSupervisor(manifest, true, instanceName) : await this.status();
32167
+ } catch (restoreError) {
32168
+ if (operationError) {
32169
+ throw new Error(
32170
+ "Inference-host recovery failed and the previous supervisor could not be restored.",
32171
+ { cause: new AggregateError([operationError, restoreError]) }
32172
+ );
32173
+ }
32174
+ throw new Error(
32175
+ "Inference-host recovery completed, but the previous supervisor could not be restored.",
32176
+ { cause: restoreError }
32177
+ );
32178
+ }
32179
+ if (operationError) throw operationError;
32180
+ return { result: result2, status: restoredStatus };
32181
+ });
32182
+ }
32183
+ async stopUnlocked(preserveManifestGenerationOnFailure = false) {
31938
32184
  const manifest = await readInferenceHostServiceManifest(this.manifestPath());
31939
32185
  if (!manifest) {
31940
32186
  throw new Error("Inference-host service is not installed.");
@@ -31976,7 +32222,7 @@ ${result2.stderr}`)) {
31976
32222
  } catch (error48) {
31977
32223
  if (!restoreOnFailure) throw error48;
31978
32224
  try {
31979
- await this.restoreRunningSupervisor(manifest);
32225
+ await this.restoreRunningSupervisor(manifest, preserveManifestGenerationOnFailure);
31980
32226
  } catch (restoreError) {
31981
32227
  throw new Error(
31982
32228
  "Inference-host stop failed and the previous supervisor could not be restored.",
@@ -32143,7 +32389,7 @@ ${result2.stderr}`)) {
32143
32389
  `);
32144
32390
  }
32145
32391
  };
32146
- const child = spawn5(manifest.executable, args, {
32392
+ const child = spawn6(manifest.executable, args, {
32147
32393
  env: inferenceHostServiceChildEnvironment(worker.runtime_environment),
32148
32394
  windowsHide: true,
32149
32395
  stdio: ["pipe", "pipe", "pipe"]
@@ -32433,8 +32679,8 @@ __export(cli_exports, {
32433
32679
  registerInferenceHostServiceControlInput: () => registerInferenceHostServiceControlInput,
32434
32680
  runInferenceHostCli: () => runInferenceHostCli
32435
32681
  });
32436
- import { randomUUID as randomUUID2 } from "node:crypto";
32437
- import { spawn as spawn6 } from "node:child_process";
32682
+ import { createHash as createHash7, randomUUID as randomUUID2 } from "node:crypto";
32683
+ import { spawn as spawn7 } from "node:child_process";
32438
32684
  import { lstat as lstat4, realpath as realpath4, rm as rm5 } from "node:fs/promises";
32439
32685
  import { join as join6, resolve as resolve5 } from "node:path";
32440
32686
  async function runInferenceHostCli(argv2, env = process.env, dependencies = {}) {
@@ -32444,6 +32690,21 @@ async function runInferenceHostCli(argv2, env = process.env, dependencies = {})
32444
32690
  return { exitCode: 0, stdout: INFERENCE_HOST_HELP, stderr: "" };
32445
32691
  }
32446
32692
  const parsed = parseInferenceHostArgs(argv2, env);
32693
+ if (parsed.forceRecovery && !(parsed.command === "service" && parsed.serviceAction === "recover")) {
32694
+ throw new Error("--force-recovery is accepted only by service recover.");
32695
+ }
32696
+ if (parsed.command === "service" && parsed.serviceAction === "recover") {
32697
+ if (!parsed.instanceFlagExplicit) {
32698
+ throw new Error(
32699
+ "Service recovery requires an explicit --instance NAME; an environment or default instance is not accepted."
32700
+ );
32701
+ }
32702
+ if (!parsed.forceRecovery) {
32703
+ throw new Error(
32704
+ `Service recovery is disruptive. Rerun with --instance ${parsed.instanceName} --force-recovery after checking service status and logs.`
32705
+ );
32706
+ }
32707
+ }
32447
32708
  const config2 = await resolveInferenceHostCommandConfig(parsed, env, dependencies);
32448
32709
  if (parsed.command === "login") {
32449
32710
  return await login(config2, parsed, env, dependencies, warnings);
@@ -32482,7 +32743,18 @@ async function runInferenceHostCli(argv2, env = process.env, dependencies = {})
32482
32743
  );
32483
32744
  }
32484
32745
  if (parsed.command === "service") {
32485
- return await serviceCommand(config2, parsed, env, dependencies, warnings);
32746
+ const mutationActions = /* @__PURE__ */ new Set(["install", "start", "stop", "recover", "uninstall"]);
32747
+ if (!parsed.serviceAction || !mutationActions.has(parsed.serviceAction)) {
32748
+ return await serviceCommand(config2, parsed, env, dependencies, warnings);
32749
+ }
32750
+ const recoveryCommandLock = await acquireInferenceHostProcessLock(
32751
+ serviceRecoveryCommandLockPath(config2)
32752
+ );
32753
+ try {
32754
+ return await serviceCommand(config2, parsed, env, dependencies, warnings);
32755
+ } finally {
32756
+ await recoveryCommandLock.release();
32757
+ }
32486
32758
  }
32487
32759
  if (parsed.command === "status") {
32488
32760
  return await localStatus(config2, parsed, dependencies, warnings);
@@ -32511,7 +32783,7 @@ async function runInferenceHostCli(argv2, env = process.env, dependencies = {})
32511
32783
  };
32512
32784
  }
32513
32785
  }
32514
- var INFERENCE_HOST_CLI_VERSION, runtimeReceiptPath, codexRecoveryPath, codexGuardianReceiptRoot, revocationCheckpointPath, foregroundHostLockPath, credentialLifecycleLockPath, AGENT_HEARTBEAT_INTERVAL_MS, AGENT_HEARTBEAT_MAX_RETRY_DELAY_MS, retryableAgentHeartbeatError, assertRevocationCheckpoint, readRevocationCheckpoint, writeRevocationCheckpoint, clearRevocationCheckpoint, render, INFERENCE_HOST_HELP, parseHostConcurrency, parseInferenceHostArgs, defaultOpenBrowser, registerInferenceHostServiceControlInput, defaultRegisterLifecycleSignalHandlers, lifecycleCancellation, configuredCredentialStore, restoreInferenceHostCredentialContext, recoverInferenceHostCredentialContextTransition, accountKeyForState, assertCredentialMatchesState2, revocationCheckpointForCredential, assertRevocationCheckpointMatchesLocalIdentity, fileExistsPrivately, inspectCodexAuthentication, clearLocalRuntimeArtifacts, assertDurableServiceUninstalled, assertLocalRuntimeArtifactsMayBeDiscarded, defaultRunForeground, login, codexLogin, codexLogout, localStatus, doctor, cleanupLogin, runHost, defaultReadStdin, parseAgentStdin, agentOperationId, agentSession, agentConnect, agentRun, agentNext, agentComplete, agentFail, withAgentCommandLock, serviceCommand, hasExplicitCredentialStoreConfiguration, credentialEnvironmentForIdentity, commandRequiresRetainedCredentialStore, commandUsesInstalledServiceCredentials, resolveInferenceHostCommandConfig;
32786
+ var INFERENCE_HOST_CLI_VERSION, runtimeReceiptPath, codexRecoveryPath, codexGuardianReceiptRoot, revocationCheckpointPath, foregroundHostLockPath, credentialLifecycleLockPath, serviceRecoveryCommandLockPath, AGENT_HEARTBEAT_INTERVAL_MS, AGENT_HEARTBEAT_MAX_RETRY_DELAY_MS, retryableAgentHeartbeatError, assertRevocationCheckpoint, readRevocationCheckpoint, writeRevocationCheckpoint, clearRevocationCheckpoint, render, INFERENCE_HOST_HELP, parseHostConcurrency, parseInferenceHostArgs, defaultOpenBrowser, registerInferenceHostServiceControlInput, defaultRegisterLifecycleSignalHandlers, lifecycleCancellation, configuredCredentialStore, restoreInferenceHostCredentialContext, recoverInferenceHostCredentialContextTransition, accountKeyForState, assertCredentialMatchesState2, revocationCheckpointForCredential, assertRevocationCheckpointMatchesLocalIdentity, fileExistsPrivately, inspectCodexAuthentication, clearLocalRuntimeArtifacts, assertDurableServiceUninstalled, assertLocalRuntimeArtifactsMayBeDiscarded, defaultRunForeground, login, codexLogin, codexLogout, localStatus, doctor, cleanupLogin, runHost, defaultReadStdin, parseAgentStdin, agentOperationId, agentSession, agentConnect, agentRun, agentNext, agentComplete, agentFail, withAgentCommandLock, recoveryBackupPath, serviceRecoveryTransactionPath, readServiceRecoveryTransaction, assertResumableCodexRecoveryEvidence, recoverInstalledCodexService, serviceCommand, hasExplicitCredentialStoreConfiguration, credentialEnvironmentForIdentity, commandRequiresRetainedCredentialStore, commandUsesInstalledServiceCredentials, resolveInferenceHostCommandConfig;
32515
32787
  var init_cli = __esm({
32516
32788
  "lib/inference-host/cli.ts"() {
32517
32789
  "use strict";
@@ -32520,6 +32792,7 @@ var init_cli = __esm({
32520
32792
  init_agent_client();
32521
32793
  init_agent_state();
32522
32794
  init_codex_adapter();
32795
+ init_codex_recovery_process();
32523
32796
  init_codex_app_server();
32524
32797
  init_codex_binary();
32525
32798
  init_config();
@@ -32536,6 +32809,7 @@ var init_cli = __esm({
32536
32809
  revocationCheckpointPath = (config2) => `${config2.statePath}.revoke.json`;
32537
32810
  foregroundHostLockPath = (config2) => `${config2.processLockPath}.foreground`;
32538
32811
  credentialLifecycleLockPath = (config2) => `${inferenceHostCredentialContextPath(config2)}.lock`;
32812
+ serviceRecoveryCommandLockPath = (config2) => `${config2.supervisorProcessLockPath}.service-recovery-command`;
32539
32813
  AGENT_HEARTBEAT_INTERVAL_MS = 3e3;
32540
32814
  AGENT_HEARTBEAT_MAX_RETRY_DELAY_MS = 9e3;
32541
32815
  retryableAgentHeartbeatError = (error48) => {
@@ -32632,7 +32906,8 @@ Durable service:
32632
32906
  vtx inference-host codex-login --instance codex-2
32633
32907
  vtx inference-host service install --instance codex-2
32634
32908
  vtx inference-host service uninstall --instance codex-2
32635
- vtx inference-host service <start|stop|status|logs|uninstall>
32909
+ vtx inference-host service recover --instance codex-1 --force-recovery
32910
+ vtx inference-host service <start|stop|status|logs|recover|uninstall>
32636
32911
  `;
32637
32912
  parseHostConcurrency = (raw, label) => {
32638
32913
  const value = Number(raw);
@@ -32648,6 +32923,7 @@ Durable service:
32648
32923
  let maxConcurrency = configuredConcurrency ? parseHostConcurrency(configuredConcurrency, "VTX_INFERENCE_HOST_MAX_CONCURRENCY") : null;
32649
32924
  let instanceName = String(env.VTX_INFERENCE_HOST_INSTANCE || "default").trim();
32650
32925
  let instanceExplicit = Boolean(String(env.VTX_INFERENCE_HOST_INSTANCE || "").trim());
32926
+ let instanceFlagExplicit = false;
32651
32927
  let displayName = String(env.VTX_INFERENCE_HOST_DISPLAY_NAME || "").trim() || "Codex subscription host";
32652
32928
  let displayNameExplicit = Boolean(String(env.VTX_INFERENCE_HOST_DISPLAY_NAME || "").trim());
32653
32929
  let adapter = null;
@@ -32657,6 +32933,7 @@ Durable service:
32657
32933
  let waitSeconds = 50;
32658
32934
  let lines = 100;
32659
32935
  let serviceManifestPath = null;
32936
+ let forceRecovery = false;
32660
32937
  const positionals = [];
32661
32938
  for (let index = 0; index < argv2.length; index += 1) {
32662
32939
  const argument = argv2[index];
@@ -32668,6 +32945,10 @@ Durable service:
32668
32945
  once = true;
32669
32946
  continue;
32670
32947
  }
32948
+ if (argument === "--force-recovery") {
32949
+ forceRecovery = true;
32950
+ continue;
32951
+ }
32671
32952
  if (argument === "--max-concurrency") {
32672
32953
  const raw = argv2[index + 1];
32673
32954
  if (!raw) throw new Error("--max-concurrency requires a value.");
@@ -32706,6 +32987,7 @@ Durable service:
32706
32987
  if (argument === "--instance") {
32707
32988
  instanceName = raw.trim();
32708
32989
  instanceExplicit = true;
32990
+ instanceFlagExplicit = true;
32709
32991
  }
32710
32992
  index += 1;
32711
32993
  continue;
@@ -32736,6 +33018,7 @@ Durable service:
32736
33018
  maxConcurrency,
32737
33019
  instanceName,
32738
33020
  instanceExplicit,
33021
+ instanceFlagExplicit,
32739
33022
  displayName,
32740
33023
  adapter,
32741
33024
  modelId,
@@ -32744,13 +33027,14 @@ Durable service:
32744
33027
  waitSeconds,
32745
33028
  lines,
32746
33029
  serviceAction,
32747
- serviceManifestPath
33030
+ serviceManifestPath,
33031
+ forceRecovery
32748
33032
  };
32749
33033
  };
32750
33034
  defaultOpenBrowser = (url2) => {
32751
33035
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
32752
33036
  const args = process.platform === "win32" ? ["/c", "start", "", url2] : [url2];
32753
- const child = spawn6(command, args, {
33037
+ const child = spawn7(command, args, {
32754
33038
  detached: true,
32755
33039
  stdio: "ignore",
32756
33040
  windowsHide: true
@@ -32914,10 +33198,14 @@ Durable service:
32914
33198
  codexRecoveryPath(config2),
32915
33199
  "Codex attempt recovery file"
32916
33200
  );
33201
+ const serviceRecoveryPresent = await fileExistsPrivately(
33202
+ `${config2.supervisorStatePath}.service-recovery.json`,
33203
+ "Inference-host service recovery transaction"
33204
+ );
32917
33205
  const revocationCheckpointPresent = await readRevocationCheckpoint(config2) !== null;
32918
33206
  const agentAttemptPresent = await readInferenceAgentAttemptState(config2.statePath) !== null;
32919
33207
  const agentNextRecoveryPresent = await readInferenceAgentNextState(config2.statePath) !== null;
32920
- if (pendingAttempts > 0 || codexRecoveryPresent || revocationCheckpointPresent || agentAttemptPresent || agentNextRecoveryPresent) {
33208
+ if (pendingAttempts > 0 || codexRecoveryPresent || serviceRecoveryPresent || revocationCheckpointPresent || agentAttemptPresent || agentNextRecoveryPresent) {
32921
33209
  throw new Error(
32922
33210
  "Inference host logout refused because attempt recovery is still pending (Codex, agent-driven, or revocation recovery). Rerun agent-next, complete or fail the active attempt, run the automated host to reconcile it, or resume revoke."
32923
33211
  );
@@ -33232,6 +33520,10 @@ Waiting for approval...
33232
33520
  codexRecoveryPath(config2),
33233
33521
  "Codex attempt recovery file"
33234
33522
  );
33523
+ const serviceRecoveryPresent = await fileExistsPrivately(
33524
+ `${config2.supervisorStatePath}.service-recovery.json`,
33525
+ "Inference-host service recovery transaction"
33526
+ );
33235
33527
  const agentAttempt = await readInferenceAgentAttemptState(config2.statePath);
33236
33528
  const agentNextRecoveryPresent = await readInferenceAgentNextState(config2.statePath) !== null;
33237
33529
  return {
@@ -33260,6 +33552,7 @@ Waiting for approval...
33260
33552
  } : null,
33261
33553
  agent_next_recovery_present: agentNextRecoveryPresent,
33262
33554
  codex_recovery_present: recoveryPresent,
33555
+ service_recovery_present: serviceRecoveryPresent,
33263
33556
  codex_auth: codexAuthentication,
33264
33557
  source: "local_only"
33265
33558
  }, parsed.json),
@@ -34016,10 +34309,292 @@ Waiting for approval...
34016
34309
  await lock2.release();
34017
34310
  }
34018
34311
  };
34312
+ recoveryBackupPath = (config2, transactionId) => `${codexRecoveryPath(config2)}.service-recovery-${transactionId}.backup`;
34313
+ serviceRecoveryTransactionPath = (config2) => `${config2.supervisorStatePath}.service-recovery.json`;
34314
+ readServiceRecoveryTransaction = async (config2) => {
34315
+ const raw = await readInferencePrivateFile(
34316
+ serviceRecoveryTransactionPath(config2),
34317
+ "Inference-host service recovery transaction"
34318
+ );
34319
+ if (raw === null) return null;
34320
+ let value;
34321
+ try {
34322
+ value = JSON.parse(raw);
34323
+ } catch {
34324
+ throw new Error("Inference-host service recovery transaction is not valid JSON.");
34325
+ }
34326
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
34327
+ throw new Error("Inference-host service recovery transaction is invalid.");
34328
+ }
34329
+ const record2 = value;
34330
+ const exactKeys = [
34331
+ "schema_version",
34332
+ "phase",
34333
+ "transaction_id",
34334
+ "instance_name",
34335
+ "manifest_generation",
34336
+ "restore_desired_running",
34337
+ "recovery_backup_path",
34338
+ "recovery_backup_sha256",
34339
+ "cleanup_confirmed_attempt_ids",
34340
+ "started_at"
34341
+ ];
34342
+ if (Object.keys(record2).sort().join("\0") !== exactKeys.sort().join("\0") || record2.schema_version !== "vtx_inference_service_recovery_v1" || record2.phase !== "prepared" && record2.phase !== "reconciled" || typeof record2.transaction_id !== "string" || !/^[0-9a-f-]{36}$/u.test(record2.transaction_id) || typeof record2.instance_name !== "string" || typeof record2.manifest_generation !== "string" || typeof record2.restore_desired_running !== "boolean" || typeof record2.recovery_backup_path !== "string" || typeof record2.recovery_backup_sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(record2.recovery_backup_sha256) || !Array.isArray(record2.cleanup_confirmed_attempt_ids) || record2.cleanup_confirmed_attempt_ids.some(
34343
+ (attemptId) => typeof attemptId !== "string" || attemptId.length < 1 || attemptId.length > 256
34344
+ ) || new Set(record2.cleanup_confirmed_attempt_ids).size !== record2.cleanup_confirmed_attempt_ids.length || [...record2.cleanup_confirmed_attempt_ids].sort().join("\0") !== record2.cleanup_confirmed_attempt_ids.join("\0") || typeof record2.started_at !== "string" || !Number.isFinite(Date.parse(record2.started_at)) || new Date(record2.started_at).toISOString() !== record2.started_at) {
34345
+ throw new Error("Inference-host service recovery transaction is invalid.");
34346
+ }
34347
+ if (resolve5(record2.recovery_backup_path) !== resolve5(recoveryBackupPath(config2, record2.transaction_id))) {
34348
+ throw new Error("Inference-host service recovery backup path is invalid.");
34349
+ }
34350
+ return record2;
34351
+ };
34352
+ assertResumableCodexRecoveryEvidence = (backupAttempts, currentAttempts, cleanupConfirmedAttemptIds, phase) => {
34353
+ const backupIds = Object.keys(backupAttempts).sort();
34354
+ const currentIds = Object.keys(currentAttempts).sort();
34355
+ const backupIdSet = new Set(backupIds);
34356
+ const cleanupConfirmedIdSet = new Set(cleanupConfirmedAttemptIds);
34357
+ if (backupIds.length === 0 || currentIds.some((attemptId) => !backupIdSet.has(attemptId)) || cleanupConfirmedAttemptIds.some((attemptId) => !backupIdSet.has(attemptId)) || phase === "reconciled" && backupIds.some((attemptId) => !cleanupConfirmedIdSet.has(attemptId))) {
34358
+ throw new Error("Codex recovery evidence does not match the retained service recovery backup.");
34359
+ }
34360
+ for (const attemptId of backupIds) {
34361
+ const original = backupAttempts[attemptId];
34362
+ const current = currentAttempts[attemptId];
34363
+ if (!current) {
34364
+ if (!cleanupConfirmedIdSet.has(attemptId)) {
34365
+ throw new Error("Codex recovery evidence does not match the retained service recovery backup.");
34366
+ }
34367
+ continue;
34368
+ }
34369
+ if (cleanupConfirmedIdSet.has(attemptId) && !original.cleanupConfirmed && !(current.processState === "terminated" && current.cleanupConfirmed)) {
34370
+ throw new Error("Codex recovery evidence regressed after confirmed cleanup.");
34371
+ }
34372
+ const {
34373
+ processState: _originalProcessState,
34374
+ cleanupConfirmed: _originalCleanupConfirmed,
34375
+ ...originalStable
34376
+ } = original;
34377
+ const {
34378
+ processState: _currentProcessState,
34379
+ cleanupConfirmed: _currentCleanupConfirmed,
34380
+ ...currentStable
34381
+ } = current;
34382
+ if (JSON.stringify(originalStable) !== JSON.stringify(currentStable) || !(current.processState === original.processState && current.cleanupConfirmed === original.cleanupConfirmed) && !(current.processState === "terminated" && current.cleanupConfirmed)) {
34383
+ throw new Error("Codex recovery evidence changed outside an exact cleanup transition.");
34384
+ }
34385
+ }
34386
+ };
34387
+ recoverInstalledCodexService = async (config2, parsed, env, dependencies, manager) => {
34388
+ if (!parsed.instanceFlagExplicit) {
34389
+ throw new Error(
34390
+ "Service recovery requires an explicit --instance NAME; an environment or default instance is not accepted."
34391
+ );
34392
+ }
34393
+ if (!parsed.forceRecovery) {
34394
+ throw new Error(
34395
+ `Service recovery is disruptive. Rerun with --instance ${config2.instanceName} --force-recovery after checking service status and logs.`
34396
+ );
34397
+ }
34398
+ const credentialLock = await acquireInferenceHostProcessLock(
34399
+ credentialLifecycleLockPath(config2)
34400
+ );
34401
+ try {
34402
+ const recoveryStore = new FileCodexAttemptRecoveryStore(codexRecoveryPath(config2));
34403
+ const existingTransaction = await readServiceRecoveryTransaction(config2);
34404
+ if (existingTransaction && existingTransaction.instance_name !== config2.instanceName) {
34405
+ throw new Error(
34406
+ `Service recovery for instance ${existingTransaction.instance_name} must finish before another instance can recover.`
34407
+ );
34408
+ }
34409
+ const initialAttempts = await recoveryStore.loadAll();
34410
+ const pendingAttempts = Object.values(initialAttempts).filter(
34411
+ (checkpoint) => !checkpoint.cleanupConfirmed
34412
+ );
34413
+ if (pendingAttempts.length === 0 && !existingTransaction) {
34414
+ const status = await manager.status();
34415
+ return {
34416
+ exitCode: 0,
34417
+ stdout: render({
34418
+ status: "service_recovery_not_needed",
34419
+ instance_name: config2.instanceName,
34420
+ recovery_needed: false,
34421
+ recovered_attempt_count: 0,
34422
+ outcome_unknown_attempt_count: 0,
34423
+ desired_running: status.desired_running,
34424
+ manager_active: status.manager_active,
34425
+ worker_runtime_state: status.workers.find(
34426
+ (worker) => worker.instance_name === config2.instanceName
34427
+ )?.runtime_state ?? "unknown"
34428
+ }, parsed.json),
34429
+ stderr: ""
34430
+ };
34431
+ }
34432
+ const recoveryRaw = await readInferencePrivateFile(
34433
+ codexRecoveryPath(config2),
34434
+ "Codex attempt recovery file"
34435
+ );
34436
+ if (recoveryRaw === null && existingTransaction?.phase !== "reconciled") {
34437
+ throw new Error("Codex recovery evidence changed before service recovery began.");
34438
+ }
34439
+ const transactionId = existingTransaction?.transaction_id ?? randomUUID2();
34440
+ const backupPath = existingTransaction?.recovery_backup_path ?? recoveryBackupPath(config2, transactionId);
34441
+ if (!existingTransaction) {
34442
+ await writeAtomicInferencePrivateFile(backupPath, recoveryRaw);
34443
+ }
34444
+ const backupRaw = await readInferencePrivateFile(
34445
+ backupPath,
34446
+ "Codex service recovery backup"
34447
+ );
34448
+ const backupSha256 = backupRaw === null ? null : createHash7("sha256").update(backupRaw).digest("hex");
34449
+ if (backupSha256 === null || existingTransaction && backupSha256 !== existingTransaction.recovery_backup_sha256) {
34450
+ throw new Error("Codex service recovery backup is missing or changed.");
34451
+ }
34452
+ const backupAttempts = await new FileCodexAttemptRecoveryStore(backupPath).loadAll();
34453
+ assertResumableCodexRecoveryEvidence(
34454
+ backupAttempts,
34455
+ initialAttempts,
34456
+ existingTransaction?.cleanup_confirmed_attempt_ids ?? [],
34457
+ existingTransaction?.phase ?? "prepared"
34458
+ );
34459
+ const recoverySha256 = recoveryRaw === null ? null : createHash7("sha256").update(recoveryRaw).digest("hex");
34460
+ const unresolvedAttemptIds = Object.keys(backupAttempts).filter((attemptId) => !existingTransaction?.cleanup_confirmed_attempt_ids.includes(attemptId) && !initialAttempts[attemptId]?.cleanupConfirmed);
34461
+ const binary = unresolvedAttemptIds.length > 0 ? await (dependencies.resolveBinary ?? resolvePinnedCodexBinary)(env) : null;
34462
+ let activeTransaction = existingTransaction;
34463
+ let completed = false;
34464
+ try {
34465
+ const recovered = await manager.recoverInstance(config2.instanceName, async ({ manifest }) => {
34466
+ const workerLocks = [];
34467
+ try {
34468
+ for (const worker of [...manifest.workers].sort(
34469
+ (left, right) => left.instance_name.localeCompare(right.instance_name)
34470
+ )) {
34471
+ const workerConfig = resolveInferenceHostConfig({
34472
+ ...env,
34473
+ ...worker.runtime_environment,
34474
+ VTX_INFERENCE_HOST_INSTANCE: worker.instance_name
34475
+ });
34476
+ workerLocks.push(await acquireInferenceHostProcessLock(
34477
+ foregroundHostLockPath(workerConfig)
34478
+ ));
34479
+ workerLocks.push(await acquireInferenceHostProcessLock(workerConfig.processLockPath));
34480
+ }
34481
+ try {
34482
+ if (!activeTransaction) {
34483
+ throw new Error("Inference-host service recovery transaction was not prepared.");
34484
+ }
34485
+ const attempts = await recoveryStore.loadAll();
34486
+ assertResumableCodexRecoveryEvidence(
34487
+ backupAttempts,
34488
+ attempts,
34489
+ activeTransaction.cleanup_confirmed_attempt_ids,
34490
+ activeTransaction.phase
34491
+ );
34492
+ const unresolved = Object.keys(backupAttempts).filter((attemptId) => !activeTransaction.cleanup_confirmed_attempt_ids.includes(attemptId) && !attempts[attemptId]?.cleanupConfirmed);
34493
+ if (unresolved.length === 0) return 0;
34494
+ const currentRaw = await readInferencePrivateFile(
34495
+ codexRecoveryPath(config2),
34496
+ "Codex attempt recovery file"
34497
+ );
34498
+ if (currentRaw === null || recoverySha256 === null || createHash7("sha256").update(currentRaw).digest("hex") !== recoverySha256) {
34499
+ throw new Error("Codex recovery evidence changed while the service was stopping.");
34500
+ }
34501
+ const proof = await (dependencies.proveCodexQuiescence ?? proveCodexSameBootQuiescence)({
34502
+ attempts,
34503
+ confirmNoManagedCodexProcesses: async () => await (dependencies.confirmNoManagedCodexProcesses ?? confirmNoVtxManagedCodexProcesses)({ pinnedBinaryPath: binary.path })
34504
+ });
34505
+ return await (dependencies.reconcileCodexRecovery ?? reconcileCodexAttemptRecovery)({
34506
+ recoveryHooks: recoveryStore,
34507
+ codexHome: config2.codexHomePath,
34508
+ guardianReceiptRoot: codexGuardianReceiptRoot(config2),
34509
+ sameBootQuiescenceProof: proof
34510
+ });
34511
+ } finally {
34512
+ if (!activeTransaction) {
34513
+ throw new Error("Inference-host service recovery transaction was not prepared.");
34514
+ }
34515
+ const progressedAttempts = await recoveryStore.loadAll();
34516
+ assertResumableCodexRecoveryEvidence(
34517
+ backupAttempts,
34518
+ progressedAttempts,
34519
+ activeTransaction.cleanup_confirmed_attempt_ids,
34520
+ activeTransaction.phase
34521
+ );
34522
+ const priorConfirmedIds = new Set(activeTransaction.cleanup_confirmed_attempt_ids);
34523
+ const cleanupConfirmedAttemptIds = Object.keys(backupAttempts).filter((attemptId) => priorConfirmedIds.has(attemptId) || progressedAttempts[attemptId]?.cleanupConfirmed).sort();
34524
+ activeTransaction = {
34525
+ ...activeTransaction,
34526
+ phase: cleanupConfirmedAttemptIds.length === Object.keys(backupAttempts).length ? "reconciled" : "prepared",
34527
+ cleanup_confirmed_attempt_ids: cleanupConfirmedAttemptIds
34528
+ };
34529
+ await writeAtomicInferencePrivateFile(
34530
+ serviceRecoveryTransactionPath(config2),
34531
+ `${JSON.stringify(activeTransaction, null, 2)}
34532
+ `
34533
+ );
34534
+ }
34535
+ } finally {
34536
+ for (const lock2 of workerLocks.reverse()) await lock2.release();
34537
+ }
34538
+ }, {
34539
+ restoreDesiredRunning: existingTransaction?.restore_desired_running,
34540
+ expectedManifestGeneration: existingTransaction?.manifest_generation,
34541
+ prepare: existingTransaction ? void 0 : async ({ manifest, desiredRunning }) => {
34542
+ activeTransaction = {
34543
+ schema_version: "vtx_inference_service_recovery_v1",
34544
+ phase: "prepared",
34545
+ transaction_id: transactionId,
34546
+ instance_name: config2.instanceName,
34547
+ manifest_generation: manifest.generation,
34548
+ restore_desired_running: desiredRunning,
34549
+ recovery_backup_path: backupPath,
34550
+ recovery_backup_sha256: backupSha256,
34551
+ cleanup_confirmed_attempt_ids: [],
34552
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
34553
+ };
34554
+ await writeAtomicInferencePrivateFile(
34555
+ serviceRecoveryTransactionPath(config2),
34556
+ `${JSON.stringify(activeTransaction, null, 2)}
34557
+ `
34558
+ );
34559
+ }
34560
+ });
34561
+ completed = true;
34562
+ const outcomeUnknownAttemptCount = Object.values(backupAttempts).filter(
34563
+ (checkpoint) => checkpoint.dispatchOutcome === "outcome_unknown"
34564
+ ).length;
34565
+ return {
34566
+ exitCode: 0,
34567
+ stdout: render({
34568
+ status: "service_recovered",
34569
+ instance_name: config2.instanceName,
34570
+ recovery_needed: true,
34571
+ recovered_attempt_count: recovered.result,
34572
+ outcome_unknown_attempt_count: outcomeUnknownAttemptCount,
34573
+ desired_running: recovered.status.desired_running,
34574
+ manager_active: recovered.status.manager_active,
34575
+ worker_runtime_state: recovered.status.workers.find(
34576
+ (worker) => worker.instance_name === config2.instanceName
34577
+ )?.runtime_state ?? "unknown"
34578
+ }, parsed.json),
34579
+ stderr: ""
34580
+ };
34581
+ } finally {
34582
+ if (completed) {
34583
+ await clearInferencePrivateFile(
34584
+ serviceRecoveryTransactionPath(config2),
34585
+ "Inference-host service recovery transaction"
34586
+ );
34587
+ await rm5(backupPath, { force: true });
34588
+ }
34589
+ }
34590
+ } finally {
34591
+ await credentialLock.release();
34592
+ }
34593
+ };
34019
34594
  serviceCommand = async (config2, parsed, env, dependencies, warnings) => {
34020
34595
  const action = parsed.serviceAction;
34021
34596
  if (!action) {
34022
- throw new Error("Usage: vtx inference-host service <install|start|stop|status|logs|uninstall>.");
34597
+ throw new Error("Usage: vtx inference-host service <install|start|stop|status|logs|recover|uninstall>.");
34023
34598
  }
34024
34599
  if (action === "run-internal") {
34025
34600
  if (!parsed.serviceManifestPath) throw new Error("Internal service manifest path is required.");
@@ -34046,15 +34621,32 @@ Waiting for approval...
34046
34621
  await serviceLock.release();
34047
34622
  }
34048
34623
  }
34624
+ if (parsed.forceRecovery && action !== "recover") {
34625
+ throw new Error("--force-recovery is accepted only by service recover.");
34626
+ }
34049
34627
  const manager = dependencies.createServiceManager?.(
34050
34628
  config2,
34051
34629
  dependencies.serviceDependencies
34052
34630
  ) ?? new InferenceHostServiceManager(config2, dependencies.serviceDependencies);
34631
+ if (action === "recover") {
34632
+ return await recoverInstalledCodexService(
34633
+ config2,
34634
+ parsed,
34635
+ env,
34636
+ dependencies,
34637
+ manager
34638
+ );
34639
+ }
34053
34640
  if (action === "install") {
34054
34641
  const credentialLock = await acquireInferenceHostProcessLock(
34055
34642
  credentialLifecycleLockPath(config2)
34056
34643
  );
34057
34644
  try {
34645
+ if (await readServiceRecoveryTransaction(config2)) {
34646
+ throw new Error(
34647
+ "A guarded service recovery transaction must finish before installing a subscription."
34648
+ );
34649
+ }
34058
34650
  const adapter = parsed.adapter || "codex";
34059
34651
  if (adapter !== "codex") {
34060
34652
  throw new Error(
@@ -34112,9 +34704,15 @@ Waiting for approval...
34112
34704
  };
34113
34705
  }
34114
34706
  if (action === "start") {
34707
+ if (await readServiceRecoveryTransaction(config2)) {
34708
+ throw new Error("A guarded service recovery transaction must finish before starting the service.");
34709
+ }
34115
34710
  return { exitCode: 0, stdout: render({ status: "service_started", ...await manager.start() }, parsed.json), stderr: "" };
34116
34711
  }
34117
34712
  if (action === "stop") {
34713
+ if (await readServiceRecoveryTransaction(config2)) {
34714
+ throw new Error("A guarded service recovery transaction must finish before stopping the service.");
34715
+ }
34118
34716
  return { exitCode: 0, stdout: render({ status: "service_stopped", ...await manager.stop() }, parsed.json), stderr: "" };
34119
34717
  }
34120
34718
  if (action === "status") {
@@ -34127,6 +34725,11 @@ Waiting for approval...
34127
34725
  credentialLifecycleLockPath(config2)
34128
34726
  );
34129
34727
  try {
34728
+ if (await readServiceRecoveryTransaction(config2)) {
34729
+ throw new Error(
34730
+ "A guarded service recovery transaction must finish before uninstalling a subscription."
34731
+ );
34732
+ }
34130
34733
  if (parsed.instanceExplicit) {
34131
34734
  const retained = await readInferenceHostCredentialContext(config2);
34132
34735
  const retainedIdentity = retained ? resolveInferenceHostConfig({
@@ -34145,7 +34748,7 @@ Waiting for approval...
34145
34748
  await credentialLock.release();
34146
34749
  }
34147
34750
  }
34148
- throw new Error("Usage: vtx inference-host service <install|start|stop|status|logs|uninstall>.");
34751
+ throw new Error("Usage: vtx inference-host service <install|start|stop|status|logs|recover|uninstall>.");
34149
34752
  };
34150
34753
  hasExplicitCredentialStoreConfiguration = (env) => Boolean(
34151
34754
  String(env.VTX_INFERENCE_HOST_CREDENTIAL_STORE || "").trim() || String(env.VTX_INFERENCE_HOST_CREDENTIAL_FILE || "").trim()
@@ -34167,8 +34770,8 @@ Waiting for approval...
34167
34770
  }
34168
34771
  throw new Error("Inference host credential store identity is invalid.");
34169
34772
  };
34170
- commandRequiresRetainedCredentialStore = (parsed) => parsed.command === "logout" || parsed.command === "revoke" || parsed.command === "service" && parsed.instanceExplicit && (parsed.serviceAction === "install" || parsed.serviceAction === "uninstall");
34171
- commandUsesInstalledServiceCredentials = (parsed) => parsed.command === "login" || parsed.command === "logout" || parsed.command === "revoke" || parsed.command === "status" || parsed.command === "doctor" || parsed.command === "service" && parsed.serviceAction === "install" || parsed.command === "service" && parsed.serviceAction === "uninstall" && parsed.instanceExplicit;
34773
+ commandRequiresRetainedCredentialStore = (parsed) => parsed.command === "logout" || parsed.command === "revoke" || parsed.command === "service" && parsed.instanceExplicit && (parsed.serviceAction === "install" || parsed.serviceAction === "recover" || parsed.serviceAction === "uninstall");
34774
+ commandUsesInstalledServiceCredentials = (parsed) => parsed.command === "login" || parsed.command === "logout" || parsed.command === "revoke" || parsed.command === "status" || parsed.command === "doctor" || parsed.command === "service" && parsed.serviceAction === "install" || parsed.command === "service" && parsed.serviceAction === "recover" || parsed.command === "service" && parsed.serviceAction === "uninstall" && parsed.instanceExplicit;
34172
34775
  resolveInferenceHostCommandConfig = async (parsed, env, dependencies) => {
34173
34776
  const selectedEnv = {
34174
34777
  ...env,
@@ -34191,6 +34794,9 @@ Waiting for approval...
34191
34794
  const installedEnvironment = manifest?.workers.find(
34192
34795
  (worker) => worker.instance_name === baseConfig.instanceName
34193
34796
  )?.runtime_environment;
34797
+ if (parsed.command === "service" && parsed.serviceAction === "recover" && !installedEnvironment) {
34798
+ throw new Error(`Inference-host instance ${baseConfig.instanceName} is not installed.`);
34799
+ }
34194
34800
  const persistedContext = installedEnvironment ? null : await readInferenceHostCredentialContext(baseConfig);
34195
34801
  const revocationCheckpoint = !installedEnvironment && !persistedContext && parsed.command === "revoke" ? await readRevocationCheckpoint(baseConfig) : null;
34196
34802
  const retainedEnvironment = installedEnvironment ?? (persistedContext ? {
@@ -38718,7 +39324,7 @@ var init_utils = __esm({
38718
39324
  });
38719
39325
 
38720
39326
  // node_modules/ethers/lib.esm/crypto/crypto.js
38721
- import { createHash as createHash6, createHmac, pbkdf2Sync, randomBytes as randomBytes5 } from "crypto";
39327
+ import { createHash as createHash8, createHmac, pbkdf2Sync, randomBytes as randomBytes5 } from "crypto";
38722
39328
  var init_crypto2 = __esm({
38723
39329
  "node_modules/ethers/lib.esm/crypto/crypto.js"() {
38724
39330
  }
@@ -39932,10 +40538,10 @@ var init_sha22 = __esm({
39932
40538
  init_crypto2();
39933
40539
  init_utils();
39934
40540
  _sha256 = function(data) {
39935
- return createHash6("sha256").update(data).digest();
40541
+ return createHash8("sha256").update(data).digest();
39936
40542
  };
39937
40543
  _sha512 = function(data) {
39938
- return createHash6("sha512").update(data).digest();
40544
+ return createHash8("sha512").update(data).digest();
39939
40545
  };
39940
40546
  __sha256 = _sha256;
39941
40547
  __sha512 = _sha512;
@@ -52112,7 +52718,7 @@ __export(vtx_exports, {
52112
52718
  runVtxCli: () => runVtxCli
52113
52719
  });
52114
52720
  import { randomUUID as randomUUID6 } from "node:crypto";
52115
- import { spawn as spawn7 } from "node:child_process";
52721
+ import { spawn as spawn8 } from "node:child_process";
52116
52722
  function render2(value, json2) {
52117
52723
  if (json2) {
52118
52724
  return `${JSON.stringify(value, null, 2)}
@@ -52142,7 +52748,7 @@ function openBrowser(url2) {
52142
52748
  const platform = process.platform;
52143
52749
  const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
52144
52750
  const args = platform === "win32" ? ["/c", "start", "", url2] : [url2];
52145
- const child = spawn7(command, args, { detached: true, stdio: "ignore" });
52751
+ const child = spawn8(command, args, { detached: true, stdio: "ignore" });
52146
52752
  child.unref();
52147
52753
  }
52148
52754
  function parseFlags(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtxmacro/cli",
3
- "version": "2026.8.39",
3
+ "version": "2026.8.40",
4
4
  "description": "VTX Macro CLI, MCP server, and durable subscription inference host.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",