@vtxmacro/cli 2026.8.38 → 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 +30 -10
  2. package/bin/vtx.js +688 -45
  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
 
@@ -58,10 +63,10 @@ exposed. Before starting a Trader, confirm that the VTX AI page shows the
58
63
  intended authenticated ChatGPT email and plan. The host reads the live Codex
59
64
  account window: a reached limit pauses new dispatch until its reported reset,
60
65
  while a transient throttle uses a short bounded cooldown.
61
- The selected AI model row shows the open VTX profile's rolling 1-hour, 24-hour,
62
- and 7-day request and reported-token activity for that subscription path.
63
- Account-wide quota percentages remain in safe host diagnostics instead of the
64
- profile-scoped selector.
66
+ The selected AI model row estimates the open VTX profile's attributable
67
+ membership burn rate for that subscription, expressed as equivalent percentage
68
+ points per hour, day, and seven days. Account-wide quota percentages remain in
69
+ safe host diagnostics instead of the profile-scoped selector.
65
70
 
66
71
  ```bash
67
72
  vtx inference-host login
@@ -86,12 +91,27 @@ 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
 
89
- For one VTX account and adapter, the selected host is tried first and other
90
- compatible memberships follow in setup order. Quota/credits exhaustion,
91
- unusable authentication, and exhausted recoverable provider failures can
92
- advance the same logical call. VTX never changes provider, model, effort, or
93
- response mode, and never cascades a bad request, policy rejection, or uncertain
94
- dispatch outcome.
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
+
109
+ For each VTX profile and lane, **Subscription #1** is tried first and later
110
+ compatible subscriptions follow in the saved Account cascade order.
111
+ Quota/credits exhaustion, unusable authentication, and exhausted recoverable
112
+ provider failures can advance the same logical call. VTX never changes
113
+ provider, model, effort, or response mode, and never cascades a bad request,
114
+ policy rejection, or uncertain dispatch outcome.
95
115
 
96
116
  Automated hosts do not impose a subscription-specific profile-count or
97
117
  concurrency limit by default. Set an explicit positive integer with
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.38",
41
+ package_version: "2026.8.40",
42
42
  codex_package_name: "@openai/codex",
43
43
  codex_version: "0.147.0",
44
44
  platforms: {
@@ -14375,7 +14375,7 @@ var init_zod = __esm({
14375
14375
  });
14376
14376
 
14377
14377
  // lib/external-inference-contract.ts
14378
- var EXTERNAL_INFERENCE_CONTRACT_VERSION, EXTERNAL_INFERENCE_CONTROLLER_SCHEMA_VERSION, EXTERNAL_INFERENCE_ADVERTISEMENT_SCHEMA_VERSION, EXTERNAL_INFERENCE_ENVELOPE_SCHEMA_VERSION, EXTERNAL_INFERENCE_JOB_INPUT_SCHEMA_VERSION, LEGACY_EXTERNAL_INFERENCE_JOB_INPUT_SCHEMA_VERSION, EXTERNAL_INFERENCE_USAGE_SCHEMA_VERSION, EXTERNAL_INFERENCE_INPUT_HASH_DOMAIN, MAX_EXTERNAL_INFERENCE_STATUS_HOSTS, MAX_GENERATION, MAX_SAFE_INTEGER, MAX_PROMPT_BYTES, MAX_IMMUTABLE_JOB_INPUT_BYTES, MAX_CANDIDATE_PLAINTEXT_BYTES, MAX_CIPHERTEXT_CHARS, SHA256_INITIAL_STATE, SHA256_ROUND_CONSTANTS, rotateRight, sha256BytesHex, sha256Utf8Hex, computeImmutableJobInputSha256, identifierSchema, operationIdentifierSchema, sha256HexSchema, generationSchema, zeroBasedGenerationSchema, nonNegativeSafeIntegerSchema, positiveSafeIntegerSchema, sqlIntegerTokenCountSchema, timestampSchema, safeCodeSchema, externalInferenceAdapterIdSchema, reasoningEffortSchema, modelNameSchema, displayNameSchema, authenticatedAccountEmailSchema, protocolVersionSchema, base64UrlAlphabet, isCanonicalBase64Url, base64Url32BytesSchema, base64UrlNonceSchema, base64UrlCiphertextSchema, claimHandleSchema, boundedJsonTextSchema, boundedPromptTextSchema, laneSchema, botModeSchema, executionModeSchema, responseModeSchema, dispatchOutcomeSchema, responseOutcomeSchema, nativeModelControllerSchema, externalAgentModelControllerSchema, externalAgentUnboundControllerSchema, liveModelControllerSchema, snapshotModelControllerSchema, modelIdentityMappingSchema, advertisedModelSchema, hostAdvertisementSchema, envelopeAadSchema, validateEnvelopeAad, commonEnvelopeShape, sealedKeyWrapEnvelopeSchema, sealedInputEnvelopeSchema, sealedCandidateEnvelopeSchema, sealedEnvelopeSchema, screenerCandidateRuntimeBindingSchema, screenerClientLeaseBindingSchema, screenerRuntimeBindingSchema, serverRuntimeBindingSchema, clientRuntimeBindingSchema, runtimeBindingSchema, immutableJobInputSchema, canonicalizeJsonValue, serializeImmutableJobInput, reportedTokenUsageSchema, unavailableTokenUsageSchema, tokenUsageSchema, agentConnectRequestSchema, agentNextRequestSchema, agentHeartbeatRequestSchema, emptyAgentNextResultSchema, claimedAgentNextResultSchema, agentNextResultSchema, agentCompleteRequestSchema, agentFailRequestSchema, requestedModelSelectionSchema, completedModelProvenanceSchema, attemptModelProvenanceSchema, providerWeeklyQuotaReadSchema, hostHeartbeatRequestSchema, jobClaimRequestSchema, emptyJobClaimSchema, claimedJobSchema, jobClaimResultSchema, attemptStartRequestSchema, jobHeartbeatRequestSchema, continueHeartbeatDirectiveSchema, cancelHeartbeatDirectiveSchema, jobHeartbeatResultSchema, adapterOutcomeEvidenceSchema, completionBaseShape, validateCompletion, providerResponseCompletionSchema, decisionCandidateCompletionSchema, jobCompleteRequestSchema, jobFailRequestSchema, jobCancelRequestSchema, jobCancellationReceiptSchema, jobDeliveryRequestSchema, deliveryBaseShape, validateDeliveryCandidate, providerResponseDeliverySchema, decisionCandidateDeliverySchema, jobDeliveryResultSchema, jobConsumptionRequestSchema, jobConsumptionReceiptSchema, jobStateSchema, jobStatusReadSchema, hostStatusReadSchema, controllerBindingStatusReadSchema, queueStatusReadSchema, outcomeStatusReadSchema, externalInferenceVolumeWindowReadSchema, externalInferenceFailureCountReadSchema, externalInferenceHostDiagnosticsReadSchema, externalInferenceProfileActivityReadSchema, externalInferenceStatusReadSchema, nativeResearchEvidenceSourceSchema, externalAgentResearchEvidenceSourceSchema, researchEvidenceSourceSchema, reportedResearchTokenUsageSchema, unavailableResearchTokenUsageSchema, researchTokenUsageSchema, externalInferenceProductLineageBaseShape, traderExternalInferenceProductLineageSchema, decisionChatExternalInferenceProductLineageSchema, screenerExternalInferenceProductLineageSchema, externalInferenceProductLineageSchema, normalProductLinkageSchema, evidenceMaterializationMarkerSchema, deliveredExternalAgentSourceIdentitySchema, deliveredRequestEvidenceSchema, researchEvidenceBaseShape, researchEvidenceBaseSchema, validateExactUtf8, validateResearchEvidence, providerResponseResearchEvidenceSchema, decisionCandidateResearchEvidenceSchema, researchEvidenceSchema;
14378
+ var EXTERNAL_INFERENCE_CONTRACT_VERSION, EXTERNAL_INFERENCE_CONTROLLER_SCHEMA_VERSION, EXTERNAL_INFERENCE_ADVERTISEMENT_SCHEMA_VERSION, EXTERNAL_INFERENCE_ENVELOPE_SCHEMA_VERSION, EXTERNAL_INFERENCE_JOB_INPUT_SCHEMA_VERSION, LEGACY_EXTERNAL_INFERENCE_JOB_INPUT_SCHEMA_VERSION, EXTERNAL_INFERENCE_USAGE_SCHEMA_VERSION, EXTERNAL_INFERENCE_INPUT_HASH_DOMAIN, MAX_EXTERNAL_INFERENCE_STATUS_HOSTS, MAX_GENERATION, MAX_SAFE_INTEGER, MAX_PROMPT_BYTES, MAX_IMMUTABLE_JOB_INPUT_BYTES, MAX_CANDIDATE_PLAINTEXT_BYTES, MAX_CIPHERTEXT_CHARS, SHA256_INITIAL_STATE, SHA256_ROUND_CONSTANTS, rotateRight, sha256BytesHex, sha256Utf8Hex, computeImmutableJobInputSha256, identifierSchema, operationIdentifierSchema, sha256HexSchema, generationSchema, zeroBasedGenerationSchema, nonNegativeSafeIntegerSchema, positiveSafeIntegerSchema, sqlIntegerTokenCountSchema, timestampSchema, safeCodeSchema, externalInferenceAdapterIdSchema, reasoningEffortSchema, modelNameSchema, displayNameSchema, authenticatedAccountEmailSchema, protocolVersionSchema, base64UrlAlphabet, isCanonicalBase64Url, base64Url32BytesSchema, base64UrlNonceSchema, base64UrlCiphertextSchema, claimHandleSchema, boundedJsonTextSchema, boundedPromptTextSchema, laneSchema, botModeSchema, executionModeSchema, responseModeSchema, dispatchOutcomeSchema, responseOutcomeSchema, nativeModelControllerSchema, externalAgentModelControllerSchema, externalAgentUnboundControllerSchema, orderedFallbackHostIdsSchema, externalInferenceFallbackHostIdsSchema, liveModelControllerSchema, snapshotModelControllerSchema, modelIdentityMappingSchema, advertisedModelSchema, hostAdvertisementSchema, envelopeAadSchema, validateEnvelopeAad, commonEnvelopeShape, sealedKeyWrapEnvelopeSchema, sealedInputEnvelopeSchema, sealedCandidateEnvelopeSchema, sealedEnvelopeSchema, screenerCandidateRuntimeBindingSchema, screenerClientLeaseBindingSchema, screenerRuntimeBindingSchema, serverRuntimeBindingSchema, clientRuntimeBindingSchema, runtimeBindingSchema, immutableJobInputSchema, canonicalizeJsonValue, serializeImmutableJobInput, reportedTokenUsageSchema, unavailableTokenUsageSchema, tokenUsageSchema, agentConnectRequestSchema, agentNextRequestSchema, agentHeartbeatRequestSchema, emptyAgentNextResultSchema, claimedAgentNextResultSchema, agentNextResultSchema, agentCompleteRequestSchema, agentFailRequestSchema, requestedModelSelectionSchema, completedModelProvenanceSchema, attemptModelProvenanceSchema, providerWeeklyQuotaReadSchema, hostHeartbeatRequestSchema, jobClaimRequestSchema, emptyJobClaimSchema, claimedJobSchema, jobClaimResultSchema, attemptStartRequestSchema, jobHeartbeatRequestSchema, continueHeartbeatDirectiveSchema, cancelHeartbeatDirectiveSchema, jobHeartbeatResultSchema, adapterOutcomeEvidenceSchema, completionBaseShape, validateCompletion, providerResponseCompletionSchema, decisionCandidateCompletionSchema, jobCompleteRequestSchema, jobFailRequestSchema, jobCancelRequestSchema, jobCancellationReceiptSchema, jobDeliveryRequestSchema, deliveryBaseShape, validateDeliveryCandidate, providerResponseDeliverySchema, decisionCandidateDeliverySchema, jobDeliveryResultSchema, jobConsumptionRequestSchema, jobConsumptionReceiptSchema, jobStateSchema, jobStatusReadSchema, hostStatusReadSchema, controllerBindingStatusReadSchema, queueStatusReadSchema, outcomeStatusReadSchema, externalInferenceVolumeWindowReadSchema, externalInferenceFailureCountReadSchema, externalInferenceHostDiagnosticsReadSchema, externalInferenceProfileActivityReadSchema, externalInferenceStatusReadSchema, nativeResearchEvidenceSourceSchema, externalAgentResearchEvidenceSourceSchema, researchEvidenceSourceSchema, reportedResearchTokenUsageSchema, unavailableResearchTokenUsageSchema, researchTokenUsageSchema, externalInferenceProductLineageBaseShape, traderExternalInferenceProductLineageSchema, decisionChatExternalInferenceProductLineageSchema, screenerExternalInferenceProductLineageSchema, externalInferenceProductLineageSchema, normalProductLinkageSchema, evidenceMaterializationMarkerSchema, deliveredExternalAgentSourceIdentitySchema, deliveredRequestEvidenceSchema, researchEvidenceBaseShape, researchEvidenceBaseSchema, validateExactUtf8, validateResearchEvidence, providerResponseResearchEvidenceSchema, decisionCandidateResearchEvidenceSchema, researchEvidenceSchema;
14379
14379
  var init_external_inference_contract = __esm({
14380
14380
  "lib/external-inference-contract.ts"() {
14381
14381
  "use strict";
@@ -14646,6 +14646,15 @@ var init_external_inference_contract = __esm({
14646
14646
  response_mode_hint: responseModeSchema,
14647
14647
  binding_state: external_exports.literal("rebind_required")
14648
14648
  });
14649
+ orderedFallbackHostIdsSchema = external_exports.array(identifierSchema).max(127).refine(
14650
+ (values) => new Set(values).size === values.length,
14651
+ { message: "fallback host IDs must be unique" }
14652
+ );
14653
+ externalInferenceFallbackHostIdsSchema = external_exports.strictObject({
14654
+ main: orderedFallbackHostIdsSchema,
14655
+ review: orderedFallbackHostIdsSchema,
14656
+ screener: orderedFallbackHostIdsSchema
14657
+ });
14649
14658
  liveModelControllerSchema = external_exports.discriminatedUnion("kind", [
14650
14659
  nativeModelControllerSchema,
14651
14660
  externalAgentModelControllerSchema
@@ -19496,7 +19505,7 @@ async function logoutCodexSubscription(options) {
19496
19505
  await session.close();
19497
19506
  }
19498
19507
  }
19499
- var CODEX_INFERENCE_PERMISSION_PROFILE, CODEX_REASONING_CONTENT_MAX_UTF8_BYTES, CODEX_REASONING_SUMMARY_MAX_UTF8_BYTES, CODEX_ACCOUNT_PLAN_TYPES, CodexAppServerError, objectOrNull, finiteToken, tokenUsageFromBreakdown, usageFromNotification, validPlanType, nonnegativeSafeIntegerOrNull, nonnegativeFiniteNumberOrNull, parseRateLimitWindow, RATE_LIMIT_REACHED_TYPES, DEFAULT_CODEX_QUOTA_COOLDOWN_MS, MAX_CODEX_QUOTA_COOLDOWN_MS, CODEX_TRANSIENT_RATE_LIMIT_COOLDOWN_MS, parseRateLimitSnapshot, codexRateLimitRetryAtMs, codexAccountRateLimitReached, forbiddenMethod, forbiddenTerminalItem, classifyCodexTurnFailure, scrubbedCodexEnvironment, killWindowsProcessTree, appServerArgs, GUARDIAN_SCRIPT, WINDOWS_RECEIPT_REPLACE_ERROR_CODES, replaceCodexGuardianReceiptFile, writeCodexGuardianSpawnIntent, parseGuardianReceipt, readCodexGuardianReceipt, waitForCodexGuardianState, defaultSpawn, CodexAppServerSession;
19508
+ var CODEX_INFERENCE_PERMISSION_PROFILE, CODEX_REASONING_CONTENT_MAX_UTF8_BYTES, CODEX_REASONING_SUMMARY_MAX_UTF8_BYTES, CODEX_ACCOUNT_PLAN_TYPES, CodexAppServerError, objectOrNull, finiteToken, tokenUsageFromBreakdown, usageFromNotification, validPlanType, nonnegativeSafeIntegerOrNull, nonnegativeFiniteNumberOrNull, parseRateLimitWindow, RATE_LIMIT_REACHED_TYPES, DEFAULT_CODEX_QUOTA_COOLDOWN_MS, MAX_CODEX_QUOTA_COOLDOWN_MS, CODEX_TRANSIENT_RATE_LIMIT_COOLDOWN_MS, parseRateLimitSnapshot, codexRateLimitRetryAtMs, codexAccountRateLimitReached, forbiddenMethod, forbiddenTerminalItem, classifyCodexTurnFailure, scrubbedCodexEnvironment, killWindowsProcessTree, appServerArgs, GUARDIAN_SCRIPT, WINDOWS_RECEIPT_REPLACE_ERROR_CODES, MAX_RETIRED_RESPONSE_IDS, LATE_RESPONSE_SAFE_METHODS, replaceCodexGuardianReceiptFile, writeCodexGuardianSpawnIntent, parseGuardianReceipt, readCodexGuardianReceipt, waitForCodexGuardianState, defaultSpawn, CodexAppServerSession;
19500
19509
  var init_codex_app_server = __esm({
19501
19510
  "lib/inference-host/codex-app-server.ts"() {
19502
19511
  "use strict";
@@ -19959,6 +19968,8 @@ child.once('close', async () => {
19959
19968
  "ENOTEMPTY",
19960
19969
  "EPERM"
19961
19970
  ]);
19971
+ MAX_RETIRED_RESPONSE_IDS = 256;
19972
+ LATE_RESPONSE_SAFE_METHODS = /* @__PURE__ */ new Set(["account/rateLimits/read"]);
19962
19973
  replaceCodexGuardianReceiptFile = async (temporaryPath, receiptPath, options = {}) => {
19963
19974
  const platform = options.platform ?? process.platform;
19964
19975
  const renameFile = options.renameFile ?? rename2;
@@ -20081,6 +20092,7 @@ child.once('close', async () => {
20081
20092
  CodexAppServerSession = class _CodexAppServerSession {
20082
20093
  constructor(options) {
20083
20094
  this.pending = /* @__PURE__ */ new Map();
20095
+ this.retiredResponseIds = /* @__PURE__ */ new Map();
20084
20096
  this.notificationListeners = /* @__PURE__ */ new Set();
20085
20097
  this.fatalListeners = /* @__PURE__ */ new Set();
20086
20098
  this.accountLoginCompletions = /* @__PURE__ */ new Map();
@@ -20219,6 +20231,9 @@ child.once('close', async () => {
20219
20231
  if (typeof id2 === "number" && Number.isSafeInteger(id2) && !("method" in message)) {
20220
20232
  const pending = this.pending.get(id2);
20221
20233
  if (!pending) {
20234
+ const retiredMethod = this.retiredResponseIds.get(id2);
20235
+ this.retiredResponseIds.delete(id2);
20236
+ if (retiredMethod && LATE_RESPONSE_SAFE_METHODS.has(retiredMethod)) return;
20222
20237
  this.failTransport("unexpected_response_id");
20223
20238
  return;
20224
20239
  }
@@ -20283,6 +20298,14 @@ child.once('close', async () => {
20283
20298
  this.pending.clear();
20284
20299
  for (const listener of this.fatalListeners) listener(this.fatalError);
20285
20300
  }
20301
+ retireResponseId(id2, method) {
20302
+ this.retiredResponseIds.set(id2, method);
20303
+ while (this.retiredResponseIds.size > MAX_RETIRED_RESPONSE_IDS) {
20304
+ const oldest = this.retiredResponseIds.keys().next().value;
20305
+ if (typeof oldest !== "number") break;
20306
+ this.retiredResponseIds.delete(oldest);
20307
+ }
20308
+ }
20286
20309
  write(message) {
20287
20310
  if (this.closed || this.fatalError || this.process.exitCode !== null) {
20288
20311
  throw this.fatalError ?? new CodexAppServerError({
@@ -20321,6 +20344,7 @@ child.once('close', async () => {
20321
20344
  const pending = { resolve: resolve6, reject, timer: null, abortCleanup: null };
20322
20345
  pending.timer = setTimeout(() => {
20323
20346
  this.pending.delete(id2);
20347
+ this.retireResponseId(id2, method);
20324
20348
  pending.abortCleanup?.();
20325
20349
  reject(new CodexAppServerError({
20326
20350
  message: "Codex app-server request timed out.",
@@ -20332,6 +20356,7 @@ child.once('close', async () => {
20332
20356
  if (options.signal) {
20333
20357
  const onAbort = () => {
20334
20358
  this.pending.delete(id2);
20359
+ this.retireResponseId(id2, method);
20335
20360
  if (pending.timer) clearTimeout(pending.timer);
20336
20361
  pending.abortCleanup?.();
20337
20362
  reject(new CodexAppServerError({
@@ -21448,10 +21473,10 @@ import {
21448
21473
  realpath as realpath3,
21449
21474
  rm as rm3
21450
21475
  } from "node:fs/promises";
21451
- import { randomBytes as randomBytes3 } from "node:crypto";
21476
+ import { createHash as createHash4, randomBytes as randomBytes3 } from "node:crypto";
21452
21477
  import { tmpdir as tmpdir2 } from "node:os";
21453
21478
  import { isAbsolute as isAbsolute3, join as join4, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
21454
- 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;
21455
21480
  var init_codex_adapter = __esm({
21456
21481
  "lib/inference-host/codex-adapter.ts"() {
21457
21482
  "use strict";
@@ -21462,6 +21487,34 @@ var init_codex_adapter = __esm({
21462
21487
  MAX_PROMPT_BYTES2 = 3e5;
21463
21488
  CODEX_MODEL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
21464
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
+ };
21465
21518
  tokenUsageReceiptSchema = external_exports.strictObject({
21466
21519
  inputTokens: external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
21467
21520
  cachedInputTokens: external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
@@ -21934,11 +21987,20 @@ var init_codex_adapter = __esm({
21934
21987
  throw new Error("Codex cross-boot recovery receipt ownership is unconfirmed.");
21935
21988
  }
21936
21989
  } else {
21937
- await confirmGuardianTerminatedForRecovery(
21938
- checkpoint,
21939
- guardian,
21940
- options.deadlineMs ?? 5e3
21941
- );
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
+ }
21942
22004
  }
21943
22005
  assertRecoveryResourceScope(checkpoint);
21944
22006
  await removeRecoveredThread(checkpoint, options.codexHome);
@@ -22224,9 +22286,21 @@ var init_codex_adapter = __esm({
22224
22286
  dispatchOutcome: checkpoint.dispatchOutcome
22225
22287
  });
22226
22288
  }
22227
- const receipt = await readCodexGuardianReceipt(checkpoint.processReceiptPath);
22228
- if (checkpoint.processState === "terminated" || receipt === null || receipt.state === "terminated") {
22229
- await rm3(checkpoint.processReceiptPath, { force: true });
22289
+ let sharedReceiptStillRequired = true;
22290
+ if (recoveryHooks.loadAll) {
22291
+ try {
22292
+ const checkpoints = await recoveryHooks.loadAll();
22293
+ const receiptPath = resolve3(checkpoint.processReceiptPath);
22294
+ sharedReceiptStillRequired = Object.values(checkpoints).some((candidate) => candidate.attemptId !== attemptId && !candidate.cleanupConfirmed && candidate.processReceiptPath !== null && resolve3(candidate.processReceiptPath) === receiptPath);
22295
+ } catch {
22296
+ sharedReceiptStillRequired = true;
22297
+ }
22298
+ }
22299
+ if (!sharedReceiptStillRequired) {
22300
+ const receipt = await readCodexGuardianReceipt(checkpoint.processReceiptPath);
22301
+ if (checkpoint.processState === "terminated" || receipt === null || receipt.state === "terminated") {
22302
+ await rm3(checkpoint.processReceiptPath, { force: true });
22303
+ }
22230
22304
  }
22231
22305
  }
22232
22306
  await recoveryHooks.clear(attemptId);
@@ -22570,11 +22644,147 @@ var init_codex_adapter = __esm({
22570
22644
  }
22571
22645
  });
22572
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
+
22573
22783
  // lib/inference-host/crypto.ts
22574
22784
  import {
22575
22785
  createCipheriv,
22576
22786
  createDecipheriv,
22577
- createHash as createHash4,
22787
+ createHash as createHash5,
22578
22788
  createPrivateKey,
22579
22789
  createPublicKey,
22580
22790
  diffieHellman,
@@ -22613,7 +22823,7 @@ var init_crypto = __esm({
22613
22823
  const aad = envelopeAadSchema.parse(input);
22614
22824
  return Buffer.from(JSON.stringify(canonicalize(aad)), "utf8");
22615
22825
  };
22616
- externalInferenceSha256 = (value) => createHash4("sha256").update(value).digest("hex");
22826
+ externalInferenceSha256 = (value) => createHash5("sha256").update(value).digest("hex");
22617
22827
  decodeCanonicalBase64Url = (value, fieldName, expectedBytes) => {
22618
22828
  if (!/^[A-Za-z0-9_-]+$/u.test(value)) {
22619
22829
  throw new ExternalInferenceEnvelopeError("invalid_encoding", `${fieldName} is invalid.`);
@@ -22712,7 +22922,7 @@ var init_crypto = __esm({
22712
22922
  return Buffer.from(hkdfSync(
22713
22923
  "sha256",
22714
22924
  root,
22715
- createHash4("sha256").update(aadBytes).digest(),
22925
+ createHash5("sha256").update(aadBytes).digest(),
22716
22926
  info,
22717
22927
  32
22718
22928
  ));
@@ -29126,7 +29336,7 @@ var require_ajv = __commonJS({
29126
29336
  });
29127
29337
 
29128
29338
  // lib/inference-host/runner.ts
29129
- import { createHash as createHash5 } from "node:crypto";
29339
+ import { createHash as createHash6 } from "node:crypto";
29130
29340
  function createDefaultInferenceHostRunnerDependencies(options) {
29131
29341
  const fetchImpl = options.fetchImpl ?? fetch;
29132
29342
  return {
@@ -29434,7 +29644,7 @@ var init_runner = __esm({
29434
29644
  attempts
29435
29645
  };
29436
29646
  };
29437
- sha256 = (value) => createHash5("sha256").update(value, "utf8").digest("hex");
29647
+ sha256 = (value) => createHash6("sha256").update(value, "utf8").digest("hex");
29438
29648
  stableOperationId = (kind, parts) => `${kind}_${sha256(JSON.stringify(parts)).slice(0, 48)}`;
29439
29649
  buildAttemptStartRequest = (claim, attemptId, startedAt) => attemptStartRequestSchema.parse({
29440
29650
  schema_version: "external_inference_attempt_start_v1",
@@ -31073,7 +31283,7 @@ var init_runner = __esm({
31073
31283
  });
31074
31284
 
31075
31285
  // lib/inference-host/service.ts
31076
- import { spawn as spawn5 } from "node:child_process";
31286
+ import { spawn as spawn6 } from "node:child_process";
31077
31287
  import { randomUUID } from "node:crypto";
31078
31288
  import { createWriteStream, readFileSync } from "node:fs";
31079
31289
  import { access as access3, chmod as chmod3, mkdir as mkdir3, readFile as readFile5, rm as rm4, writeFile as writeFile2 } from "node:fs/promises";
@@ -31119,7 +31329,7 @@ var init_service = __esm({
31119
31329
  plistEscape = xmlEscape;
31120
31330
  systemdQuote = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`;
