@bitkyc08/opencodex 2.84.0 → 2.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
  5. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  6. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/package.json +1 -2
  9. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  10. package/src/adapters/anthropic/client-identity.ts +1 -1
  11. package/src/adapters/anthropic/passthrough.ts +33 -4
  12. package/src/adapters/azure.ts +19 -1
  13. package/src/adapters/client-fingerprint.ts +24 -0
  14. package/src/adapters/coding-agent/turn.ts +22 -9
  15. package/src/adapters/cursor/arg-codec.ts +17 -5
  16. package/src/adapters/cursor/catalog.ts +8 -0
  17. package/src/adapters/cursor/effort-map.ts +3 -0
  18. package/src/adapters/cursor/freeform-args.ts +75 -0
  19. package/src/adapters/cursor/protobuf-events.ts +36 -23
  20. package/src/adapters/google-antigravity-wire.ts +22 -1
  21. package/src/adapters/google.ts +2 -2
  22. package/src/adapters/openai-chat/response-events.ts +59 -1
  23. package/src/adapters/openai-chat.ts +8 -5
  24. package/src/adapters/qoder/adapter.ts +40 -1
  25. package/src/advisor/consult.ts +220 -0
  26. package/src/advisor/context.ts +226 -0
  27. package/src/advisor/disclosure.ts +20 -0
  28. package/src/advisor/runtime.ts +305 -0
  29. package/src/advisor/settings.ts +135 -0
  30. package/src/advisor/state.ts +369 -0
  31. package/src/advisor/synthetic-tool.ts +33 -0
  32. package/src/bridge/errors.ts +5 -0
  33. package/src/chat/inbound.ts +30 -6
  34. package/src/chat/outbound.ts +5 -0
  35. package/src/claude/desktop-3p-library.ts +43 -3
  36. package/src/claude/desktop-3p.ts +22 -8
  37. package/src/claude/desktop-picker-profile.ts +11 -4
  38. package/src/claude/desktop-profile.ts +10 -1
  39. package/src/claude/desktop-remote-store-io.ts +5 -3
  40. package/src/claude/inbound.ts +121 -50
  41. package/src/claude/intercept/local-ca.ts +8 -1
  42. package/src/claude/model-default-settings.ts +154 -0
  43. package/src/claude/model-info.ts +7 -7
  44. package/src/claude/outbound.ts +13 -6
  45. package/src/cli/advisor.ts +151 -0
  46. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  47. package/src/cli/capabilities-agents-routing.ts +18 -0
  48. package/src/cli/capabilities-base.ts +1 -1
  49. package/src/cli/chatgpt-command.ts +35 -14
  50. package/src/cli/claude.ts +46 -6
  51. package/src/cli/dispatch.ts +5 -1
  52. package/src/cli/doctor.ts +9 -0
  53. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  54. package/src/cli/export-command.ts +1 -1
  55. package/src/cli/help.ts +2 -1
  56. package/src/cli/index.ts +13 -4
  57. package/src/cli/integration-plan-dto.ts +2 -0
  58. package/src/cli/registry.ts +15 -3
  59. package/src/cli/restart-scope.ts +3 -1
  60. package/src/clients/config-export/contracts.ts +1 -0
  61. package/src/clients/config-export/model-metadata.ts +3 -2
  62. package/src/clients/config-export/qoder.ts +93 -0
  63. package/src/clients/config-export.ts +17 -4
  64. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  65. package/src/codex/auth-context.ts +79 -5
  66. package/src/codex/autostart-health.ts +4 -0
  67. package/src/codex/catalog/build-entries.ts +11 -2
  68. package/src/codex/catalog/configured-tiers.ts +50 -0
  69. package/src/codex/catalog/effort.ts +4 -1
  70. package/src/codex/catalog/model-hints.ts +21 -3
  71. package/src/codex/catalog/native-ultrafast.ts +106 -0
  72. package/src/codex/catalog/parsing.ts +5 -0
  73. package/src/codex/catalog/provider-models.ts +4 -0
  74. package/src/codex/catalog/retained-sync.ts +2 -0
  75. package/src/codex/catalog/routed-gather.ts +22 -4
  76. package/src/codex/catalog/vendor-context.ts +82 -0
  77. package/src/codex/config-write-lock.ts +40 -2
  78. package/src/codex/convergence.ts +2 -0
  79. package/src/codex/desktop-app/types.ts +2 -0
  80. package/src/codex/desktop-app/windows.ts +29 -16
  81. package/src/codex/desktop-app-restart.ts +4 -2
  82. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  83. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  84. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  85. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  86. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  87. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  88. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  89. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  90. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  91. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  92. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  93. package/src/codex/desktop-compatibility/service.ts +30 -0
  94. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  95. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  96. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  97. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  98. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  99. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  100. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  101. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  102. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  103. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  104. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  105. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  106. package/src/codex/history-provider.ts +1 -1
  107. package/src/codex/inject/config-write-section.ts +4 -1
  108. package/src/codex/inject/remove.ts +18 -6
  109. package/src/codex/journal.ts +16 -1
  110. package/src/codex/main-account.ts +99 -15
  111. package/src/codex/native-main-refresh-mode.ts +217 -0
  112. package/src/codex/routing/health-store.ts +1 -1
  113. package/src/codex/routing/pool-recovery.ts +79 -0
  114. package/src/codex/shim-diagnostics.ts +6 -0
  115. package/src/codex/shim-overlay.ts +7 -2
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/live-reconcile.ts +13 -0
  118. package/src/config/load-degrade.ts +6 -1
  119. package/src/config/paths.ts +10 -3
  120. package/src/config/schema/config-schema.ts +17 -0
  121. package/src/config/schema/desktop-compatibility.ts +9 -0
  122. package/src/config/schema/leaf-validators.ts +1 -0
  123. package/src/generated/compatibility-version.json +451 -163
  124. package/src/generated/model-metadata.ts +1 -1
  125. package/src/integrations/catalog-refresh.ts +1 -1
  126. package/src/integrations/config-io.ts +20 -2
  127. package/src/integrations/journal.ts +15 -1
  128. package/src/integrations/mutation-plan.ts +1 -0
  129. package/src/integrations/registry.ts +7 -0
  130. package/src/integrations/store.ts +3 -0
  131. package/src/integrations/writer.ts +10 -1
  132. package/src/lib/advisor-activation.ts +16 -0
  133. package/src/lib/background-work.ts +65 -0
  134. package/src/lib/bounded-file-read.ts +58 -0
  135. package/src/lib/bun-path-runtime.d.mts +10 -1
  136. package/src/lib/bun-path-runtime.mjs +62 -3
  137. package/src/lib/desktop-proxy-route.ts +30 -0
  138. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  139. package/src/lib/errors.ts +7 -0
  140. package/src/lib/local-internal-call-capability.ts +55 -0
  141. package/src/lib/package-tree-integrity.ts +2 -2
  142. package/src/lib/socks5-fetch.ts +48 -71
  143. package/src/lib/socks5-handshake.ts +112 -0
  144. package/src/lib/translator-budget.ts +7 -1
  145. package/src/oauth/generic-account-failover.ts +29 -5
  146. package/src/oauth/index.ts +4 -2
  147. package/src/oauth/kiro-device-login.ts +2 -1
  148. package/src/oauth/kiro.ts +7 -1
  149. package/src/protocols/codecs/chat-ir.ts +467 -0
  150. package/src/protocols/codecs/chat.ts +10 -1
  151. package/src/protocols/codecs/messages-ir.ts +500 -0
  152. package/src/protocols/features.ts +10 -2
  153. package/src/protocols/path.ts +11 -1
  154. package/src/protocols/plan-snapshot.ts +39 -16
  155. package/src/protocols/plan.ts +9 -1
  156. package/src/protocols/settings.ts +8 -0
  157. package/src/providers/antigravity-account-roster.ts +102 -0
  158. package/src/providers/antigravity-models.ts +85 -7
  159. package/src/providers/model-rename-fields.ts +2 -0
  160. package/src/providers/model-rename-migration.ts +5 -3
  161. package/src/providers/registry/entries-core.ts +7 -6
  162. package/src/providers/registry/entries-extended.ts +19 -0
  163. package/src/responses/parser.ts +30 -19
  164. package/src/responses/state.ts +32 -29
  165. package/src/routing/history/build-file.ts +92 -0
  166. package/src/routing/history/indexer.ts +248 -216
  167. package/src/routing/history/ingest.ts +238 -0
  168. package/src/routing/history/rebuild-job.ts +111 -0
  169. package/src/routing/history/rebuild-protocol.ts +15 -0
  170. package/src/routing/history/rebuild-worker.ts +34 -0
  171. package/src/server/admission-model-scope.ts +4 -1
  172. package/src/server/antigravity-direct-eligibility.ts +208 -0
  173. package/src/server/auth-cors.ts +4 -0
  174. package/src/server/chat-completions.ts +75 -9
  175. package/src/server/claude-messages.ts +110 -20
  176. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  177. package/src/server/index/serve-options.ts +2 -2
  178. package/src/server/index/startup-warnings.ts +9 -0
  179. package/src/server/index/websocket-handler.ts +9 -0
  180. package/src/server/index.ts +13 -10
  181. package/src/server/inference/client-encoder-delivery.ts +6 -5
  182. package/src/server/login-shell-path.ts +146 -0
  183. package/src/server/management/advisor-routes.ts +191 -0
  184. package/src/server/management/agent-settings-routes.ts +22 -4
  185. package/src/server/management/companion-routes.ts +4 -0
  186. package/src/server/management/config-routes.ts +2 -2
  187. package/src/server/management/context.ts +4 -0
  188. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  189. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  190. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  191. package/src/server/management/model-routes.ts +2 -2
  192. package/src/server/management/protocol-settings-patch.ts +1 -0
  193. package/src/server/management/route-registry.ts +11 -0
  194. package/src/server/management/sibling-guard.ts +1 -0
  195. package/src/server/management-api.ts +6 -0
  196. package/src/server/messages-antigravity-direct.ts +195 -0
  197. package/src/server/messages-native.ts +5 -1
  198. package/src/server/request-log.ts +7 -0
  199. package/src/server/responses/adapter-continuation.ts +35 -18
  200. package/src/server/responses/adapter-delivery.ts +24 -2
  201. package/src/server/responses/adapter-dispatch.ts +29 -2
  202. package/src/server/responses/advisor-plan-slot.ts +27 -0
  203. package/src/server/responses/advisor-slot.ts +336 -0
  204. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  205. package/src/server/responses/core-combo-failure.ts +10 -1
  206. package/src/server/responses/core-combo.ts +21 -0
  207. package/src/server/responses/core-lifetime.ts +2 -0
  208. package/src/server/responses/core-normalize.ts +11 -0
  209. package/src/server/responses/core-options.ts +46 -0
  210. package/src/server/responses/core-replay.ts +7 -4
  211. package/src/server/responses/core.ts +1 -1
  212. package/src/server/responses/passthrough-delivery.ts +16 -10
  213. package/src/server/responses/passthrough-dispatch.ts +14 -3
  214. package/src/server/responses/request-prepare.ts +68 -5
  215. package/src/server/responses/request-transport.ts +16 -0
  216. package/src/server/responses/run-turn-execution.ts +70 -8
  217. package/src/server/responses/sidecar-execution.ts +78 -3
  218. package/src/server/responses/terminal-guard.ts +2 -1
  219. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  220. package/src/server/startup-action-control.ts +16 -2
  221. package/src/server/startup-health-cache.ts +1 -0
  222. package/src/service/desktop-startup.ts +14 -0
  223. package/src/service/health-sentinel-main.ts +23 -4
  224. package/src/service/health-sentinel-probe.ts +24 -7
  225. package/src/service/logging.ts +72 -0
  226. package/src/tray/windows-tray.ps1 +43 -24
  227. package/src/types/config.ts +63 -6
  228. package/src/types/provider.ts +7 -0
  229. package/src/types/request.ts +15 -0
  230. package/src/types/tools.ts +2 -0
  231. package/src/update/async-check.ts +1 -1
  232. package/src/update/badge.ts +2 -2
  233. package/src/update/index.ts +88 -15
  234. package/src/update/job-supervised-restart.ts +107 -0
  235. package/src/update/job.ts +32 -32
  236. package/src/update/notify.ts +1 -1
  237. package/src/update/refresh-scheduler.ts +2 -2
  238. package/src/update/supervised-recovery.d.mts +27 -0
  239. package/src/update/supervised-recovery.mjs +157 -0
  240. package/src/usage/cache-diagnostic.ts +2 -0
  241. package/src/usage/expected-prices.ts +12 -0
  242. package/src/web-search/gemini-executor.ts +2 -2
  243. package/assets/sponsors/packycode.png +0 -0
  244. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  245. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  246. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -1,3 +1,4 @@
