@bitkyc08/opencodex 2.81.0 → 2.83.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 (272) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ba8uG696.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/cursor/catalog.ts +2 -2
  11. package/src/adapters/cursor/effort-map.ts +1 -1
  12. package/src/adapters/devin/live-models.ts +2 -2
  13. package/src/adapters/google-truncation.ts +22 -0
  14. package/src/adapters/google.ts +7 -3
  15. package/src/adapters/ollama-native.ts +18 -14
  16. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  17. package/src/adapters/openai-responses/passthrough.ts +11 -4
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  21. package/src/bridge/errors.ts +7 -1
  22. package/src/claude/agents-inject.ts +14 -3
  23. package/src/claude/context-windows.ts +22 -20
  24. package/src/claude/desktop-3p.ts +88 -12
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/desktop-profile.ts +2 -1
  27. package/src/claude/gateway-cache.ts +18 -1
  28. package/src/claude/inbound.ts +77 -3
  29. package/src/claude/intercept/cli-catalog.ts +3 -0
  30. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  31. package/src/claude/intercept/picker-models.ts +14 -6
  32. package/src/claude/intercept/settings.ts +3 -0
  33. package/src/claude/long-context.ts +66 -0
  34. package/src/claude/model-info.ts +19 -16
  35. package/src/claude/outbound.ts +94 -9
  36. package/src/claude/subagent-model.ts +40 -11
  37. package/src/cli/account-auth.ts +20 -12
  38. package/src/cli/agent.ts +9 -5
  39. package/src/cli/aside-profile-recovery.ts +33 -0
  40. package/src/cli/capabilities-base.ts +5 -5
  41. package/src/cli/capabilities-integrations.ts +2 -2
  42. package/src/cli/cli-path-diagnostics.ts +132 -0
  43. package/src/cli/desktop-runtime-guidance.ts +65 -0
  44. package/src/cli/dispatch.ts +5 -0
  45. package/src/cli/doctor.ts +16 -3
  46. package/src/cli/index.ts +14 -9
  47. package/src/cli/integration-aside-sync.ts +2 -1
  48. package/src/cli/integrations.ts +16 -3
  49. package/src/cli/resolve.ts +26 -3
  50. package/src/cli/root.ts +4 -2
  51. package/src/cli/status.ts +64 -12
  52. package/src/cli/update-restart-child.ts +4 -2
  53. package/src/cli/update-restart-home.ts +28 -10
  54. package/src/cli/update-restart-service-record.ts +116 -0
  55. package/src/cli/update-restart-supervision.ts +96 -0
  56. package/src/cli/update-restart.ts +59 -13
  57. package/src/cli/v2.ts +27 -20
  58. package/src/cli/version-skew-notice.ts +77 -0
  59. package/src/clients/config-export/model-metadata.ts +16 -0
  60. package/src/clients/config-export/zcode-store.ts +46 -11
  61. package/src/clients/config-export/zcode.ts +3 -5
  62. package/src/clients/config-export.ts +1 -1
  63. package/src/clients/lazycodex.ts +1 -1
  64. package/src/clients/omo-role-models.ts +32 -7
  65. package/src/codex/auth-api/account-selection.ts +107 -0
  66. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  67. package/src/codex/auth-api/routes.ts +10 -49
  68. package/src/codex/auth-context.ts +28 -4
  69. package/src/codex/autostart-health.ts +12 -3
  70. package/src/codex/catalog/build-entries.ts +18 -0
  71. package/src/codex/catalog/bundled.ts +155 -26
  72. package/src/codex/catalog/combo-member.ts +3 -2
  73. package/src/codex/catalog/effort.ts +26 -5
  74. package/src/codex/catalog/gather-capture.ts +5 -0
  75. package/src/codex/catalog/model-hints.ts +15 -8
  76. package/src/codex/catalog/model-visibility.ts +3 -1
  77. package/src/codex/catalog/native-models.ts +1 -1
  78. package/src/codex/catalog/operator-tiers.ts +38 -0
  79. package/src/codex/catalog/provider-models.ts +68 -35
  80. package/src/codex/catalog/retained-sync.ts +7 -5
  81. package/src/codex/catalog/routed-gather.ts +15 -1
  82. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  83. package/src/codex/catalog-auto-refresh.ts +19 -0
  84. package/src/codex/catalog.ts +1 -1
  85. package/src/codex/codex-home-owner.ts +3 -0
  86. package/src/codex/config-write-lock.ts +333 -0
  87. package/src/codex/convergence.ts +2 -1
  88. package/src/codex/desktop-app/darwin.ts +1 -1
  89. package/src/codex/desktop-app/handoff.ts +1 -1
  90. package/src/codex/desktop-app/linux.ts +1 -1
  91. package/src/codex/desktop-app/lock.ts +1 -1
  92. package/src/codex/desktop-app/types.ts +1 -1
  93. package/src/codex/desktop-app/windows.ts +1 -1
  94. package/src/codex/desktop-app-restart.ts +1 -1
  95. package/src/codex/features.ts +257 -200
  96. package/src/codex/inject/config-write-section.ts +34 -0
  97. package/src/codex/inject/multi-agent-v2.ts +14 -6
  98. package/src/codex/inject/remove.ts +36 -11
  99. package/src/codex/inject/restore.ts +85 -20
  100. package/src/codex/inject-coordination.ts +15 -6
  101. package/src/codex/inject.ts +62 -20
  102. package/src/codex/journal.ts +62 -17
  103. package/src/codex/main-account-cache.ts +43 -0
  104. package/src/codex/main-account.ts +6 -7
  105. package/src/codex/paths.ts +2 -0
  106. package/src/codex/prepared-home.ts +18 -0
  107. package/src/codex/prompt-journal.ts +25 -12
  108. package/src/codex/prompt-layers.ts +49 -26
  109. package/src/codex/prompt-lock-claim.ts +134 -0
  110. package/src/codex/prompt-lock-evidence.ts +26 -0
  111. package/src/codex/prompt-lock-io.ts +16 -0
  112. package/src/codex/prompt-lock-owner.ts +130 -0
  113. package/src/codex/prompt-lock.ts +84 -53
  114. package/src/codex/routing/failure-window.ts +146 -0
  115. package/src/codex/routing/selection.ts +4 -1
  116. package/src/codex/routing.ts +13 -17
  117. package/src/codex/runtime.ts +106 -18
  118. package/src/codex/shim.ts +45 -19
  119. package/src/combos/jev-service-exchange.ts +281 -0
  120. package/src/combos/jev.ts +34 -220
  121. package/src/config/atomic-write.ts +1 -1
  122. package/src/config/codex-ws-reuse-setting.ts +10 -0
  123. package/src/config/live-reconcile.ts +17 -0
  124. package/src/config/load-degrade.ts +8 -0
  125. package/src/config/rebase-provenance.ts +11 -0
  126. package/src/config/schema/config-schema.ts +4 -0
  127. package/src/generated/compatibility-version.json +375 -227
  128. package/src/generated/model-metadata.ts +2 -2
  129. package/src/grok/inject.ts +276 -206
  130. package/src/grok/reset-coupon-ledger.ts +223 -57
  131. package/src/grok/reset-coupons.ts +13 -6
  132. package/src/grok/sync.ts +2 -1
  133. package/src/images/loop.ts +29 -11
  134. package/src/integrations/aside-profiles.ts +10 -3
  135. package/src/integrations/catalog-refresh.ts +18 -2
  136. package/src/integrations/cursor-detect.ts +1 -1
  137. package/src/integrations/writer.ts +9 -3
  138. package/src/lib/bun-path-runtime.d.mts +32 -0
  139. package/src/lib/bun-path-runtime.mjs +95 -0
  140. package/src/lib/bun-runtime-preflight.ts +140 -0
  141. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  142. package/src/lib/desktop-cli-handoff.mjs +92 -0
  143. package/src/lib/desktop-cli-record.d.mts +29 -0
  144. package/src/lib/desktop-cli-record.mjs +102 -0
  145. package/src/lib/errors.ts +25 -0
  146. package/src/lib/redact.ts +2 -2
  147. package/src/lib/request-execution-budget.ts +8 -0
  148. package/src/lib/synced-state-location.ts +46 -6
  149. package/src/lib/test-home-guard.ts +87 -1
  150. package/src/lib/translator-budget.ts +17 -4
  151. package/src/lib/windows-atomic-replace.ts +38 -16
  152. package/src/lib/windows-elevation.ts +6 -0
  153. package/src/lib/windows-secret-acl.ts +92 -25
  154. package/src/lib/winsw.ts +2 -3
  155. package/src/oauth/anthropic-account-refusal.ts +39 -1
  156. package/src/oauth/anthropic-routing.ts +1 -1
  157. package/src/oauth/anthropic-send-ownership.ts +3 -3
  158. package/src/oauth/chatgpt.ts +113 -41
  159. package/src/oauth/index.ts +7 -3
  160. package/src/oauth/meta-muse-device.ts +1 -1
  161. package/src/oauth/store.ts +1 -1
  162. package/src/oauth/types.ts +1 -1
  163. package/src/providers/anthropic-fast.ts +1 -1
  164. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  165. package/src/providers/azure-model-metadata.ts +115 -0
  166. package/src/providers/derive.ts +21 -0
  167. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  168. package/src/providers/devin-provider-merge-migration.ts +1 -1
  169. package/src/providers/github-copilot-auto.ts +169 -0
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/muse-subscription-usage.ts +1 -1
  172. package/src/providers/quota/vendor-probes-key.ts +127 -17
  173. package/src/providers/quota.ts +1 -1
  174. package/src/providers/registry/entries-core.ts +6 -6
  175. package/src/providers/registry/entries-extended.ts +4 -0
  176. package/src/providers/registry/model-seeds.ts +3 -3
  177. package/src/providers/xai-fast-model.ts +1 -1
  178. package/src/responses/reasoning-envelope.ts +29 -1
  179. package/src/responses/reasoning-replay-cache.ts +17 -1
  180. package/src/responses/spill-store.ts +15 -2
  181. package/src/responses/state/metrics.ts +8 -0
  182. package/src/responses/state/spill-failure.ts +12 -0
  183. package/src/responses/state/spill-queue.ts +12 -5
  184. package/src/responses/state.ts +18 -10
  185. package/src/responses/task-input.ts +17 -0
  186. package/src/server/background-lifecycle.ts +27 -0
  187. package/src/server/chat-completions.ts +1 -0
  188. package/src/server/chat-native.ts +28 -12
  189. package/src/server/claude-messages.ts +43 -6
  190. package/src/server/inbound-body-admission.ts +25 -7
  191. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  192. package/src/server/index/link-listener.ts +30 -17
  193. package/src/server/index/serve-options.ts +18 -4
  194. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  195. package/src/server/index/startup-warnings.ts +19 -0
  196. package/src/server/index.ts +2 -1
  197. package/src/server/lifecycle.ts +31 -4
  198. package/src/server/management/agent-settings-routes.ts +93 -32
  199. package/src/server/management/codex-agent-role-routes.ts +4 -2
  200. package/src/server/management/codex-prompt-routes.ts +1 -0
  201. package/src/server/management/codex-role-auto-assign.ts +13 -2
  202. package/src/server/management/config-routes.ts +59 -8
  203. package/src/server/management/context.ts +2 -2
  204. package/src/server/management/grok-coupon-routes.ts +277 -52
  205. package/src/server/management/link-routes.ts +42 -20
  206. package/src/server/management/route-registry.ts +2 -2
  207. package/src/server/management/shared.ts +3 -3
  208. package/src/server/messages-native-oauth.ts +9 -8
  209. package/src/server/messages-native.ts +49 -13
  210. package/src/server/messages-response-headers.ts +14 -0
  211. package/src/server/relay-eager.ts +2 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-conversation.ts +48 -5
  214. package/src/server/request-log-failure-attribution.ts +3 -0
  215. package/src/server/request-log-terminal-status.ts +186 -0
  216. package/src/server/request-log.ts +15 -89
  217. package/src/server/responses/adapter-continuation.ts +11 -3
  218. package/src/server/responses/adapter-dispatch.ts +99 -19
  219. package/src/server/responses/codex-ws-exchange.ts +17 -2
  220. package/src/server/responses/codex-ws-pool.ts +100 -13
  221. package/src/server/responses/codex-ws-wire.ts +9 -0
  222. package/src/server/responses/combo-requested-effort.ts +3 -1
  223. package/src/server/responses/compact.ts +3 -2
  224. package/src/server/responses/core-codex-account.ts +22 -1
  225. package/src/server/responses/core-combo-native.ts +20 -3
  226. package/src/server/responses/core-combo.ts +22 -14
  227. package/src/server/responses/core-lifetime.ts +3 -36
  228. package/src/server/responses/core-opaque-recovery.ts +5 -5
  229. package/src/server/responses/core-options.ts +5 -0
  230. package/src/server/responses/core-replay.ts +73 -0
  231. package/src/server/responses/core.ts +9 -9
  232. package/src/server/responses/empty-completion-guard.ts +18 -2
  233. package/src/server/responses/fetch-helpers.ts +26 -3
  234. package/src/server/responses/jev-model-invoke.ts +1 -1
  235. package/src/server/responses/passthrough-delivery.ts +24 -2
  236. package/src/server/responses/passthrough-dispatch.ts +134 -62
  237. package/src/server/responses/passthrough-execution.ts +3 -3
  238. package/src/server/responses/request-prepare.ts +4 -0
  239. package/src/server/responses/request-send-budget.ts +112 -49
  240. package/src/server/responses/request-transport.ts +125 -5
  241. package/src/server/responses/response-effects.ts +1 -0
  242. package/src/server/responses/run-turn-execution.ts +15 -5
  243. package/src/server/responses/sidecar-execution.ts +30 -16
  244. package/src/server/responses/sidecar-send-budget.ts +58 -0
  245. package/src/server/responses/spend-storage-error.ts +33 -0
  246. package/src/server/responses/ws-upstream.ts +19 -6
  247. package/src/server/startup-health-cache.ts +2 -1
  248. package/src/server/workflow-refusal.ts +3 -0
  249. package/src/service/cli.ts +20 -4
  250. package/src/service/desktop-command-guard.ts +32 -0
  251. package/src/service/desktop-startup.ts +66 -40
  252. package/src/service/desktop-supervision.d.mts +26 -0
  253. package/src/service/desktop-supervision.mjs +140 -0
  254. package/src/service/managing-cli.ts +1 -0
  255. package/src/service/orchestration.ts +46 -20
  256. package/src/service/repair.ts +25 -9
  257. package/src/service/state.ts +4 -3
  258. package/src/service/systemd.ts +17 -8
  259. package/src/service/windows-ops.ts +61 -22
  260. package/src/storage/policy-job.ts +5 -1
  261. package/src/storage/policy-worker.ts +3 -1
  262. package/src/storage/policy.ts +9 -0
  263. package/src/types/config.ts +21 -0
  264. package/src/types/request.ts +10 -0
  265. package/src/types/wire.ts +1 -0
  266. package/src/update/index.ts +57 -9
  267. package/src/update/restart-ownership.ts +10 -3
  268. package/src/update/runtime-ownership.d.mts +6 -0
  269. package/src/update/runtime-ownership.mjs +23 -7
  270. package/src/usage/expected-prices.ts +1 -1
  271. package/src/web-search/loop.ts +5 -2
  272. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -0,0 +1,85 @@