31121
31331
  defaultRunCommand = async (command, args) => await new Promise((resolvePromise) => {
31122
- const child = spawn5(command, [...args], {
31332
+ const child = spawn6(command, [...args], {
31123
31333
  windowsHide: true,
31124
31334
  stdio: ["ignore", "pipe", "pipe"]
31125
31335
  });
@@ -31592,8 +31802,10 @@ WantedBy=default.target
31592
31802
  }
31593
31803
  throw new Error("Background service did not reach an active state within 10 seconds.");
31594
31804
  }
31595
- async restoreRunningSupervisor(manifest) {
31596
- 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({
31597
31809
  ...manifest,
31598
31810
  generation: randomUUID(),
31599
31811
  installed_at: this.now().toISOString()
@@ -31621,10 +31833,15 @@ ${result2.stderr}`)) {
31621
31833
  }
31622
31834
  await this.waitForManagerActive();
31623
31835
  if (!this.confirmInitialReadiness) return await this.status();
31624
- 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
+ );
31625
31842
  return await this.status(runtime);
31626
31843
  }
31627
- async waitForManifestApplied(manifest, targetInstanceName, maxAttempts = this.reconcileWaitAttempts) {
31844
+ async waitForManifestApplied(manifest, targetInstanceName, maxAttempts = this.reconcileWaitAttempts, minimumRuntimeUpdatedAtExclusive) {
31628
31845
  let consecutiveReadyObservations = 0;
31629
31846
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
31630
31847
  let runtime = null;
@@ -31635,7 +31852,9 @@ ${result2.stderr}`)) {
31635
31852
  throw error48;
31636
31853
  }
31637
31854
  }
31638
- 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) {
31639
31858
  const configuredNames = manifest.workers.map((worker) => worker.instance_name).sort();
31640
31859
  const runtimeNames = runtime.workers.map((worker) => worker.instance_name).sort();
31641
31860
  const exactWorkerSet = JSON.stringify(configuredNames) === JSON.stringify(runtimeNames);
@@ -31897,7 +32116,71 @@ ${result2.stderr}`)) {
31897
32116
  async stop() {
31898
32117
  return await this.withControlLock(async () => await this.stopUnlocked());
31899
32118
  }
31900
- 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) {
31901
32184
  const manifest = await readInferenceHostServiceManifest(this.manifestPath());
31902
32185
  if (!manifest) {
31903
32186
  throw new Error("Inference-host service is not installed.");
@@ -31939,7 +32222,7 @@ ${result2.stderr}`)) {
31939
32222
  } catch (error48) {
31940
32223
  if (!restoreOnFailure) throw error48;
31941
32224
  try {
31942
- await this.restoreRunningSupervisor(manifest);
32225
+ await this.restoreRunningSupervisor(manifest, preserveManifestGenerationOnFailure);
31943
32226
  } catch (restoreError) {
31944
32227
  throw new Error(
31945
32228
  "Inference-host stop failed and the previous supervisor could not be restored.",
@@ -32106,7 +32389,7 @@ ${result2.stderr}`)) {
32106
32389
  `);
32107
32390
  }
32108
32391
  };