1
+ import type { VendorContextInference } from "./vendor-context";
1
2
  import { isCodexControlPlaneModel } from "../control-plane-models";
2
3
  import { execFileSync } from "node:child_process";
3
4
  import { createHash } from "node:crypto";
@@ -120,6 +121,8 @@ export interface CatalogModel {
120
121
  suppressSyntheticMax?: boolean;
121
122
  defaultReasoningEffort?: string;
122
123
  contextWindow?: number;
124
+ /** Bundled fallback only; never a serving-provider capability assertion. */
125
+ contextWindowInference?: VendorContextInference;
123
126
  maxInputTokens?: number;
124
127
  /** Model-scoped output-token ceiling; omitted when no authoritative value is known. */
125
128
  maxOutputTokens?: number;
@@ -139,6 +142,8 @@ export interface CatalogModel {
139
142
  supportsVerbosity?: boolean;
140
143
  /** Whether this exact routed model has a verified OpenAI-compatible service tier. */
141
144
  supportsServiceTier?: boolean;
145
+ /** Captured eligible service-tier wire explicitly maps ultrafast to ultrafast. */
146
+ supportsUltraFastTier?: boolean;
142
147
  /** Optional provider-specific copy for the advertised Fast tier. */
143
148
  fastTierDescription?: string;
144
149
  supportsReasoningSummaries?: boolean;
@@ -53,6 +53,7 @@ import {
53
53
  import type { FastPolicyAuthority } from "../../providers/fastwire";
54
54
  import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../providers/registry";
55
55
  import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
56
+ import { recordAntigravityAccountRoster } from "../../providers/antigravity-account-roster";
56
57
  import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
57
58
  import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
58
59
  import { effectiveModelAliases } from "../../providers/default-aliases";
@@ -807,6 +808,9 @@ export async function fetchProviderModelsWithAuth(
807
808
  provider: name,
808
809
  cacheGeneration,
809
810
  });
811
+ // Entitlement is per account (Claude 4.6 vs 5.5, Argon): remember what THIS account
812
+ // lists so account selection can prefer one that serves the requested model.
813
+ if (auth.oauthAccountId) recordAntigravityAccountRoster(name, auth.oauthAccountId, antigravity);
810
814
  if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
811
815
  return observed(withConfiguredRetention(configured), "degraded");
812
816
  }
@@ -1,4 +1,5 @@
1
1
  import { projectAntigravitySelectedModels } from "../../providers/antigravity-effort-families";
2
+ import { configuredUltraFastSlugs } from "./configured-tiers";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { join } from "node:path";
4
5
  import { initializeConfigOwnership } from "../../lib/config-ownership";
@@ -509,6 +510,7 @@ function writeRetainedCatalogSync({
509
510
  : [];
510
511
  catalog.models = mergeCatalogEntriesFromObservedState({
511
512
  ultraFastTier: ultraFastTierEnabled(config),
513
+ configuredUltraFastSlugs: configuredUltraFastSlugs(orderedGoModels),
512
514
  modelPickerOrder,
513
515
  accountSelectors,
514
516
  catalogModels: catalogModelsForMerge,
@@ -1,5 +1,7 @@
1
+ import { canInferVendorContext } from "./vendor-context";
1
2
  import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
2
3
  import { initialModelSelectionPending } from "../../providers/initial-model-selection";
4
+ import { supportsConfiguredUltraFast } from "./configured-tiers";
3
5
  import { execFileSync } from "node:child_process";
4
6
  import { createHash, createHmac, randomBytes } from "node:crypto";
5
7
  import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
@@ -318,6 +320,7 @@ function boundCustomNativeReasoning(
318
320
  return bounded;
319
321
  }
320
322
 
323
+ /** Gather discovered and custom rows under one captured provider policy, including speed capabilities. */
321
324
  async function gatherRoutedModelsUncached(
322
325
  config: OcxConfig,
323
326
  capture: GatherFlightCapture,
@@ -358,6 +361,7 @@ async function gatherRoutedModelsUncached(
358
361
  const policy = fastPolicyForModel(apiProvider.provider, model.id, apiProvider.name);
359
362
  const supported = serviceTierSupportFromPolicy(policy);
360
363
  if (supported !== undefined) model.supportsServiceTier = supported;
364
+ model.supportsUltraFastTier = supportsConfiguredUltraFast(policy);
361
365
  if (supported === true && policy.fastTierDescription !== undefined) model.fastTierDescription = policy.fastTierDescription;
362
366
  }
363
367
  }
@@ -467,7 +471,7 @@ async function gatherRoutedModelsUncached(
467
471
  ...inherited,
468
472
  catalogKind: CODEX_CUSTOM_MODEL_CATALOG_KIND,
469
473
  ...(typeof custom.contextWindow === "number" && custom.contextWindow > 0
470
- ? { contextWindow: custom.contextWindow }
474
+ ? { contextWindow: custom.contextWindow, contextWindowInference: undefined }
471
475
  : {}),
472
476
  ...(Array.isArray(custom.inputModalities)
473
477
  ? { inputModalities: [...custom.inputModalities] }
@@ -654,6 +658,7 @@ async function gatherRoutedModelsUncached(
654
658
  ...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}),
655
659
  ...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}),
656
660
  ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
661
+ supportsUltraFastTier: supportsConfiguredUltraFast(fastPolicy),
657
662
  ...(supportsServiceTier === true && fastPolicy?.fastTierDescription !== undefined
658
663
  ? { fastTierDescription: fastPolicy.fastTierDescription }
659
664
  : {}),
@@ -686,7 +691,8 @@ async function gatherRoutedModelsUncached(
686
691
  : undefined;
687
692
  const merged: CatalogModel = replaced ? {
688
693
  ...base,
689
- ...(base.contextWindow === undefined && replaced.contextWindow !== undefined ? { contextWindow: replaced.contextWindow } : {}),
694
+ ...(base.contextWindow === undefined && replaced.contextWindow !== undefined
695
+ ? { contextWindow: replaced.contextWindow, ...(replaced.contextWindowInference ? { contextWindowInference: replaced.contextWindowInference, contextCap: replaced.contextCap, contextCapped: replaced.contextCapped } : {}) } : {}),
690
696
  ...(mergedMaxInput !== undefined ? { maxInputTokens: mergedMaxInput } : {}),
691
697
  ...(mergedMaxOutput !== undefined ? { maxOutputTokens: mergedMaxOutput } : {}),
692
698
  ...(base.autoCompactTokenLimit === undefined && replaced.autoCompactTokenLimit !== undefined
@@ -703,17 +709,29 @@ async function gatherRoutedModelsUncached(
703
709
  ...(base.codexToolMode === undefined && replaced.codexToolMode !== undefined ? { codexToolMode: replaced.codexToolMode } : {}),
704
710
  ...(base.capabilities === undefined && replaced.capabilities !== undefined ? { capabilities: replaced.capabilities } : {}),
705
711
  } : base;
712
+ // A custom-only model may never appear in /models or the provider's configured roster.
713
+ // Fill only its absent context; a full hint overlay would overwrite custom capabilities.
714
+ const customContextHints = merged.contextWindow === undefined && effectiveProvider && canInferVendorContext(cm.provider, effectiveProvider)
715
+ ? applyProviderConfigHints(cm.provider, effectiveProvider, { provider: cm.provider, id: cm.modelId },
716
+ providerContextCap(config, cm.provider), metadataModelIdCaseFoldByProvider.get(cm.provider),
717
+ undefined, metadataConfigDirByProvider.get(cm.provider))
718
+ : undefined;
719
+ const contextResolved: CatalogModel = customContextHints?.contextWindow !== undefined
720
+ ? { ...merged, contextWindow: customContextHints.contextWindow,
721
+ ...(customContextHints.contextWindowInference ? { contextWindowInference: customContextHints.contextWindowInference } : {}),
722
+ ...(customContextHints.contextCap !== undefined ? { contextCap: customContextHints.contextCap, contextCapped: customContextHints.contextCapped } : {}) }
723
+ : merged;
706
724
  // Catalog-advertised efforts are bounded whenever the model id is a pinned native
707
725
  // slug. Desktop validates that id, so a gateway such as YYLJ/gpt-6-astra still cannot
708
726
  // advertise none/minimal. Full native identity stays behind the alias predicate.
709
727
  const nativeEffortSource = hasNativeOpenAiCapabilityMetadata(cm.modelId);
710
728
  const reasoningBounded = nativeEffortSource
711
729
  ? boundCustomNativeReasoning(
712
- merged,
730
+ contextResolved,
713
731
  nativeReasoningEfforts(cm.modelId),
714
732
  nativeAliasDefaultEffort ?? nativeDefaultReasoningEffort(cm.modelId),
715
733
  )
716
- : merged;
734
+ : contextResolved;
717
735
  // Vision-sidecar coverage only: when the enriched provider's shared predicate matches
718
736
  // noVisionModels or text-without-image modelInputModalities, advertise image input so the
719
737
  // Codex app lets images reach the sidecar (#349/#344). Deliberately NOT the full
@@ -0,0 +1,82 @@
1
+ import { listModelMetadata, resolveMetadataProvider, type ModelMetadata } from "../../generated/model-metadata";
2
+ import { PROVIDER_REGISTRY } from "../../providers/registry";
3
+ import type { OcxProviderConfig } from "../../types";
4
+
5
+ /** A bundled model-window estimate, not evidence of the serving gateway's capacity. */
6
+ export interface VendorContextInference {
7
+ source: "vendor" | "aggregator";
8
+ provider: string;
9
+ modelId: string;
10
+ contextWindow: number;
11
+ }
12
+
13
+ // Capacity and price have different authorities: hosting vendors (Cerebras/Bedrock), Azure
14
+ // and the Codex subscription table must not replace the original vendor's API window.
15
+ const VENDORS = ["anthropic", "openai", "google", "moonshot", "minimax", "deepseek", "xai", "zai", "mistral", "xiaomi"];
16
+ const AGGREGATORS = ["openrouter", "opencode-go"];
17
+ // Snapshot names once, never consult the mutable registry after gather flight admission.
18
+ const BUILTIN_NAMES = new Set(PROVIDER_REGISTRY.map(entry => entry.id.toLowerCase()));
19
+ /**
20
+ * Opt-in only (#1073): an id-only custom row keeps the conservative fallback unless the
21
+ * operator sets `inferContextWindow: true`. Built-in names stay excluded even when set.
22
+ */
23
+ export function canInferVendorContext(provider: string, config: Pick<OcxProviderConfig, "inferContextWindow"> | undefined): boolean {
24
+ return config?.inferContextWindow === true
25
+ && !BUILTIN_NAMES.has(provider.toLowerCase()) && !resolveMetadataProvider(provider);
26
+ }
27
+
28
+ const normalize = (id: string): string => id.toLowerCase().replaceAll(".", "-");
29
+ type Candidate = VendorContextInference | null;
30
+
31
+ /** Build a pure lookup; conflicting windows within one authority tier fail closed. */
32
+ export function createVendorContextLookup(
33
+ vendors: readonly ModelMetadata[],
34
+ aggregators: readonly ModelMetadata[],
35
+ ): (id: string) => VendorContextInference | undefined {
36
+ const build = (rows: readonly ModelMetadata[], source: VendorContextInference["source"]) => {
37
+ const exact = new Map<string, Candidate>();
38
+ const normalized = new Map<string, Candidate>();
39
+ const add = (map: Map<string, Candidate>, id: string, value: VendorContextInference) => {
40
+ if (!map.has(id)) map.set(id, value);
41
+ else if (map.get(id)?.contextWindow !== value.contextWindow) map.set(id, null);
42
+ };
43
+ for (const row of rows) {
44
+ const window = row.contextWindow;
45
+ if (!Number.isSafeInteger(window) || window! <= 0 || row.id.startsWith("~")) continue;
46
+ const value: VendorContextInference = { source, provider: row.provider, modelId: row.id, contextWindow: window! };
47
+ const ids = source === "aggregator" ? [row.id, row.id.slice(row.id.lastIndexOf("/") + 1)] : [row.id];
48
+ for (const id of ids) {
49
+ if (!id) continue;
50
+ add(exact, id, value);
51
+ add(normalized, normalize(id), value);
52
+ }
53
+ }
54
+ return { exact, normalized };
55
+ };
56
+ const tiers = [build(vendors, "vendor"), build(aggregators, "aggregator")];
57
+ return rawId => {
58
+ const id = rawId.trim();
59
+ if (!id || id.startsWith("~")) return undefined;
60
+ for (const tier of tiers) {
61
+ // An exact spelling wins over its normalized alias. A conflicting key does not
62
+ // fall through to a lower-authority source and accidentally regain a larger window.
63
+ const map = tier.exact.has(id) ? tier.exact : tier.normalized;
64
+ const key = map === tier.exact ? id : normalize(id);
65
+ if (map.has(key)) {
66
+ const value = map.get(key);
67
+ return value ? { ...value } : undefined;
68
+ }
69
+ }
70
+ return undefined;
71
+ };
72
+ }
73
+
74
+ let lookup: ReturnType<typeof createVendorContextLookup> | undefined;
75
+ export function vendorContextInference(modelId: string): VendorContextInference | undefined {
76
+ lookup ??= createVendorContextLookup(VENDORS.flatMap(listModelMetadata), AGGREGATORS.flatMap(listModelMetadata));
77
+ return lookup(modelId);
78
+ }
79
+
80
+ export function vendorContextWindow(modelId: string): number | undefined {
81
+ return vendorContextInference(modelId)?.contextWindow;
82
+ }
@@ -166,15 +166,21 @@ function canonicalFileIdentity(canonical: string): string | null {
166
166
  /** Capture before the first write; nested native children share the same recovery scope. */
167
167
  export function withConfigWriteRecovery<T>(
168
168
  configPath: string, held: LockHandle, run: () => T,
169
- options: { refreshBeforeSpawn?: boolean; preimage?: Buffer } = {},
169
+ options: { refreshBeforeSpawn?: boolean; preimage?: Buffer; requireMatchingPreimage?: boolean } = {},
170
170
  ): T {
171
- if (recoveries.has(held)) return run();
171
+ const nested = recoveries.get(held);
172
+ if (nested && !options.requireMatchingPreimage) return run();
172
173
  let canonical: string;
173
174
  try { canonical = assertConfigWriteDestination(configPath, held); }
174
175
  catch { throw new ConfigWriteDestinationChanged(); }
175
176
  const identity = canonicalFileIdentity(canonical);
176
177
  const preimage = identity === null ? null : readFileSync(canonical);
177
178
  if (canonicalFileIdentity(canonical) !== identity) throw new ConfigWriteDestinationChanged();
179
+ if (options.requireMatchingPreimage && (!options.preimage || !preimage?.equals(options.preimage))) {
180
+ throw new ConfigWriteDestinationChanged();
181
+ }
182
+ if (nested && (nested.canonical !== canonical || nested.identity !== identity)) throw new ConfigWriteDestinationChanged();
183
+ if (nested) return run();
178
184
  const recovery: ConfigWriteRecovery = {
179
185
  canonical, identity, preimage: options.preimage ?? preimage,
180
186
  refreshBeforeSpawn: options.refreshBeforeSpawn === true && options.preimage === undefined,
@@ -317,6 +323,38 @@ export function publishConfigWriteTarget<T>(
317
323
  });
318
324
  }
319
325
 
326
+ export function removeConfigWriteEntry(configPath: string, held: LockHandle, path: string): void {
327
+ assertConfigWriteDestination(configPath, held);
328
+ watchConfigWriteTargets(held, [path]);
329
+ const watched = targets.get(held)!;
330
+ const expected = watched.get(path)!;
331
+ const current = destination(path);
332
+ if (expected.canonical !== current.canonical || expected.inode !== current.inode
333
+ || current.canonical !== join(realpathSync.native(dirname(resolve(path))), basename(resolve(path)))) {
334
+ throw new Error("Codex artifact destination changed; files and recovery evidence were preserved.");
335
+ }
336
+ if (expected.inode === null) return;
337
+
338
+ const entry = join(realpathSync.native(dirname(resolve(path))), basename(resolve(path)));
339
+ const identity = (file: { dev: bigint; ino: bigint }) => `${file.dev}:${file.ino}`;
340
+ const before = lstatSync(entry, { bigint: true });
341
+ if (before.isSymbolicLink() || !before.isFile() || identity(before) !== expected.inode) {
342
+ throw new Error("Codex artifact destination changed; files and recovery evidence were preserved.");
343
+ }
344
+ assertConfigWriteDestination(configPath, held);
345
+ const now = lstatSync(entry, { bigint: true });
346
+ if (now.isSymbolicLink() || !now.isFile() || identity(now) !== expected.inode) {
347
+ throw new Error("Codex artifact destination changed; files and recovery evidence were preserved.");
348
+ }
349
+ unlinkSync(entry);
350
+ assertConfigWriteDestination(configPath, held);
351
+ const after = destination(path);
352
+ if (after.inode !== null || after.canonical !== expected.canonical) {
353
+ throw new Error("Codex artifact destination changed; recovery evidence was preserved.");
354
+ }
355
+ watched.set(path, after);
356
+ }
357
+
320
358
  export function configWriteLockFailureMessage(failure: Exclude<ConfigWriteAcquireResult, { ok: true }>): string {
321
359
  if (failure.error === "missing-parent") return `Codex configuration parent is missing or unresolved at ${failure.detail}; no files were changed.`;
322
360
  return failure.error === "unsafe"
@@ -1,4 +1,5 @@
1
1
  import { projectAntigravitySelectedModels } from "../providers/antigravity-effort-families";
2
+ import { configuredUltraFastSlugs } from "./catalog/configured-tiers";
2
3
  import { join } from "node:path";
3
4
  import { sameCatalogHealPath } from "./catalog/heal-observation";
4
5
 
@@ -371,6 +372,7 @@ function prepareCatalog(
371
372
  );
372
373
  const mergedModels = mergeCatalogEntriesFromObservedState({
373
374
  ultraFastTier: ultraFastTierEnabled(config),
375
+ configuredUltraFastSlugs: configuredUltraFastSlugs(ordered),
374
376
  modelPickerOrder,
375
377
  accountSelectors,
376
378
  catalogModels,
@@ -51,6 +51,8 @@ export interface DesktopProcess {
51
51
  createdAt: string;
52
52
  /** Absolute executable path, used for membership and the shell predicate. */
53
53
  executable: string;
54
+ /** Windows-only private launch context; never included in restart results or logs. */
55
+ commandLine?: string;
54
56
  }
55
57
 
56
58
  export interface DesktopAppAdapter {
@@ -13,6 +13,7 @@
13
13
  import { execFileSync } from "node:child_process";
14
14
  import { sep, win32 } from "node:path";
15
15
  import { resolveTrustedWindowsPowerShellExe, resolveTrustedWindowsTaskkillExe } from "../../lib/windows-elevation";
16
+ import { activateWindowsCodexCompatibility, assertWindowsCompatibilityContext, captureWindowsCompatibilityContext } from "../desktop-compatibility/windows-package-command";
16
17
  import {
17
18
  isUnderRoot,
18
19
  type DesktopAppAdapter,
@@ -50,8 +51,7 @@ function isMemberExecutable(executable: string, root: string): boolean {
50
51
  * changes between builds, so a literal AUMID would silently stop matching and
51
52
  * then either do nothing or — worse — match a package we did not mean.
52
53
  */
53
- function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
54
- const script = [
54
+ export const WINDOWS_PACKAGE_DISCOVERY_SCRIPT = [
55
55
  "$ErrorActionPreference='SilentlyContinue'",
56
56
  "Import-Module Appx -ErrorAction SilentlyContinue",
57
57
  "$p = Get-AppxPackage -Name OpenAI.Codex",
@@ -60,12 +60,8 @@ function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
60
60
  " $p.PackageFamilyName; $p.InstallLocation; \"$($p.PackageFamilyName)!App\"",
61
61
  "}",
62
62
  ].join("; ");
63
- let stdout: string;
64
- try {
65
- stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], POWERSHELL_PROBE_OPTIONS);
66
- } catch {
67
- return null;
68
- }
63
+
64
+ export function parseWindowsDesktopPackage(stdout: string): DesktopAppInstall | null {
69
65
  const lines = stdout.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
70
66
  if (lines.length < 3 || lines[0] === "MISS") return null;
71
67
  const [family, installLocation, aumid] = lines;
@@ -73,6 +69,16 @@ function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
73
69
  return { id: family, root: installLocation, relaunch: aumid };
74
70
  }
75
71
 
72
+ function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
73
+ let stdout: string;
74
+ try {
75
+ stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_PACKAGE_DISCOVERY_SCRIPT], POWERSHELL_PROBE_OPTIONS);
76
+ } catch {
77
+ return null;
78
+ }
79
+ return parseWindowsDesktopPackage(stdout);
80
+ }
81
+
76
82
  /**
77
83
  * Only `ChatGPT.exe` processes whose image lives under the discovered install
78
84
  * location AND owned by the current user. The install location alone is not
@@ -100,7 +106,8 @@ function listPackageProcesses(exec: DesktopExec, install: DesktopAppInstall): De
100
106
  " if ($o -and $o.ReturnValue -eq 0 -and $o.User) {",
101
107
  " $owner = if ($o.Domain) { \"$($o.Domain)\\$($o.User)\" } else { $o.User }",
102
108
  " if ($owner -ieq $me) {",
103
- " \"$($_.ProcessId) $($_.ParentProcessId) $($_.CreationDate.ToString('o')) $($_.ExecutablePath)\"",
109
+ " $encoded=[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$_.CommandLine))",
110
+ " \"$($_.ProcessId) $($_.ParentProcessId) $($_.CreationDate.ToString('o')) $($_.ExecutablePath)`t$encoded\"",
104
111
  " }",
105
112
  " }",
106
113
  " }",
@@ -126,7 +133,9 @@ function listPackageProcesses(exec: DesktopExec, install: DesktopAppInstall): De
126
133
  }
127
134
 
128
135
  function parseProcessLine(line: string, root: string): DesktopProcess | null {
129
- const match = /^\s*(\d+)\s+(\d+)\s+(\S+)(?:\s+(.+))?$/.exec(line);
136
+ const [listing, encoded, ...extra] = line.split("\t");
137
+ if (extra.length || (encoded && (encoded.length > 65_536 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)))) return null;
138
+ const match = /^\s*(\d+)\s+(\d+)\s+(\S+)(?:\s+(.+))?$/.exec(listing ?? "");
130
139
  if (!match) return null;
131
140
  const pid = Number(match[1]);
132
141
  const parentPid = Number(match[2]);
@@ -143,7 +152,8 @@ function parseProcessLine(line: string, root: string): DesktopProcess | null {
143
152
  // Authoritative membership. PowerShell StartsWith already cheap-filtered, but
144
153
  // that test is a string prefix and is how a sibling install would sneak in.
145
154
  if (!isMemberExecutable(executable, root)) return null;
146
- return { pid, parentPid, createdAt, executable };
155
+ return { pid, parentPid, createdAt, executable,
156
+ ...(encoded !== undefined ? { commandLine: Buffer.from(encoded, "base64").toString("utf8") } : {}) };
147
157
  }
148
158
 
149
159
  /**
@@ -215,13 +225,16 @@ export const windowsDesktopAppAdapter: DesktopAppAdapter = {
215
225
  exec(resolveTrustedWindowsTaskkillExe(), ["/PID", String(root.pid), "/T", "/F"], POWERSHELL_PROBE_OPTIONS);
216
226
  },
217
227
 
218
- captureRelaunchContext(): Record<string, string> {
219
- // The session is supplied by the shell:AppsFolder launch, so nothing needs
220
- // carrying forward.
221
- return {};
228
+ captureRelaunchContext(_exec, _install, processes): Record<string, string> {
229
+ return captureWindowsCompatibilityContext(processes);
222
230
  },
223
231
 
224
- relaunch(exec, install): void {
232
+ relaunch(exec, install, context): void {
233
+ if (context.codexCompatibilityPacUrl) {
234
+ assertWindowsCompatibilityContext(context);
235
+ activateWindowsCodexCompatibility(exec, install, context.codexCompatibilityPacUrl);
236
+ return;
237
+ }
225
238
  // Throws on failure so the ladder reports relaunch_failed. The old code
226
239
  // returned targets_survived here, which was dishonest: everything HAD died
227
240
  // and it was the relaunch that failed.
@@ -89,6 +89,7 @@ export type DesktopAppRestartReason =
89
89
  | "self_ancestry"
90
90
  | "restart_in_flight"
91
91
  | "handoff_started"
92
+ | "relaunch_context_failed"
92
93
  | "targets_survived"
93
94
  | "relaunch_failed";
94
95
 
@@ -263,7 +264,9 @@ export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopApp
263
264
  // graphical session variables, and after termination there is nothing to read them
264
265
  // from. Ordering this wrongly works on macOS and Windows and produces a Linux app
265
266
  // that cannot reach the compositor.
266
- const context = adapter.captureRelaunchContext(exec, install, processes);
267
+ let context: Record<string, string>;
268
+ try { context = adapter.captureRelaunchContext(exec, install, processes); }
269
+ catch { return skipped("relaunch_context_failed"); }
267
270
 
268
271
  const isAlive = io.isAlive ?? defaultIsAlive;
269
272
  const sleep = io.sleep ?? defaultSleep;
@@ -345,4 +348,3 @@ export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopApp
345
348
  if (!handedOff) releaseDesktopRestartLock(io.lock);
346
349
  }
347
350
  }
348
-
@@ -0,0 +1,99 @@
1
+ import { join } from "node:path";
2
+ import { getConfigDir } from "../../config/paths";
3
+ import { windowsDefaultExec, windowsDesktopAppAdapter } from "../desktop-app/windows";
4
+ import { ensureDesktopCompatibilityAuthority, inspectDesktopCompatibilityAuthority, loadDesktopCompatibilityAuthority, renewDesktopCompatibilityAuthority, type DesktopAuthorityInspection, type StoredDesktopAuthority } from "./certificate-store";
5
+ import { createWindowsCertificateTrust, inspectWindowsCertificateTrust, type DesktopCertificateTrust } from "./windows-certificate-trust";
6
+ import { acquireDesktopCertificateMutation, desktopCompatibilityRuntimeActive } from "./runtime-ownership";
7
+
8
+ export interface DesktopCertificateStatus {
9
+ supported: boolean;
10
+ state: "missing" | "invalid" | "expired" | "renewal-required" | "prepared" | "trusted" | "unknown";
11
+ fingerprint?: string;
12
+ expiresAt?: number;
13
+ renewalDue?: boolean;
14
+ trust?: DesktopCertificateTrust;
15
+ busy: "prepare" | "trust" | "remove-trust" | "renew" | null;
16
+ }
17
+
18
+ export interface DesktopCertificateServiceIo {
19
+ platform?: string;
20
+ inspect?: () => DesktopAuthorityInspection;
21
+ prepare?: () => Promise<StoredDesktopAuthority>;
22
+ load?: (allowExpiredForRemoval: boolean) => Promise<StoredDesktopAuthority>;
23
+ renew?: (fingerprint: string) => Promise<StoredDesktopAuthority>;
24
+ readTrust?: (authority: Extract<DesktopAuthorityInspection, { fingerprint: string }>) => Promise<DesktopCertificateTrust>;
25
+ changeTrust?: (authority: StoredDesktopAuthority, action: "trust" | "remove") => Promise<DesktopCertificateTrust>;
26
+ appRunning?: () => Promise<boolean | null>;
27
+ }
28
+
29
+ export class DesktopCertificateServiceError extends Error {
30
+ constructor(readonly code: "unsupported" | "busy" | "not_prepared" | "fingerprint_changed" | "app_running" | "runtime_running" | "app_state_unknown" | "trust_unknown" | "trust_not_applied") {
31
+ super(`desktop_compatibility_${code}`); this.name = "DesktopCertificateServiceError";
32
+ }
33
+ }
34
+
35
+ /** This service owns setup only: no proxy listeners, app restart, account or quota writes. */
36
+ export function createDesktopCertificateService(directory = join(getConfigDir(), "codex-desktop-compatibility"), io: DesktopCertificateServiceIo = {}) {
37
+ const platform = io.platform ?? process.platform;
38
+ const inspect = io.inspect ?? (() => inspectDesktopCompatibilityAuthority(directory));
39
+ const prepare = io.prepare ?? (() => ensureDesktopCompatibilityAuthority({ directory }));
40
+ const load = io.load ?? (allowExpired => loadDesktopCompatibilityAuthority({ directory }, allowExpired));
41
+ const renew = io.renew ?? (fingerprint => renewDesktopCompatibilityAuthority({ directory }, fingerprint));
42
+ const readTrust = io.readTrust ?? (value => inspectWindowsCertificateTrust(value.certPem, value.fingerprint));
43
+ const changeTrust = io.changeTrust ?? ((authority, action) => createWindowsCertificateTrust(authority, authority.fingerprint)[action]());
44
+ const appRunning = io.appRunning ?? (async () => {
45
+ const install = windowsDesktopAppAdapter.discover(windowsDefaultExec);
46
+ if (!install) return null; // Unknown installation is not proof that no app uses the authority.
47
+ const processes = windowsDesktopAppAdapter.listProcesses(windowsDefaultExec, install);
48
+ return processes === null ? null : processes.length > 0;
49
+ });
50
+ let busy: DesktopCertificateStatus["busy"] = null;
51
+ async function status(): Promise<DesktopCertificateStatus> {
52
+ if (platform !== "win32") return { supported: false, state: "missing", busy };
53
+ const stored = inspect();
54
+ if (!("fingerprint" in stored)) return { supported: true, state: stored.status, busy };
55
+ const trusted = await readTrust(stored).catch(() => "unknown" as const);
56
+ return { supported: true, state: stored.status === "expired" ? "expired"
57
+ : stored.status === "renewal-required" ? "renewal-required"
58
+ : trusted === "trusted" ? "trusted" : trusted === "not-trusted" ? "prepared" : "unknown",
59
+ fingerprint: stored.fingerprint, expiresAt: stored.expiresAt, renewalDue: stored.renewalDue, trust: trusted, busy };
60
+ }
61
+ async function action(kind: NonNullable<DesktopCertificateStatus["busy"]>, expectedFingerprint?: string): Promise<DesktopCertificateStatus> {
62
+ if (platform !== "win32") throw new DesktopCertificateServiceError("unsupported");
63
+ if (busy !== null) throw new DesktopCertificateServiceError("busy");
64
+ if (desktopCompatibilityRuntimeActive()) throw new DesktopCertificateServiceError("runtime_running");
65
+ let release: () => void;
66
+ try { release = acquireDesktopCertificateMutation(); } catch { throw new DesktopCertificateServiceError("busy"); }
67
+ busy = kind;
68
+ try {
69
+ if (kind !== "prepare") {
70
+ const before = inspect();
71
+ if (!("fingerprint" in before)) throw new DesktopCertificateServiceError("not_prepared");
72
+ if (before.fingerprint !== expectedFingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
73
+ if (kind === "remove-trust" || kind === "renew") {
74
+ const running = await appRunning();
75
+ if (running === null) throw new DesktopCertificateServiceError("app_state_unknown");
76
+ if (running) throw new DesktopCertificateServiceError("app_running");
77
+ }
78
+ }
79
+ const authority = kind === "prepare" ? await prepare() : await load(kind !== "trust");
80
+ if (kind !== "prepare") {
81
+ if (authority.fingerprint !== expectedFingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
82
+ const current = inspect();
83
+ if (!("fingerprint" in current) || current.fingerprint !== authority.fingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
84
+ const result = await changeTrust(authority, kind === "trust" ? "trust" : "remove");
85
+ if (result === "unknown") throw new DesktopCertificateServiceError("trust_unknown");
86
+ if (result !== (kind === "trust" ? "trusted" : "not-trusted")) throw new DesktopCertificateServiceError("trust_not_applied");
87
+ // Never discard the only removable old identity until OS trust removal is proven.
88
+ // Publication failure leaves its protected envelope intact for an explicit retry.
89
+ if (kind === "renew") await renew(authority.fingerprint);
90
+ }
91
+ busy = null;
92
+ return status();
93
+ } finally { busy = null; release(); }
94
+ }
95
+ return { status, prepare: () => action("prepare"), trust: (fingerprint: string) => action("trust", fingerprint),
96
+ removeTrust: (fingerprint: string) => action("remove-trust", fingerprint), renew: (fingerprint: string) => action("renew", fingerprint) };
97
+ }
98
+
99
+ export type DesktopCertificateService = ReturnType<typeof createDesktopCertificateService>;