1
+ import { sanitizeLogMetadataString } from "../../lib/redact";
2
+ import { isOpenAiOperatedResponsesDestination } from "../../providers/openai-tiers";
3
+ import { normalizedProviderEndpoint, PROVIDER_REGISTRY } from "../../providers/registry";
4
+ import type { OcxProviderConfig } from "../../types";
5
+
6
+ /**
7
+ * #6675: a custom Responses provider drops replayed plaintext reasoning unless it sets
8
+ * `preserveResponsesReasoningContent`. That default is deliberate (the ChatGPT backend rejects
9
+ * echoed raw reasoning, and forwarding it costs tokens), but it is silent, and the similarly named
10
+ * Chat-wire `preserveReasoningContentModels` does not opt in. Say so once per destination.
11
+ *
12
+ * Only a hand-configured `openai-responses` row with key auth qualifies. Registry presets (matched
13
+ * by endpoint on either OpenAI wire, since a preset may be routed to Responses per model), OAuth and
14
+ * forward rows, Azure, and OpenAI-operated destinations are skipped: their replay contract is
15
+ * curated, so blanking there is a decision rather than a missing setting. The notice carries only
16
+ * the destination host and option names, never request content.
17
+ */
18
+ const MAX_NOTICED_DESTINATIONS = 64;
19
+ const noticedDestinations = new Set<string>();
20
+
21
+ function destinationHost(baseUrl: unknown): string | undefined {
22
+ if (typeof baseUrl !== "string") return undefined;
23
+ try {
24
+ return sanitizeLogMetadataString(new URL(baseUrl).host, 128);
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ }
29
+
30
+ /** Azure OpenAI and Azure AI Foundry serve OpenAI-operated models under per-resource hosts. */
31
+ const AZURE_OPENAI_HOST_SUFFIXES = [".openai.azure.com", ".services.ai.azure.com", ".cognitiveservices.azure.com"];
32
+
33
+ function hostnameOf(url: string): string | undefined {
34
+ try {
35
+ return new URL(url).hostname.toLowerCase();
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ /** A templated preset (`https://{resource}.openai.azure.com/...`) matches by host pattern. */
42
+ function templatedHostMatches(template: string, hostname: string): boolean {
43
+ const host = /^[a-z][a-z0-9+.-]*:\/\/([^/?#]+)/i.exec(template)?.[1];
44
+ if (!host || !/\{[^}]*\}/.test(host)) return false;
45
+ const pattern = host.toLowerCase().split(/\{[^}]*\}/).map(part => part.replace(/[.*+?^$()|[\]\\]/g, "\\$&")).join("[^.]+");
46
+ return new RegExp(`^${pattern}$`).test(hostname);
47
+ }
48
+
49
+ function isRegistryDestination(baseUrl: string): boolean {
50
+ const endpoint = normalizedProviderEndpoint(baseUrl);
51
+ const hostname = hostnameOf(baseUrl);
52
+ if (hostname && AZURE_OPENAI_HOST_SUFFIXES.some(suffix => hostname.endsWith(suffix))) return true;
53
+ return PROVIDER_REGISTRY.some(entry =>
54
+ normalizedProviderEndpoint(entry.baseUrl) === endpoint
55
+ || (hostname !== undefined && templatedHostMatches(entry.baseUrl, hostname))
56
+ || (entry.destinationAliases ?? []).some(alias => normalizedProviderEndpoint(alias.baseUrl) === endpoint));
57
+ }
58
+
59
+ function isCustomResponsesProvider(provider: OcxProviderConfig): boolean {
60
+ if (provider.adapter !== "openai-responses") return false;
61
+ if (provider.authMode !== undefined && provider.authMode !== "key") return false;
62
+ if (typeof provider.baseUrl !== "string" || !provider.baseUrl) return false;
63
+ return !isOpenAiOperatedResponsesDestination(provider) && !isRegistryDestination(provider.baseUrl);
64
+ }
65
+
66
+ export function plaintextReasoningDroppedNotice(host: string): string {
67
+ return `[opencodex] Replayed plaintext reasoning was dropped for the custom Responses provider at ${host}. `
68
+ + "If that endpoint accepts reasoning_text replay, set \"preserveResponsesReasoningContent\": true on the provider; "
69
+ + "\"preserveReasoningContentModels\" applies only to the Chat wire.";
70
+ }
71
+
72
+ export function notePlaintextReasoningDropped(
73
+ provider: OcxProviderConfig,
74
+ warn: (message: string) => void = message => console.warn(message),
75
+ ): void {
76
+ if (!isCustomResponsesProvider(provider)) return;
77
+ const host = destinationHost(provider.baseUrl);
78
+ if (!host || noticedDestinations.has(host) || noticedDestinations.size >= MAX_NOTICED_DESTINATIONS) return;
79
+ noticedDestinations.add(host);
80
+ warn(plaintextReasoningDroppedNotice(host));
81
+ }
82
+
83
+ export function resetPlaintextReasoningNoticesForTests(): void {
84
+ noticedDestinations.clear();
85
+ }
@@ -78,6 +78,11 @@ export function sanitizeReasoningInputContent(
78
78
  * placeholder, rather than emitted in the shape the upstream 400s on.
79
79
  */
80
80
  requirePlaintextReasoning?: boolean;
81
+ /**
82
+ * Called once per sanitized body when a reasoning item that carried nonempty plaintext
83
+ * `reasoning_text` was blanked. Observability only; it never changes the body (#6675).
84
+ */
85
+ onPlaintextReasoningBlanked?: () => void;
81
86
  },
82
87
  ): unknown {
83
88
  if (!body || typeof body !== "object" || Array.isArray(body)) return body;
@@ -85,6 +90,7 @@ export function sanitizeReasoningInputContent(
85
90
  if (!Array.isArray(raw.input)) return body;
86
91
 
87
92
  let changed = false;
93
+ let blankedPlaintext = false;
88
94
  const input = raw.input.map(item => {
89
95
  if (!item || typeof item !== "object" || Array.isArray(item)) return item;
90
96
  const rec = item as Record<string, unknown>;
@@ -148,11 +154,15 @@ export function sanitizeReasoningInputContent(
148
154
  // guide merges reasoning items into the adjacent assistant message), so providers flagged
149
155
  // `preserveResponsesReasoningContent` keep it — deleting valid replay content there breaks
150
156
  // continuations after tool calls (issue #875 family).
151
- if (blankContent) next.content = [];
157
+ if (blankContent) {
158
+ if (hasPlaintextReasoningContent(rec.content)) blankedPlaintext = true;
159
+ next.content = [];
160
+ }
152
161
  if (missingPlaintextReasoning) next.content = plaintextReasoningBackfill(next);
153
162
  return next;
154
163
  });
155
164
 
165
+ if (blankedPlaintext) opts?.onPlaintextReasoningBlanked?.();
156
166
  return changed ? { ...raw, input } : body;
157
167
  }
158
168
 
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../empty-tool-output-annotation";
3
3
  import { isPlainObject } from "./internal";
4
4
  import { peekBridgeSearchReplay } from "../../responses/bridge-search-replay-cache";
5
+ import { externalTaskInputResponsesContent } from "../../responses/task-input";
5
6
 
6
7
  const MAX_RESPONSES_CALL_ID_LENGTH = 64;
7
8
 
@@ -169,6 +170,16 @@ export function repairUnidentifiedToolOutputItems(body: unknown): unknown {
169
170
  || (typeof item.call_id === "string" && item.call_id.length > 0)) {
170
171
  return item;
171
172
  }
173
+ // #6764: an external task envelope is user input, not an orphaned result. Recognize it
174
+ // before the generic repair so the summarizer and the destination see a plain user turn.
175
+ // Any nonempty string call_id stays a tool result, as on the parsed path: the request schema
176
+ // accepts a whitespace call_id as a function_call_output and strips the envelope fields
177
+ // (src/responses/schema.ts), so both paths must agree that it is not task input.
178
+ const taskInput = externalTaskInputResponsesContent(item);
179
+ if (taskInput) {
180
+ changed = true;
181
+ return { type: "message", role: "user", content: taskInput };
182
+ }
172
183
  if (!isRepairableToolOutput(item.output)) return item;
173
184
  changed = true;
174
185
  return {
@@ -380,6 +391,15 @@ export function repairOrphanedInputItems(
380
391
  const isCustomOutput = item.type === "custom_tool_call_output";
381
392
  if (isFnOutput || isCustomOutput) {
382
393
  flushPendingSyntheticOutputs();
394
+ // Same rule as repairUnidentifiedToolOutputItems: a nonempty string call_id is a tool result
395
+ // on the parsed path (the schema strips the envelope fields), so it is never task input here.
396
+ const hasStringCallId = typeof item.call_id === "string" && item.call_id.length > 0;
397
+ const taskInput = isFnOutput && !hasStringCallId ? externalTaskInputResponsesContent(item) : undefined;
398
+ if (taskInput) {
399
+ changed = true;
400
+ repaired.push({ type: "message", role: "user", content: taskInput });
401
+ continue;
402
+ }
383
403
  const callId = typeof item.call_id === "string" ? item.call_id : "";
384
404
  const paired = isFnOutput ? functionCallIds.has(callId) : customCallIds.has(callId);
385
405
  const usableOutput = isRepairableToolOutput(item.output);
@@ -12,6 +12,7 @@ import {
12
12
  cyberPolicyErrorType,
13
13
  CYBER_POLICY_ERROR_CODE,
14
14
  isCyberPolicyCode,
15
+ SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE,
15
16
  type OcxErrorPayload,
16
17
  } from "../lib/errors";
17
18
 
@@ -22,6 +23,10 @@ export function formatErrorResponse(
22
23
  options?: { code?: string | null; retryAfter?: string | null },
23
24
  ): Response {
24
25
  const error = classifyError(status, type, message);
26
+ if (options?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) {
27
+ error.type = "server_error";
28
+ error.code = SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
29
+ }
25
30
  if (isCyberPolicyCode(options?.code)) {
26
31
  error.code = CYBER_POLICY_ERROR_CODE;
27
32
  error.type = cyberPolicyErrorType(type);
@@ -36,7 +41,8 @@ export function formatErrorResponse(
36
41
  // that would hand the client a 5xx it is configured to retry four times.
37
42
  const finalStatus = error.code === CYBER_POLICY_ERROR_CODE
38
43
  ? 400
39
- : isReplayRefusalCode(error.code) ? REPLAY_REFUSED_STATUS : status;
44
+ : error.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? 503
45
+ : isReplayRefusalCode(error.code) ? REPLAY_REFUSED_STATUS : status;
40
46
  const headers = new Headers({ "Content-Type": "application/json" });
41
47
  const retryAfter = options?.retryAfter?.trim();
42
48
  if (error.code !== CYBER_POLICY_ERROR_CODE
@@ -15,7 +15,8 @@ import { lstatSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileS
15
15
  import { join } from "node:path";
16
16
  import type { OcxConfig } from "../types";
17
17
  import { renameAtomicFile } from "../lib/windows-atomic-replace";
18
- import { entryParts, SAFE_AGENT_MODEL_ID, withSubagentContextMarker } from "./subagent-model";
18
+ import { assertNotRealClaudeConfigUnderTest } from "../lib/test-home-guard";
19
+ import { accountsAt200k, entryParts, SAFE_AGENT_MODEL_ID, withSubagentContextMarker } from "./subagent-model";
19
20
  import { stripOneMillionMarker } from "./context-windows";
20
21
  import { claudeConfigDir } from "./gateway-cache";
21
22
  import { DEFAULT_SUBAGENT_MODELS } from "../config";
@@ -97,7 +98,7 @@ export function buildClaudeAgentDefs(
97
98
  // Generated defs mark [1m] on the authoritative window only — never the
98
99
  // main-session auto-context predicate (a 372K route marked [1m] would be
99
100
  // accounted at 1M with no compaction safety net in the subagent).
100
- const model = withSubagentContextMarker(alias, windows);
101
+ const model = withSubagentContextMarker(alias, windows, accountsAt200k(config));
101
102
  const bare = alias.toLowerCase();
102
103
  if (coveredModels.has(bare)) return;
103
104
  coveredModels.add(bare);
@@ -130,7 +131,7 @@ export function buildClaudeAgentDefs(
130
131
  // the next launch sync — documented limit. No resolvable default -> no self def.
131
132
  const selfModel = pickerDefaultModel(configDir) ?? (config.claudeCode?.model?.trim() || null);
132
133
  if (selfModel) {
133
- const marked = withSubagentContextMarker(selfModel, windows);
134
+ const marked = withSubagentContextMarker(selfModel, windows, accountsAt200k(config));
134
135
  defs.push({
135
136
  file: `${OWNED_PREFIX}self.md`,
136
137
  name: `${OWNED_PREFIX}self`,
@@ -203,6 +204,16 @@ function isOwnedFile(path: string): boolean {
203
204
  * atomic (tmp + rename). Best-effort — returns null on any failure.
204
205
  */
205
206
  export function syncClaudeAgentDefs(defs: readonly ClaudeAgentDef[], configDir = claudeConfigDir()): string[] | null {
207
+ // Outside the best-effort catch: under an armed test process a write or prune of the
208
+ // real Claude agents directory must throw, never degrade to "returned null" (#6775).
209
+ // Every path written is judged by where it resolves, including a pre-placed link named like
210
+ // a definition or its temporary file.
211
+ const agentsDir = join(configDir, "agents");
212
+ assertNotRealClaudeConfigUnderTest(
213
+ configDir,
214
+ agentsDir,
215
+ ...defs.flatMap(def => [join(agentsDir, def.file), `${join(agentsDir, def.file)}.tmp-${process.pid}`]),
216
+ );
206
217
  try {
207
218
  const dir = join(configDir, "agents");
208
219
  if (defs.length === 0) {
@@ -15,33 +15,19 @@ import { desktop3pAlias } from "./desktop-3p";
15
15
  import { nativeOpenAiContextWindow, type CatalogModel, type NativeContextLimitsInput } from "../codex/catalog";
16
16
  import { ANTHROPIC_MODEL_CONTEXT_WINDOWS } from "../providers/registry/model-seeds";
17
17
  import type { OcxClaudeCodeConfig } from "../types";
18
+ import { AUTO_COMPACT_WINDOW_DEFAULT, AUTO_CONTEXT_FLOOR, ONE_MILLION } from "./long-context";
18
19
 
19
- const ONE_MILLION = 1_000_000;
20
+ export { AUTO_COMPACT_WINDOW_DEFAULT, AUTO_CONTEXT_FLOOR } from "./long-context";
20
21
 
21
22
  /**
22
- * The native id each Claude Code tier alias resolves to (2.1.282: `--model opus` sends
23
- * `claude-opus-5-5`, `sonnet` sends `claude-sonnet-5`, `fable` sends `claude-fable-5-1`).
23
+ * The native id each Claude Code tier alias resolves to (2.1.296: `--model opus` sends
24
+ * `claude-opus-5-5`, `sonnet` sends `claude-sonnet-5-5`, `fable` sends `claude-fable-5-1`).
24
25
  * Behind a gateway Claude Code accounts an unmarked id at 200k, so an unset tier slot is filled
25
26
  * with this id when the map marks it (#5755). A marked alias cannot stand in: Claude Code sends
26
27
  * `sonnet[1m]` upstream unresolved. Haiku has no entry; its 200k window never marks.
27
28
  */
28
- const CLAUDE_CODE_NATIVE_TIERS = { opus: "claude-opus-5-5", sonnet: "claude-sonnet-5", fable: "claude-fable-5-1" } as const;
29
+ const CLAUDE_CODE_NATIVE_TIERS = { opus: "claude-opus-5-5", sonnet: "claude-sonnet-5-5", fable: "claude-fable-5-1" } as const;
29
30
 
30
- /**
31
- * Auto-context defaults (devlog 260712 020, user-approved).
32
- *
33
- * The compact window is the token count at which Claude Code starts compacting, and it is
34
- * also the floor `shouldMarkOneMillion` uses — a model may only carry the marker if it can
35
- * host this window. 350,000 was chosen when the widest native row advertised 372,000.
36
- *
37
- * It now matches the auto-compaction limit the Codex catalog ships for the same models
38
- * (`nativeAutoCompactLimit`: 829,800 against the 922,000 native window). Leaving the two
39
- * apart meant one model compacting at 350k under Claude Code and at 829,800 under Codex.
40
- * The value stays clear of the measured 922,000 ceiling by ~92k, so compaction still has
41
- * room to run before the upstream refuses.
42
- */
43
- export const AUTO_COMPACT_WINDOW_DEFAULT = 829_800;
44
- export const AUTO_CONTEXT_FLOOR = 200_000;
45
31
  /** Binary-verified accepted range for CLAUDE_CODE_AUTO_COMPACT_WINDOW (2.1.207: pSo=1e5, yDs=1e6). */
46
32
  export const AUTO_COMPACT_WINDOW_MIN = 100_000;
47
33
  export const AUTO_COMPACT_WINDOW_MAX = ONE_MILLION;
@@ -59,14 +45,27 @@ export interface AutoContextMode {
59
45
  enabled: boolean;
60
46
  /** Effective CLAUDE_CODE_AUTO_COMPACT_WINDOW value (tokens). */
61
47
  compactWindow: number;
48
+ /** claudeCode.contextAccounting "200k": nothing is marked [1m] automatically, whatever its window. */
49
+ accounting200k?: true;
62
50
  }
63
51
 
64
52
  export const AUTO_CONTEXT_OFF: AutoContextMode = { enabled: false, compactWindow: AUTO_COMPACT_WINDOW_DEFAULT };
65
53
 
54
+ /**
55
+ * Marking mode for surfaces whose runner may not inherit the compaction env (Desktop pickers,
56
+ * discovery, generated subagents). shouldMarkOneMillion under it equals isLongContextWindow
57
+ * (long-context.ts); kept as a mode so the existing marker helpers serve every surface.
58
+ */
59
+ export const UNPAIRED_AUTO_CONTEXT: AutoContextMode = { enabled: true, compactWindow: AUTO_COMPACT_WINDOW_DEFAULT };
60
+
61
+ /** The "200k" opt-in: no widening, no compact-window injection, no automatic marker. */
62
+ export const ACCOUNTING_200K: AutoContextMode = { enabled: false, compactWindow: AUTO_COMPACT_WINDOW_DEFAULT, accounting200k: true };
63
+
66
64
  interface AutoContextConfigSlice {
67
65
  autoContext?: boolean;
68
66
  autoCompactWindow?: number;
69
67
  maxContextTokens?: number;
68
+ contextAccounting?: string;
70
69
  }
71
70
 
72
71
  function inAutoCompactRange(value: number): boolean {
@@ -90,6 +89,8 @@ function inAutoCompactRange(value: number): boolean {
90
89
  * to AUTO_COMPACT_WINDOW_DEFAULT (the management API rejects them; this guards hand-edits).
91
90
  */
92
91
  export function resolveAutoContext(claudeCode: AutoContextConfigSlice | undefined, envOverride?: string): AutoContextMode {
92
+ // The 200k opt-in wins over every other lever, a user-exported compact window included.
93
+ if (claudeCode?.contextAccounting === "200k") return ACCOUNTING_200K;
93
94
  if (claudeCode?.autoContext === false) return AUTO_CONTEXT_OFF;
94
95
  const maxCtx = claudeCode?.maxContextTokens;
95
96
  if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) return AUTO_CONTEXT_OFF;
@@ -132,6 +133,7 @@ export function claudeToolSearchEnv(value: boolean | string | undefined): string
132
133
  */
133
134
  export function shouldMarkOneMillion(window: number | undefined, auto: AutoContextMode): boolean {
134
135
  if (typeof window !== "number" || window <= 0) return false;
136
+ if (auto.accounting200k) return false;
135
137
  if (window >= ONE_MILLION) return true;
136
138
  return auto.enabled && window > AUTO_CONTEXT_FLOOR && window >= auto.compactWindow;
137
139
  }
@@ -248,7 +250,7 @@ export interface ClaudeTierModels {
248
250
  * value injected into BOTH haiku variables.
249
251
  */
250
252
  export function effectiveModelEnv(
251
- claudeCode: { model?: string; smallFastModel?: string; tierModels?: ClaudeTierModels; autoContext?: boolean; autoCompactWindow?: number; maxContextTokens?: number } | undefined,
253
+ claudeCode: { model?: string; smallFastModel?: string; tierModels?: ClaudeTierModels; autoContext?: boolean; autoCompactWindow?: number; maxContextTokens?: number; contextAccounting?: string } | undefined,
252
254
  windows: Record<string, number>,
253
255
  autoOverride?: AutoContextMode,
254
256
  ): Record<string, string> {
@@ -10,9 +10,10 @@ import {
10
10
  } from "./desktop-3p-library";
11
11
  export { resolveDesktop3pConfigLibraryPath, type Desktop3pConfigLibraryOptions } from "./desktop-3p-library";
12
12
  import { createHash, randomUUID } from "node:crypto";
13
- import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
13
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, lstatSync, unlinkSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { atomicWriteFile, readConfigDiagnostics, withConfigMutationLockSync } from "../config";
16
+ import { atomicWriteFileNoFollow } from "../config/atomic-write";
16
17
  import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
17
18
  import type { OcxClaudeDesktopProfile } from "../types";
18
19
  import {
@@ -24,6 +25,7 @@ import {
24
25
  import { nativeOpenAiContextWindow, type NativeContextLimitsInput } from "../codex/catalog";
25
26
  import { localAdmissionToken, localInferenceDestination } from "../lib/local-destinations";
26
27
  import { assertDesktop3pModelsValid } from "./desktop-3p-guard";
28
+ import { claudeSurfaceSupportsOneMillion, ONE_MILLION } from "./long-context";
27
29
 
28
30
  export interface Desktop3pModelEntry {
29
31
  name: string;
@@ -55,12 +57,12 @@ export interface Desktop3pRoutedModel {
55
57
  }
56
58
 
57
59
  /**
58
- * 1M-context eligibility, shared with the Desktop DTO so the dashboard's 1M chip can
59
- * never disagree with what the writer emits. The DTO imports this from here — keeping
60
- * the constant in this module avoids a cycle, since shared.ts already reads
61
- * claude/desktop-profile.
60
+ * The genuine-1M threshold real Anthropic rows still need. Every other row follows the
61
+ * long-window rule; the writer, the profile renderer and the dashboard DTO all decide through
62
+ * `claudeSurfaceSupportsOneMillion` (long-context.ts), so the 1M chip cannot disagree with the
63
+ * written config.
62
64
  */
63
- export const DESKTOP_SUPPORTS_1M_THRESHOLD = 1_000_000;
65
+ export const DESKTOP_SUPPORTS_1M_THRESHOLD = ONE_MILLION;
64
66
 
65
67
  /** CLI arg parsing for `ocx claude desktop` mode flags (mutually exclusive). */
66
68
  export function parseDesktop3pModeArgs(flags: string[]): { mode: Desktop3pConfigMode } | { error: string } {
@@ -251,7 +253,7 @@ function collectDesktop3pModels(
251
253
  for (const { provider, id, contextWindow } of candidates) {
252
254
  const route = `${provider}/${id}`;
253
255
  const alias = desktop3pAlias(provider, id);
254
- const supports1m = typeof contextWindow === "number" && contextWindow >= DESKTOP_SUPPORTS_1M_THRESHOLD
256
+ const supports1m = claudeSurfaceSupportsOneMillion(provider, id, contextWindow)
255
257
  ? { supports1m: true as const }
256
258
  : {};
257
259
  if (alias === id) {
@@ -317,16 +319,30 @@ export function buildDesktop3pRegistry(
317
319
  }
318
320
 
319
321
  /** Generate Claude Desktop 3P model entries from the proxy's available models. */
322
+ export interface Desktop3pModelOptions {
323
+ /**
324
+ * false under claudeCode.contextAccounting "200k": a long-window model stays selectable at 1M
325
+ * (`supports1m`) but no longer defaults to it (`prefer1m` dropped).
326
+ */
327
+ preferOneMillion?: boolean;
328
+ }
329
+
320
330
  export function generateDesktop3pModels(
321
331
  nativeSlugs: string[],
322
332
  routedModels: Array<Desktop3pRoutedModel>,
323
333
  profile?: OcxClaudeDesktopProfile,
324
334
  nativeContextCap?: NativeContextLimitsInput,
335
+ options: Desktop3pModelOptions = {},
325
336
  ): Desktop3pModelEntry[] {
326
337
  const { models, registry, realAnthropicIds } = collectDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
327
338
  desktop3pRegistry = registry;
328
339
  desktop3pRealAnthropicIds = realAnthropicIds;
329
- return models;
340
+ return options.preferOneMillion === false ? models.map(({ prefer1m: _prefer1m, ...model }) => model) : models;
341
+ }
342
+
343
+ /** The Desktop model options a config asks for (one place, so writer and export agree). */
344
+ export function desktop3pModelOptions(claudeCode: { contextAccounting?: string } | undefined): Desktop3pModelOptions {
345
+ return { preferOneMillion: claudeCode?.contextAccounting !== "200k" };
330
346
  }
331
347
 
332
348
  /** Resolve an alias using the most recently generated Desktop model registry. */
@@ -382,6 +398,7 @@ export function generateDesktop3pConfig(
382
398
  mode: Desktop3pConfigMode = "static",
383
399
  profile?: OcxClaudeDesktopProfile,
384
400
  nativeContextCap?: NativeContextLimitsInput,
401
+ options: Desktop3pModelOptions = {},
385
402
  ): object {
386
403
  const base = {
387
404
  inferenceProvider: "gateway",
@@ -398,7 +415,7 @@ export function generateDesktop3pConfig(
398
415
  ...base,
399
416
  modelDiscoveryEnabled: mode === "hybrid",
400
417
  inferenceModels: (() => {
401
- const models = generateDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
418
+ const models = generateDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap, options);
402
419
  // Fail loud at the write boundary rather than ship a config Desktop rejects:
403
420
  // the output counterpart of the request-path guards.
404
421
  assertDesktop3pModelsValid(models);
@@ -662,6 +679,7 @@ export function writeDesktop3pConfig(
662
679
  profile?: OcxClaudeDesktopProfile,
663
680
  nativeContextCap?: NativeContextLimitsInput,
664
681
  lifecycleLockDeps?: ClientLifecycleLockDeps,
682
+ refreshOnly?: { appliedFingerprint: string; admit: () => boolean },
665
683
  ): { written: boolean; path: string; reason?: string; fingerprint?: string } {
666
684
  try {
667
685
  return withClientLifecycleSync(() => withConfigMutationLockSync(() => {
@@ -674,9 +692,29 @@ export function writeDesktop3pConfig(
674
692
  if (!claudeDesktopIntegrationEnabled(latest.config)) {
675
693
  return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desired_state_changed" };
676
694
  }
695
+ if (refreshOnly && !desktopRefreshPathsSafe(resolveDesktop3pConfigLibraryPath())) {
696
+ return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_refresh_only_skipped" };
697
+ }
677
698
  if (connection.kind === "connected" || inspectRemoteDesktopCleanup().kind !== "absent") {
678
699
  return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_remote_store_active" };
679
700
  }
701
+ let requiredId: string | undefined;
702
+ if (refreshOnly) {
703
+ const refused = { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_refresh_only_skipped" };
704
+ // Ownership and bytes must still match under L→C; discovery outside these locks is not authority.
705
+ if (!refreshOnly.admit() || latest.config.claudeCode?.desktopProfile?.appliedFingerprint !== refreshOnly.appliedFingerprint) return refused;
706
+ const inspected = inspectDesktop3pConfigLibrary({ appliedFingerprint: refreshOnly.appliedFingerprint });
707
+ if (inspected.kind !== "gateway_ours" || !inspected.appliedId || !inspected.selectedProfilePath) return refused;
708
+ if (!desktopRefreshPathsSafe(inspected.libraryPath, inspected.selectedProfilePath)) return refused;
709
+ const metadata = parseMetadata(join(inspected.libraryPath, "_meta.json"));
710
+ if (!isOwnedDesktopGatewayEntry(metadata.entries.find(entry => entry.id === inspected.appliedId))) return refused;
711
+ try {
712
+ const selected = JSON.parse(readFileSync(inspected.selectedProfilePath, "utf8")) as Record<string, unknown>;
713
+ mode = selected.modelDiscoveryEnabled !== true ? "static"
714
+ : Array.isArray(selected.inferenceModels) ? "hybrid" : "discovery";
715
+ } catch { return refused; }
716
+ requiredId = inspected.appliedId;
717
+ }
680
718
  // Claude Desktop runs on this machine, so it dials the unauthenticated loopback listener
681
719
  // when one is enabled — on such a hub that is the only credential-free local socket
682
720
  // (#4236) — and otherwise the bind address, which answers but demands data-plane
@@ -700,8 +738,9 @@ export function writeDesktop3pConfig(
700
738
  );
701
739
  }
702
740
  return writeDesktop3pConfigWithGenerator(() => (
703
- generateDesktop3pConfig(destination.origin, nativeSlugs, routedModels, gatewayKey, mode, profile, nativeContextCap)
704
- ));
741
+ generateDesktop3pConfig(destination.origin, nativeSlugs, routedModels, gatewayKey, mode, profile, nativeContextCap,
742
+ desktop3pModelOptions(latest.config.claudeCode))
743
+ ), requiredId && refreshOnly ? { requiredId, expectedFingerprint: refreshOnly.appliedFingerprint, admit: refreshOnly.admit } : undefined);
705
744
  }), lifecycleLockDeps);
706
745
  } catch { return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_lifecycle_busy_or_unsafe" }; }
707
746
  }
@@ -734,17 +773,38 @@ export function writeRemoteDesktop3pConfig(options: {
734
773
  } catch { return { written: false, path: "", reason: "desktop_lifecycle_busy_or_unsafe" }; }
735
774
  }
736
775
 
776
+ /** Refresh-only writes never follow links, including a dangling backup link. */
777
+ function desktopRefreshPathsSafe(libraryPath: string, configPath?: string): boolean {
778
+ try {
779
+ if (!lstatSync(libraryPath).isDirectory() || !lstatSync(join(libraryPath, "_meta.json")).isFile()) return false;
780
+ if (configPath) {
781
+ if (!lstatSync(configPath).isFile()) return false;
782
+ try { if (lstatSync(`${configPath}.bak`).isSymbolicLink()) return false; }
783
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") return false; }
784
+ }
785
+ return true;
786
+ } catch { return false; }
787
+ }
788
+
737
789
  function writeDesktop3pConfigWithGenerator(
738
790
  generate: () => object,
791
+ refreshOnly?: { requiredId: string; expectedFingerprint: string; admit: () => boolean },
739
792
  ): { written: boolean; path: string; reason?: string; fingerprint?: string } {
740
793
  const libraryPath = resolveDesktop3pConfigLibraryPath();
741
794
  const metadataPath = join(libraryPath, "_meta.json");
742
795
  let configPath = libraryPath;
743
796
 
744
797
  try {
745
- mkdirSync(libraryPath, { recursive: true, mode: 0o700 });
798
+ if (refreshOnly && !desktopRefreshPathsSafe(libraryPath)) {
799
+ return { written: false, path: libraryPath, reason: "desktop_refresh_only_skipped" };
800
+ }
801
+ if (!refreshOnly) mkdirSync(libraryPath, { recursive: true, mode: 0o700 });
746
802
  const metadata = parseMetadata(metadataPath);
747
803
  const selected = metadata.entries.find(entry => entry?.id === metadata.appliedId && isOwnedDesktopGatewayEntry(entry));
804
+ // A refresh may reuse only the applied entry; fallback allocation would silently enroll again.
805
+ if (refreshOnly && selected?.id !== refreshOnly.requiredId) {
806
+ return { written: false, path: libraryPath, reason: "desktop_refresh_only_skipped" };
807
+ }
748
808
  const existing = selected ?? metadata.entries.find(entry => isOwnedDesktopGatewayEntry(entry) && typeof entry.id === "string");
749
809
  const id = existing?.id ?? randomUUID();
750
810
  configPath = profilePath(libraryPath, id);
@@ -757,6 +817,22 @@ function writeDesktop3pConfigWithGenerator(
757
817
  const preserved = readDesktopProfileForeignKeys(configPath);
758
818
  const configJson = JSON.stringify({ ...preserved, ...generated }, null, 2) + "\n";
759
819
  const fingerprint = createHash("sha256").update(configJson).digest("hex").slice(0, 16);
820
+ if (refreshOnly) {
821
+ atomicWriteFileNoFollow(configPath, configJson, undefined, {
822
+ validateBeforeRename: () => {
823
+ try {
824
+ if (!refreshOnly.admit() || !desktopRefreshPathsSafe(libraryPath, configPath)) throw new Error();
825
+ const freshMetadata = parseMetadata(metadataPath);
826
+ if (freshMetadata.appliedId !== refreshOnly.requiredId
827
+ || !isOwnedDesktopGatewayEntry(freshMetadata.entries.find(entry => entry.id === refreshOnly.requiredId))
828
+ || createHash("sha256").update(readFileSync(configPath)).digest("hex").slice(0, 16) !== refreshOnly.expectedFingerprint) {
829
+ throw new Error();
830
+ }
831
+ } catch { throw new Error("desktop_refresh_only_skipped"); }
832
+ },
833
+ });
834
+ return { written: true, path: configPath, fingerprint };
835
+ }
760
836
  const { backupPath } = atomicReplaceDesktopConfig(configPath, configJson);
761
837
  try {
762
838
  atomicWriteFile(metadataPath, JSON.stringify({ ...metadata, appliedId: id, entries }, null, 2) + "\n");
@@ -2,7 +2,7 @@
2
2
  * Claude Desktop picker mode: the one server-side controller for every picker mutation.
3
3
  *
4
4
  * While a server runs, enable, disable and Desktop mode transitions run here, serialized by one
5
- * lock (devlog/_plan/260924_claude_desktop_picker_mode/030_wp4_picker_activation.md, D11).
5
+ * lock (devlog/_fin/260924_claude_desktop_picker_mode/030_wp4_picker_activation.md, D11).
6
6
  */
7
7
  import type { OcxConfig } from "../types";
8
8
  import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { routeSupportsOneMillion } from "./long-context";
2
3
  import type {
3
4
  OcxClaudeDesktopAssignment,
4
5
  OcxClaudeDesktopFamily,
@@ -359,7 +360,7 @@ export function renderDesktopProfile(
359
360
  name: isRealAnthropicRoute(route) ? assignment.alias : desktopProfileWireAlias(assignment.alias),
360
361
  family: assignment.family,
361
362
  isFamilyDefault: effectiveDefaults[assignment.family] === route,
362
- supports1m: typeof model.contextWindow === "number" && model.contextWindow >= 1_000_000,
363
+ supports1m: routeSupportsOneMillion(route, model.contextWindow),
363
364
  };
364
365
  });
365
366
  }
@@ -17,6 +17,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
17
17
  import { homedir } from "node:os";
18
18
  import { join } from "node:path";
19
19
  import { localAdmissionToken, localInferenceDestination } from "../lib/local-destinations";
20
+ import { assertNotRealClaudeConfigUnderTest } from "../lib/test-home-guard";
20
21
  import type { OcxConfig } from "../types";
21
22
 
22
23
  export interface GatewayModelRow {
@@ -46,11 +47,27 @@ export interface GatewayModelTarget {
46
47
  /** Claude Code config dir (CLAUDE_CONFIG_DIR override honored, like the CLI). */
47
48
  export function claudeConfigDir(): string {
48
49
  const custom = process.env.CLAUDE_CONFIG_DIR;
49
- return custom && custom.length > 0 ? custom : join(homedir(), ".claude");
50
+ return custom && custom.length > 0 ? custom : join(currentUserHome(), ".claude");
51
+ }
52
+
53
+ /**
54
+ * The home Claude Code itself resolves. Claude Code runs on Node, whose `os.homedir()`
55
+ * consults HOME (POSIX) or USERPROFILE (Windows) at call time when it is set; Bun's
56
+ * `os.homedir()` keeps the value it read at startup. In production the two agree. Under
57
+ * the test preload, which rewrites HOME after Bun has started, only this one follows the
58
+ * sandbox — the cached value is the developer's real home (#6775). Exported for tests.
59
+ */
60
+ export function currentUserHome(env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string {
61
+ const fromEnv = platform === "win32" ? env.USERPROFILE : env.HOME;
62
+ return fromEnv !== undefined && fromEnv !== "" ? fromEnv : homedir();
50
63
  }
51
64
 
52
65
  /** Write the cache file; returns its path or null (best-effort, never throws). */
53
66
  export function writeGatewayModelCache(baseUrl: string, models: readonly GatewayModelRow[], configDir = claudeConfigDir()): string | null {
67
+ // Outside the best-effort catch: an armed test process must not write here, and must
68
+ // fail loudly rather than degrade to "returned null".
69
+ // The file itself too: writeFileSync follows a link at gateway-models.json.
70
+ assertNotRealClaudeConfigUnderTest(configDir, join(configDir, "cache"), join(configDir, "cache", "gateway-models.json"));
54
71
  try {
55
72
  // Mirror the CLI's usable-id filter so our file matches what it would cache.
56
73
  const usable = models.filter(m => /(claude|anthropic)/i.test(m.id));