32109
- const child = spawn5(manifest.executable, args, {
32392
+ const child = spawn6(manifest.executable, args, {
32110
32393
  env: inferenceHostServiceChildEnvironment(worker.runtime_environment),
32111
32394
  windowsHide: true,
32112
32395
  stdio: ["pipe", "pipe", "pipe"]
@@ -32396,8 +32679,8 @@ __export(cli_exports, {
32396
32679
  registerInferenceHostServiceControlInput: () => registerInferenceHostServiceControlInput,
32397
32680
  runInferenceHostCli: () => runInferenceHostCli
32398
32681
  });
32399
- import { randomUUID as randomUUID2 } from "node:crypto";
32400
- 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";
32401
32684
  import { lstat as lstat4, realpath as realpath4, rm as rm5 } from "node:fs/promises";
32402
32685
  import { join as join6, resolve as resolve5 } from "node:path";
32403
32686
  async function runInferenceHostCli(argv2, env = process.env, dependencies = {}) {
@@ -32407,6 +32690,21 @@ async function runInferenceHostCli(argv2, env = process.env, dependencies = {})
32407
32690
  return { exitCode: 0, stdout: INFERENCE_HOST_HELP, stderr: "" };
32408
32691
  }
32409
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
+ }
32410
32708
  const config2 = await resolveInferenceHostCommandConfig(parsed, env, dependencies);
32411
32709
  if (parsed.command === "login") {
32412
32710
  return await login(config2, parsed, env, dependencies, warnings);
@@ -32445,7 +32743,18 @@ async function runInferenceHostCli(argv2, env = process.env, dependencies = {})
32445
32743
  );
32446
32744
  }
32447
32745
  if (parsed.command === "service") {
32448
- 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
+ }
32449
32758
  }
32450
32759
  if (parsed.command === "status") {
32451
32760
  return await localStatus(config2, parsed, dependencies, warnings);
@@ -32474,7 +32783,7 @@ async function runInferenceHostCli(argv2, env = process.env, dependencies = {})
32474
32783
  };
32475
32784
  }
32476
32785
  }
32477
- 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;
32478
32787
  var init_cli = __esm({
32479
32788
  "lib/inference-host/cli.ts"() {
32480
32789
  "use strict";
@@ -32483,6 +32792,7 @@ var init_cli = __esm({
32483
32792
  init_agent_client();
32484
32793
  init_agent_state();
32485
32794
  init_codex_adapter();
32795
+ init_codex_recovery_process();
32486
32796
  init_codex_app_server();
32487
32797
  init_codex_binary();
32488
32798
  init_config();
@@ -32499,6 +32809,7 @@ var init_cli = __esm({
32499
32809
  revocationCheckpointPath = (config2) => `${config2.statePath}.revoke.json`;
32500
32810
  foregroundHostLockPath = (config2) => `${config2.processLockPath}.foreground`;
32501
32811
  credentialLifecycleLockPath = (config2) => `${inferenceHostCredentialContextPath(config2)}.lock`;
32812
+ serviceRecoveryCommandLockPath = (config2) => `${config2.supervisorProcessLockPath}.service-recovery-command`;
32502
32813
  AGENT_HEARTBEAT_INTERVAL_MS = 3e3;
32503
32814
  AGENT_HEARTBEAT_MAX_RETRY_DELAY_MS = 9e3;
32504
32815
  retryableAgentHeartbeatError = (error48) => {
@@ -32595,7 +32906,8 @@ Durable service:
32595
32906
  vtx inference-host codex-login --instance codex-2
32596
32907
  vtx inference-host service install --instance codex-2
32597
32908
  vtx inference-host service uninstall --instance codex-2
32598
- 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>
32599
32911
  `;
32600
32912
  parseHostConcurrency = (raw, label) => {
32601
32913
  const value = Number(raw);
@@ -32611,6 +32923,7 @@ Durable service:
32611
32923
  let maxConcurrency = configuredConcurrency ? parseHostConcurrency(configuredConcurrency, "VTX_INFERENCE_HOST_MAX_CONCURRENCY") : null;
32612
32924
  let instanceName = String(env.VTX_INFERENCE_HOST_INSTANCE || "default").trim();
32613
32925
  let instanceExplicit = Boolean(String(env.VTX_INFERENCE_HOST_INSTANCE || "").trim());
32926
+ let instanceFlagExplicit = false;
32614
32927
  let displayName = String(env.VTX_INFERENCE_HOST_DISPLAY_NAME || "").trim() || "Codex subscription host";
32615
32928
  let displayNameExplicit = Boolean(String(env.VTX_INFERENCE_HOST_DISPLAY_NAME || "").trim());
32616
32929
  let adapter = null;
@@ -32620,6 +32933,7 @@ Durable service:
32620
32933
  let waitSeconds = 50;
32621
32934
  let lines = 100;
32622
32935
  let serviceManifestPath = null;
32936
+ let forceRecovery = false;
32623
32937
  const positionals = [];
32624
32938
  for (let index = 0; index < argv2.length; index += 1) {
32625
32939
  const argument = argv2[index];
@@ -32631,6 +32945,10 @@ Durable service:
32631
32945
  once = true;
32632
32946
  continue;
32633
32947
  }
32948
+ if (argument === "--force-recovery") {
32949
+ forceRecovery = true;
32950
+ continue;
32951
+ }
32634
32952
  if (argument === "--max-concurrency") {
32635
32953
  const raw = argv2[index + 1];
32636
32954
  if (!raw) throw new Error("--max-concurrency requires a value.");
@@ -32669,6 +32987,7 @@ Durable service:
32669
32987
  if (argument === "--instance") {
32670
32988
  instanceName = raw.trim();
32671
32989
  instanceExplicit = true;
32990
+ instanceFlagExplicit = true;
32672
32991
  }
32673
32992
  index += 1;
32674
32993
  continue;
@@ -32699,6 +33018,7 @@ Durable service:
32699
33018
  maxConcurrency,
32700
33019
  instanceName,
32701
33020
  instanceExplicit,
33021
+ instanceFlagExplicit,
32702
33022
  displayName,
32703
33023
  adapter,
32704
33024
  modelId,
@@ -32707,13 +33027,14 @@ Durable service:
32707
33027
  waitSeconds,
32708
33028
  lines,
32709
33029
  serviceAction,
32710
- serviceManifestPath
33030
+ serviceManifestPath,
33031
+ forceRecovery
32711
33032
  };
32712
33033
  };
32713
33034
  defaultOpenBrowser = (url2) => {
32714
33035
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
32715
33036
  const args = process.platform === "win32" ? ["/c", "start", "", url2] : [url2];
32716
- const child = spawn6(command, args, {
33037
+ const child = spawn7(command, args, {
32717
33038
  detached: true,
32718
33039
  stdio: "ignore",
32719
33040
  windowsHide: true
@@ -32877,10 +33198,14 @@ Durable service:
32877
33198
  codexRecoveryPath(config2),
32878
33199
  "Codex attempt recovery file"
32879
33200
  );
33201
+ const serviceRecoveryPresent = await fileExistsPrivately(
33202
+ `${config2.supervisorStatePath}.service-recovery.json`,
33203
+ "Inference-host service recovery transaction"
33204
+ );
32880
33205
  const revocationCheckpointPresent = await readRevocationCheckpoint(config2) !== null;
32881
33206
  const agentAttemptPresent = await readInferenceAgentAttemptState(config2.statePath) !== null;
32882
33207
  const agentNextRecoveryPresent = await readInferenceAgentNextState(config2.statePath) !== null;
32883
- if (pendingAttempts > 0 || codexRecoveryPresent || revocationCheckpointPresent || agentAttemptPresent || agentNextRecoveryPresent) {
33208
+ if (pendingAttempts > 0 || codexRecoveryPresent || serviceRecoveryPresent || revocationCheckpointPresent || agentAttemptPresent || agentNextRecoveryPresent) {
32884
33209
  throw new Error(
32885
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."
32886
33211
  );
@@ -33195,6 +33520,10 @@ Waiting for approval...
33195
33520
  codexRecoveryPath(config2),
33196
33521
  "Codex attempt recovery file"
33197
33522
  );
33523
+ const serviceRecoveryPresent = await fileExistsPrivately(
33524
+ `${config2.supervisorStatePath}.service-recovery.json`,
33525
+ "Inference-host service recovery transaction"
33526
+ );
33198
33527
  const agentAttempt = await readInferenceAgentAttemptState(config2.statePath);
33199
33528
  const agentNextRecoveryPresent = await readInferenceAgentNextState(config2.statePath) !== null;
33200
33529
  return {
@@ -33223,6 +33552,7 @@ Waiting for approval...
33223
33552
  } : null,
33224
33553
  agent_next_recovery_present: agentNextRecoveryPresent,
33225
33554
  codex_recovery_present: recoveryPresent,
33555
+ service_recovery_present: serviceRecoveryPresent,
33226
33556
  codex_auth: codexAuthentication,
33227
33557
  source: "local_only"
33228
33558
  }, parsed.json),
@@ -33979,10 +34309,292 @@ Waiting for approval...
33979
34309
  await lock2.release();
33980
34310
  }
33981
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
+ };
33982
34594
  serviceCommand = async (config2, parsed, env, dependencies, warnings) => {
33983
34595
  const action = parsed.serviceAction;
33984
34596
  if (!action) {
33985
- 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>.");
33986
34598
  }
33987
34599
  if (action === "run-internal") {
33988
34600
  if (!parsed.serviceManifestPath) throw new Error("Internal service manifest path is required.");
@@ -34009,15 +34621,32 @@ Waiting for approval...
34009
34621
  await serviceLock.release();
34010
34622
  }
34011
34623
  }
34624
+ if (parsed.forceRecovery && action !== "recover") {
34625
+ throw new Error("--force-recovery is accepted only by service recover.");
34626
+ }
34012
34627
  const manager = dependencies.createServiceManager?.(
34013
34628
  config2,
34014
34629
  dependencies.serviceDependencies
34015
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
+ }
34016
34640
  if (action === "install") {
34017
34641
  const credentialLock = await acquireInferenceHostProcessLock(
34018
34642
  credentialLifecycleLockPath(config2)
34019
34643
  );
34020
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
+ }
34021
34650
  const adapter = parsed.adapter || "codex";
34022
34651
  if (adapter !== "codex") {
34023
34652
  throw new Error(
@@ -34075,9 +34704,15 @@ Waiting for approval...
34075
34704
  };
34076
34705
  }
34077
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
+ }
34078
34710
  return { exitCode: 0, stdout: render({ status: "service_started", ...await manager.start() }, parsed.json), stderr: "" };
34079
34711
  }
34080
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
+ }
34081
34716
  return { exitCode: 0, stdout: render({ status: "service_stopped", ...await manager.stop() }, parsed.json), stderr: "" };
34082
34717
  }
34083
34718
  if (action === "status") {
@@ -34090,6 +34725,11 @@ Waiting for approval...
34090
34725
  credentialLifecycleLockPath(config2)
34091
34726
  );
34092
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
+ }
34093
34733
  if (parsed.instanceExplicit) {
34094
34734
  const retained = await readInferenceHostCredentialContext(config2);
34095
34735
  const retainedIdentity = retained ? resolveInferenceHostConfig({
@@ -34108,7 +34748,7 @@ Waiting for approval...
34108
34748
  await credentialLock.release();
34109
34749
  }
34110
34750
  }
34111
- 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>.");
34112
34752
  };
34113
34753
  hasExplicitCredentialStoreConfiguration = (env) => Boolean(
34114
34754
  String(env.VTX_INFERENCE_HOST_CREDENTIAL_STORE || "").trim() || String(env.VTX_INFERENCE_HOST_CREDENTIAL_FILE || "").trim()
@@ -34130,8 +34770,8 @@ Waiting for approval...
34130
34770
  }
34131
34771
  throw new Error("Inference host credential store identity is invalid.");
34132
34772
  };
34133
- commandRequiresRetainedCredentialStore = (parsed) => parsed.command === "logout" || parsed.command === "revoke" || parsed.command === "service" && parsed.instanceExplicit && (parsed.serviceAction === "install" || parsed.serviceAction === "uninstall");
34134
- 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;
34135
34775
  resolveInferenceHostCommandConfig = async (parsed, env, dependencies) => {
34136
34776
  const selectedEnv = {
34137
34777
  ...env,
@@ -34154,6 +34794,9 @@ Waiting for approval...
34154
34794
  const installedEnvironment = manifest?.workers.find(
34155
34795
  (worker) => worker.instance_name === baseConfig.instanceName
34156
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
+ }
34157
34800
  const persistedContext = installedEnvironment ? null : await readInferenceHostCredentialContext(baseConfig);
34158
34801
  const revocationCheckpoint = !installedEnvironment && !persistedContext && parsed.command === "revoke" ? await readRevocationCheckpoint(baseConfig) : null;
34159
34802
  const retainedEnvironment = installedEnvironment ?? (persistedContext ? {
@@ -38681,7 +39324,7 @@ var init_utils = __esm({
38681
39324
  });
38682
39325
 
38683
39326
  // node_modules/ethers/lib.esm/crypto/crypto.js
38684
- import { createHash as createHash6, createHmac, pbkdf2Sync, randomBytes as randomBytes5 } from "crypto";
39327
+ import { createHash as createHash8, createHmac, pbkdf2Sync, randomBytes as randomBytes5 } from "crypto";
38685
39328
  var init_crypto2 = __esm({
38686
39329
  "node_modules/ethers/lib.esm/crypto/crypto.js"() {
38687
39330
  }
@@ -39895,10 +40538,10 @@ var init_sha22 = __esm({
39895
40538
  init_crypto2();
39896
40539
  init_utils();
39897
40540
  _sha256 = function(data) {
39898
- return createHash6("sha256").update(data).digest();
40541
+ return createHash8("sha256").update(data).digest();
39899
40542
  };
39900
40543
  _sha512 = function(data) {
39901
- return createHash6("sha512").update(data).digest();
40544
+ return createHash8("sha512").update(data).digest();
39902
40545
  };
39903
40546
  __sha256 = _sha256;
39904
40547
  __sha512 = _sha512;
@@ -52075,7 +52718,7 @@ __export(vtx_exports, {
52075
52718
  runVtxCli: () => runVtxCli
52076
52719
  });
52077
52720
  import { randomUUID as randomUUID6 } from "node:crypto";
52078
- import { spawn as spawn7 } from "node:child_process";
52721
+ import { spawn as spawn8 } from "node:child_process";
52079
52722
  function render2(value, json2) {
52080
52723
  if (json2) {
52081
52724
  return `${JSON.stringify(value, null, 2)}
@@ -52105,7 +52748,7 @@ function openBrowser(url2) {
52105
52748
  const platform = process.platform;
52106
52749
  const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
52107
52750
  const args = platform === "win32" ? ["/c", "start", "", url2] : [url2];
52108
- const child = spawn7(command, args, { detached: true, stdio: "ignore" });
52751
+ const child = spawn8(command, args, { detached: true, stdio: "ignore" });
52109
52752
  child.unref();
52110
52753
  }
52111
52754
  function parseFlags(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtxmacro/cli",
3
- "version": "2026.8.38",
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",