@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -15,7 +15,7 @@ import { lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync
15
15
  import { join } from "node:path";
16
16
  import type { OcxConfig } from "../types";
17
17
  import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
18
- import { resolveAutoContext, stripOneMillionMarker, withOneMillionMarker } from "./context-windows";
18
+ import { AUTO_CONTEXT_OFF, shouldMarkOneMillion, stripOneMillionMarker, withOneMillionMarker } from "./context-windows";
19
19
  import { claudeConfigDir } from "./gateway-cache";
20
20
  import { DEFAULT_SUBAGENT_MODELS, hasOwnProvider } from "../config";
21
21
  import { effectiveBlockedSkillNames, resolveInboundModel } from "./inbound";
@@ -58,6 +58,27 @@ function pickerDefaultModel(configDir: string): string | null {
58
58
  /** Roster entry -> alias + display parts. Entries are bare native slugs or "provider/id".
59
59
  * Codex-facing encoded ids (`provider/vendor-model`) decode to the native slash id first
60
60
  * so the alias joins the raw-native context-window map (context-windows.ts). */
61
+
62
+ /**
63
+ * Generated subagent defs cannot rely on the parent's auto-context compaction
64
+ * pairing, so their [1m] marker follows the AUTHORITATIVE window only: mark when
65
+ * the effective window (exact selector, then the canonical [1m] form, then bare)
66
+ * is genuinely >= 1M; strip an inherited unsafe marker back to the bare selector;
67
+ * with no window information, keep the selector as it was. Genuine routed [1m]
68
+ * ids are preserved through the canonical-exact lookup. (#854)
69
+ */
70
+ function withSubagentContextMarker(selector: string, windows: Record<string, number>): string {
71
+ const bare = stripOneMillionMarker(selector);
72
+ const wasMarked = selector !== bare;
73
+ const canonicalExact = wasMarked ? `${bare}[1m]` : selector;
74
+ const authoritativeWindow = windows[selector] ?? windows[canonicalExact] ?? windows[bare];
75
+ if (typeof authoritativeWindow === "number" && authoritativeWindow > 0) {
76
+ return shouldMarkOneMillion(authoritativeWindow, AUTO_CONTEXT_OFF)
77
+ ? (withOneMillionMarker(selector, windows) ?? selector)
78
+ : bare;
79
+ }
80
+ return wasMarked ? selector : bare;
81
+ }
61
82
  function entryParts(entry: string, config: OcxConfig): { alias: string; id: string; provider: string } {
62
83
  const slash = entry.indexOf("/");
63
84
  if (slash > 0) {
@@ -72,7 +93,6 @@ function entryParts(entry: string, config: OcxConfig): { alias: string; id: stri
72
93
  }
73
94
 
74
95
  export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string, number>, configDir = claudeConfigDir()): ClaudeAgentDef[] {
75
- const auto = resolveAutoContext(config.claudeCode);
76
96
  const blockedSkills = effectiveBlockedSkillNames(config.claudeCode);
77
97
  const blockedSkillsFor = (model: string): readonly string[] => {
78
98
  const unmarked = stripOneMillionMarker(model);
@@ -87,8 +107,10 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
87
107
  const coveredModels = new Set<string>();
88
108
 
89
109
  const push = (name: string, alias: string, description: string) => {
90
- // Effective model value: [1m] marking follows the same predicate as env slots.
91
- const model = withOneMillionMarker(alias, windows, auto) ?? alias;
110
+ // Generated defs mark [1m] on the authoritative window only never the
111
+ // main-session auto-context predicate (a 372K route marked [1m] would be
112
+ // accounted at 1M with no compaction safety net in the subagent).
113
+ const model = withSubagentContextMarker(alias, windows);
92
114
  const bare = alias.toLowerCase();
93
115
  if (coveredModels.has(bare)) return;
94
116
  coveredModels.add(bare);
@@ -120,7 +142,7 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
120
142
  // the next launch sync — documented limit. No resolvable default -> no self def.
121
143
  const selfModel = pickerDefaultModel(configDir) ?? (config.claudeCode?.model?.trim() || null);
122
144
  if (selfModel) {
123
- const marked = withOneMillionMarker(selfModel, windows, auto) ?? selfModel;
145
+ const marked = withSubagentContextMarker(selfModel, windows);
124
146
  defs.push({
125
147
  file: `${OWNED_PREFIX}self.md`,
126
148
  name: `${OWNED_PREFIX}self`,
@@ -106,6 +106,10 @@ function toolResultOutput(block: Rec): string | Rec[] {
106
106
  } else if (item.type === "image") {
107
107
  const img = imageBlockToInputImage(item);
108
108
  if (img) out.push(img);
109
+ } else if (item.type === "document") {
110
+ // Same marker as the user-message document case below: the model should see the
111
+ // attachment happened instead of an empty tool output.
112
+ out.push({ type: "input_text", text: `[document${typeof item.title === "string" ? `: ${item.title}` : ""}]` });
109
113
  }
110
114
  }
111
115
  if (isError) out.unshift({ type: "input_text", text: "[tool error]" });
@@ -494,7 +498,13 @@ export function anthropicToResponsesTranslation(raw: unknown, cc?: OcxClaudeCode
494
498
  const thinking = raw.thinking;
495
499
  const outputConfigEffort = effortFromOutputConfig(raw.output_config);
496
500
  const thinkingDisabled = isRec(thinking) && thinking.type === "disabled";
497
- if (!thinkingDisabled && (isRec(thinking) || outputConfigEffort !== undefined)) {
501
+ if (thinkingDisabled) {
502
+ // An explicit "disabled" is an instruction, not an absence. Dropping it made this
503
+ // indistinguishable from a request that never mentioned thinking — and for models that
504
+ // think by default, omission means thinking is ON, sharing the caller's max_tokens (#545).
505
+ // "none" is the parser's disable sentinel (parser.ts REASONING_EFFORTS).
506
+ body.reasoning = { effort: "none", summary: "none" };
507
+ } else if (isRec(thinking) || outputConfigEffort !== undefined) {
498
508
  const reasoning: Rec = { summary: "auto" };
499
509
  if (outputConfigEffort !== undefined) {
500
510
  // Adaptive wire: /effort arrives as output_config.effort (devlog 080).
@@ -18,7 +18,7 @@
18
18
  import { catalogModelEfforts, nativeEffortClamp, nativeOpenAiContextWindow, type CatalogModel } from "../codex/catalog";
19
19
  import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
20
20
  import { desktop3pAlias } from "./desktop-3p";
21
- import { AUTO_CONTEXT_OFF, shouldMarkOneMillion, type AutoContextMode } from "./context-windows";
21
+ import { AUTO_CONTEXT_OFF, type AutoContextMode } from "./context-windows";
22
22
 
23
23
  const MODEL_INFO_CREATED_AT = "2026-01-01T00:00:00Z";
24
24
  const ANTHROPIC_EFFORT_RUNGS = new Set(["low", "medium", "high", "xhigh", "max"]);
@@ -113,19 +113,22 @@ export function buildAnthropicModelInfos(
113
113
  const seen = new Set<string>();
114
114
  // [1m] picker variant (devlog 260712 B1): Claude Code accounts exactly 1M for ids
115
115
  // carrying the marker (2.1.207 binary: /\[1m\]/i → 1e6, compaction preserved), so
116
- // models with an authoritative >=1M window get a second selectable row. In
117
- // auto-context mode (devlog 020) the predicate widens to windows > 200k that can
118
- // host the compact window display stays honest (real window, not "1M"). Guards
119
- // (audit R1#11): same dedupe set, never double-suffix.
120
- const push1mVariant = (base: AnthropicModelInfo, contextWindow: number | undefined, mode: AutoContextMode = auto) => {
121
- if (!shouldMarkOneMillion(contextWindow, mode)) return;
116
+ // ONLY models with an authoritative >=1M window get a second selectable row
117
+ // the auto-context widening that let a 372K route carry the marker (and be
118
+ // over-filled) is the #854 defect and does not come back. Guards (audit R1#11):
119
+ // same dedupe set, never double-suffix.
120
+ const push1mVariant = (base: AnthropicModelInfo, contextWindow: number | undefined) => {
121
+ // The [1m] marker makes Claude Code account 1e6 tokens for the row, so it
122
+ // may only name models whose AUTHORITATIVE effective window is >= 1M —
123
+ // never the auto-context widening, which would mark a 372K route and have
124
+ // Claude Code over-fill it (the #854 defect).
125
+ if (contextWindow === undefined || contextWindow < ONE_MILLION) return;
122
126
  if (base.id.includes("[1m]")) return;
123
127
  const id = `${base.id}[1m]`;
124
128
  if (seen.has(id)) return;
125
129
  seen.add(id);
126
130
  const window = contextWindow as number;
127
- const label = window >= ONE_MILLION ? "1M" : `${Math.round(window / 1_000)}k`;
128
- out.push({ ...base, id, display_name: `${base.display_name} · ${label}`, max_input_tokens: Math.min(window, ONE_MILLION) });
131
+ out.push({ ...base, id, display_name: `${base.display_name} · 1M`, max_input_tokens: ONE_MILLION });
129
132
  };
130
133
  for (const slug of nativeSlugs) {
131
134
  const id = idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug);
@@ -145,7 +148,7 @@ export function buildAnthropicModelInfos(
145
148
  out.push(info);
146
149
  // Anthropic passthrough guard (audit 021 #3): never auto-widen canonical claude
147
150
  // routes — only a genuine >=1M window earns the variant row there.
148
- push1mVariant(info, m.contextWindow, m.provider === "anthropic" ? AUTO_CONTEXT_OFF : auto);
151
+ push1mVariant(info, m.contextWindow);
149
152
  }
150
153
  return out;
151
154
  }
@@ -187,6 +187,8 @@ interface OpenBlock {
187
187
  argsBufBytes?: number;
188
188
  webSearchArgsEmitted?: boolean;
189
189
  callId?: string;
190
+ /** Last reasoning part identity (item + summary/content index) seen by this thinking block. */
191
+ reasoningPartKey?: string;
190
192
  }
191
193
 
192
194
  /** Streaming: Responses SSE bytes -> Anthropic Messages SSE bytes. */
@@ -353,6 +355,21 @@ export function responsesSseToAnthropicSse(
353
355
  case "response.reasoning_text.delta": {
354
356
  if (typeof data.delta !== "string" || data.delta.length === 0) break;
355
357
  ensureBlock("thinking");
358
+ // The JSON path joins reasoning summary/content parts with "\n\n"
359
+ // (responsesJsonToAnthropicMessage); mirror that at part and item boundaries
360
+ // so multi-part summaries do not glue into one run-on paragraph. Frames
361
+ // without part indices produce a constant key and never get a separator.
362
+ const slot = eventName === "response.reasoning_summary_text.delta"
363
+ ? `s${String(data.summary_index)}`
364
+ : `c${String(data.content_index)}`;
365
+ const partKey = `${String(data.item_id)}:${slot}`;
366
+ if (open!.reasoningPartKey !== undefined && open!.reasoningPartKey !== partKey) {
367
+ emit("content_block_delta", {
368
+ type: "content_block_delta", index: open!.index,
369
+ delta: { type: "thinking_delta", thinking: "\n\n" },
370
+ });
371
+ }
372
+ open!.reasoningPartKey = partKey;
356
373
  emit("content_block_delta", {
357
374
  type: "content_block_delta", index: open!.index,
358
375
  delta: { type: "thinking_delta", thinking: data.delta },
@@ -28,6 +28,17 @@ export type ClassifyResult = { type: AccountType } | { error: string };
28
28
 
29
29
  export type AccountStdin = NodeJS.ReadableStream & { isTTY?: boolean };
30
30
 
31
+ export interface NativeMainLoginChild {
32
+ exited: Promise<number>;
33
+ kill(signal?: number | NodeJS.Signals): void;
34
+ }
35
+
36
+ export interface StageLeaseClock {
37
+ now(): number;
38
+ setTimeout(callback: () => void, ms: number): ReturnType<typeof setTimeout>;
39
+ clearTimeout(timer: ReturnType<typeof setTimeout>): void;
40
+ }
41
+
31
42
  export interface AccountDeps {
32
43
  /** Test injection: skip findLiveProxy and call the API at this base URL. */
33
44
  baseUrl?: string;
@@ -35,6 +46,14 @@ export interface AccountDeps {
35
46
  loadConfigImpl?: () => OcxConfig;
36
47
  stdinImpl?: AccountStdin;
37
48
  stdinTimeoutMs?: number;
49
+ /** Test/platform injection for the official Codex login in a restricted staging home. */
50
+ spawnCodexLoginImpl?: (codexHome: string) => NativeMainLoginChild;
51
+ /** Legacy test seam. Production always uses the spawned child handle above. */
52
+ runCodexLoginImpl?: (codexHome: string) => Promise<number>;
53
+ /** Test-only heartbeat cadence floor. Production keeps the five-second floor. */
54
+ stageHeartbeatIntervalMinMs?: number;
55
+ /** Test-only clock for deterministic native-profile lease deadline coverage. */
56
+ stageLeaseClock?: StageLeaseClock;
38
57
  }
39
58
 
40
59
  export function classifyAccount(config: OcxConfig, name: string): ClassifyResult {
@@ -68,6 +87,7 @@ export async function apiJson(
68
87
  method: "GET" | "PUT" | "POST" | "DELETE",
69
88
  path: string,
70
89
  body?: unknown,
90
+ options: { signal?: AbortSignal } = {},
71
91
  ): Promise<ApiResult> {
72
92
  const fetchImpl = deps.fetchImpl ?? fetch;
73
93
  try {
@@ -75,6 +95,7 @@ export async function apiJson(
75
95
  method,
76
96
  headers: runningProxyUpdateHeaders(),
77
97
  body: body === undefined ? undefined : JSON.stringify(body),
98
+ signal: options.signal,
78
99
  });
79
100
  const json = (await res.json().catch(() => ({}))) as Record<string, unknown>;
80
101
  return { status: res.status, json };
@@ -98,6 +119,9 @@ export function proxyUnreachable(): number {
98
119
  export function apiError(json: Record<string, unknown>, fallback: string): number {
99
120
  const message = typeof json.error === "string" ? json.error : fallback;
100
121
  console.error(`Error: ${message}`);
122
+ if (json.cleanupRequired === true) {
123
+ console.error("Warning: native-login staging cleanup is still required; run 'ocx account main doctor'.");
124
+ }
101
125
  return 1;
102
126
  }
103
127
 
@@ -11,6 +11,23 @@ import {
11
11
  type CliStdin,
12
12
  type RuntimeApiDeps,
13
13
  } from "./runtime-api";
14
+ import { writeSync } from "node:fs";
15
+
16
+ /**
17
+ * Write the whole block to fd 1 synchronously (#1007). `console.log` can
18
+ * buffer behind a pipe, which hid the authorization URL for the entire
19
+ * polling window under non-TTY stdout. A partial write loops; a zero-byte
20
+ * write is a hard failure, never silent progress.
21
+ */
22
+ function writeStdoutFully(text: string): void {
23
+ const bytes = Buffer.from(text, "utf8");
24
+ let offset = 0;
25
+ while (offset < bytes.length) {
26
+ const written = writeSync(1, bytes, offset, bytes.length - offset);
27
+ if (written <= 0) throw new CliUsageError("failed to write login instructions to stdout");
28
+ offset += written;
29
+ }
30
+ }
14
31
 
15
32
  const USAGE = `Usage:
16
33
  ocx account login <provider> [--id <account-id>] [--reauth] [--code -] [--no-wait] [--json]
@@ -82,9 +99,14 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
82
99
  body: JSON.stringify({ ...(id ? { id } : {}), ...(reauth ? { reauth: true } : {}) }),
83
100
  }, deps);
84
101
  if (!wantsJson) {
85
- if (start.url) console.log(`Open this URL to sign in:\n${start.url}`);
86
- if (start.instructions) console.log(start.instructions);
87
- if (start.flowId) console.log(`Flow: ${start.flowId}`);
102
+ // One atomic pre-poll block, flushed synchronously so a piped parent
103
+ // reads the URL before the polling window starts (#1007).
104
+ const block = [
105
+ start.url ? `Open this URL to sign in:\n${start.url}` : "",
106
+ start.instructions ?? "",
107
+ start.flowId ? `Flow: ${start.flowId}` : "",
108
+ ].filter(line => line !== "").join("\n");
109
+ if (block) writeStdoutFully(`${block}\n`);
88
110
  }
89
111
  if (code && start.flowId) {
90
112
  await runtimeRequest("/api/codex-auth/login/code", {
@@ -120,9 +142,12 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
120
142
  body: JSON.stringify({ provider, addAccount: !reauth, ...(reauth && id ? { accountId: id, reauth: true } : {}) }),
121
143
  }, deps);
122
144
  if (!wantsJson) {
123
- if (start.url) console.log(`Open this URL to sign in:\n${start.url}`);
124
- if (start.instructions) console.log(start.instructions);
125
- if (start.deviceCode) console.log(`Device code: ${start.deviceCode}`);
145
+ const block = [
146
+ start.url ? `Open this URL to sign in:\n${start.url}` : "",
147
+ start.instructions ?? "",
148
+ start.deviceCode ? `Device code: ${start.deviceCode}` : "",
149
+ ].filter(line => line !== "").join("\n");
150
+ if (block) writeStdoutFully(`${block}\n`);
126
151
  }
127
152
  if (code) {
128
153
  await runtimeRequest("/api/oauth/login/code", {
@@ -0,0 +1,317 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { codexExecInvocation } from "../codex/exec-invocation";
3
+ import { resolveAndPersistCodexRuntime, type ResolveCodexRuntimeDeps } from "../codex/runtime";
4
+ import type { ResolveDeps, SpawnInvocation } from "../lib/win-exec";
5
+ import {
6
+ apiError,
7
+ apiJson,
8
+ proxyUnreachable,
9
+ resolveBaseUrl,
10
+ type AccountDeps,
11
+ type NativeMainLoginChild,
12
+ type StageLeaseClock,
13
+ } from "./account-api";
14
+
15
+ const USAGE = `Usage:
16
+ ocx account main doctor [--json]
17
+ ocx account main list [--json]
18
+ ocx account main register <label> [--json]
19
+ ocx account main add <label>
20
+ ocx account main switch <profile-id-or-label> --yes [--json]
21
+ ocx account main recover [--rollback --yes] [--json]
22
+
23
+ Native main login profiles change the physical Codex App/CLI login in the effective CODEX_HOME.
24
+ They are independent from the OpenCodex Pool selected by 'ocx account use openai'.`;
25
+
26
+ const STAGE_LEASE_SAFETY_MARGIN_MS = 30_000;
27
+
28
+ const systemStageLeaseClock: StageLeaseClock = {
29
+ now: Date.now,
30
+ setTimeout: (callback, ms) => setTimeout(callback, ms),
31
+ clearTimeout: timer => clearTimeout(timer),
32
+ };
33
+
34
+ interface PublicProfile {
35
+ id: string;
36
+ label: string;
37
+ identityHint: string;
38
+ state: "active" | "inactive";
39
+ }
40
+
41
+ export type NativeMainCodexLoginInvocationDeps =
42
+ & ResolveDeps
43
+ & Pick<ResolveCodexRuntimeDeps, "existsSync" | "execFileSync" | "configDir" | "readFileSync">;
44
+
45
+ function flag(args: string[], name: string): boolean {
46
+ const index = args.indexOf(name);
47
+ if (index < 0) return false;
48
+ args.splice(index, 1);
49
+ return true;
50
+ }
51
+
52
+ function reject(args: string[]): number {
53
+ if (args.length > 0) console.error(`Unexpected argument(s): ${args.join(", ")}`);
54
+ console.error(USAGE);
55
+ return 1;
56
+ }
57
+
58
+ function effectiveCodexHome(result: Record<string, unknown>): string {
59
+ return typeof result.effectiveCodexHome === "string" && result.effectiveCodexHome.length > 0
60
+ ? result.effectiveCodexHome
61
+ : "the effective CODEX_HOME";
62
+ }
63
+
64
+ function printProfiles(profiles: PublicProfile[]): void {
65
+ if (profiles.length === 0) { console.log("No native main login profiles registered."); return; }
66
+ const rows = profiles.map(profile => [profile.state === "active" ? "*" : " ", profile.label, profile.id, profile.identityHint]);
67
+ const header = ["", "LABEL", "PROFILE ID", "IDENTITY"];
68
+ const widths = header.map((value, index) => Math.max(value.length, ...rows.map(row => row[index]!.length)));
69
+ const line = (columns: string[]) => columns.map((value, index) => value.padEnd(widths[index]!)).join(" ").trimEnd();
70
+ console.log([line(header), ...rows.map(line)].join("\n"));
71
+ }
72
+
73
+ export function nativeMainCodexLoginInvocation(
74
+ platform: NodeJS.Platform = process.platform,
75
+ deps: NativeMainCodexLoginInvocationDeps = {},
76
+ ): SpawnInvocation {
77
+ const command = resolveAndPersistCodexRuntime({
78
+ env: deps.env ?? process.env,
79
+ platform,
80
+ existsSync: deps.existsSync,
81
+ execFileSync: deps.execFileSync,
82
+ configDir: deps.configDir,
83
+ readFileSync: deps.readFileSync,
84
+ discoverAlternatives: false,
85
+ }).runtime.command || "codex";
86
+ return codexExecInvocation(command, ["login"], platform, deps);
87
+ }
88
+
89
+ function spawnOfficialCodexLogin(codexHome: string): NativeMainLoginChild {
90
+ const invocation = nativeMainCodexLoginInvocation();
91
+ const child = Bun.spawn([invocation.file, ...invocation.args], {
92
+ env: { ...process.env, CODEX_HOME: codexHome },
93
+ stdin: "inherit",
94
+ stdout: "inherit",
95
+ stderr: "inherit",
96
+ ...(invocation.options.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
97
+ });
98
+ return child;
99
+ }
100
+
101
+ function abortableDelay(ms: number, signal: AbortSignal, clock: StageLeaseClock): Promise<void> {
102
+ if (signal.aborted) return Promise.resolve();
103
+ return new Promise(resolve => {
104
+ const timer = clock.setTimeout(done, ms);
105
+ signal.addEventListener("abort", done, { once: true });
106
+ function done(): void {
107
+ clock.clearTimeout(timer);
108
+ signal.removeEventListener("abort", done);
109
+ resolve();
110
+ }
111
+ });
112
+ }
113
+
114
+ async function maintainStageLease(
115
+ deps: AccountDeps,
116
+ baseUrl: string,
117
+ stageId: string,
118
+ writerToken: string,
119
+ initialLeaseExpiresAt: number,
120
+ heartbeatIntervalMs: number,
121
+ child: NativeMainLoginChild,
122
+ signal: AbortSignal,
123
+ clock: StageLeaseClock,
124
+ ): Promise<{ lost: boolean }> {
125
+ let leaseExpiresAt = initialLeaseExpiresAt;
126
+ for (;;) {
127
+ if (signal.aborted) return { lost: false };
128
+ const deadlineAt = leaseExpiresAt - STAGE_LEASE_SAFETY_MARGIN_MS;
129
+ if (clock.now() >= deadlineAt) {
130
+ try { child.kill(); } catch { /* child exit below is authoritative */ }
131
+ return { lost: true };
132
+ }
133
+
134
+ const heartbeatAbort = new AbortController();
135
+ const deadlineReached = Symbol("heartbeat-deadline");
136
+ const parentCancelled = Symbol("heartbeat-parent-cancelled");
137
+ let resolveInterruption!: (result: typeof deadlineReached | typeof parentCancelled) => void;
138
+ const interruption = new Promise<typeof deadlineReached | typeof parentCancelled>(resolve => {
139
+ resolveInterruption = resolve;
140
+ });
141
+ const cancelHeartbeat = () => {
142
+ heartbeatAbort.abort();
143
+ resolveInterruption(parentCancelled);
144
+ };
145
+ signal.addEventListener("abort", cancelHeartbeat, { once: true });
146
+ const deadlineTimer = clock.setTimeout(() => {
147
+ heartbeatAbort.abort();
148
+ resolveInterruption(deadlineReached);
149
+ }, Math.max(0, deadlineAt - clock.now()));
150
+ const heartbeatRequest = apiJson(
151
+ deps,
152
+ baseUrl,
153
+ "POST",
154
+ "/api/native-main-profiles/stage/heartbeat",
155
+ { stageId, writerToken },
156
+ { signal: heartbeatAbort.signal },
157
+ );
158
+ const heartbeat = await Promise.race([heartbeatRequest, interruption]);
159
+ clock.clearTimeout(deadlineTimer);
160
+ signal.removeEventListener("abort", cancelHeartbeat);
161
+ if (signal.aborted) return { lost: false };
162
+ if (heartbeat === parentCancelled) return { lost: false };
163
+ if (heartbeat === deadlineReached) {
164
+ try { child.kill(); } catch { /* child exit below is authoritative */ }
165
+ return { lost: true };
166
+ }
167
+ if (heartbeat.status === 200 && typeof heartbeat.json.leaseExpiresAt === "number") {
168
+ leaseExpiresAt = heartbeat.json.leaseExpiresAt;
169
+ await abortableDelay(Math.min(heartbeatIntervalMs, Math.max(0, leaseExpiresAt - STAGE_LEASE_SAFETY_MARGIN_MS - clock.now())), signal, clock);
170
+ continue;
171
+ }
172
+ if (heartbeat.status === 0 && clock.now() < leaseExpiresAt - STAGE_LEASE_SAFETY_MARGIN_MS) {
173
+ await abortableDelay(Math.min(5_000, heartbeatIntervalMs, leaseExpiresAt - STAGE_LEASE_SAFETY_MARGIN_MS - clock.now()), signal, clock);
174
+ continue;
175
+ }
176
+ try { child.kill(); } catch { /* child exit below is authoritative */ }
177
+ return { lost: true };
178
+ }
179
+ }
180
+
181
+ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): Promise<number> {
182
+ const sub = args.shift();
183
+ const wantsJson = flag(args, "--json");
184
+ const confirmed = flag(args, "--yes");
185
+ const rollback = flag(args, "--rollback");
186
+ const baseUrl = await resolveBaseUrl(deps);
187
+ if (!baseUrl) return proxyUnreachable();
188
+
189
+ if (sub === "doctor" || sub === "list") {
190
+ if (args.length > 0 || confirmed || rollback) return reject(args);
191
+ const path = sub === "doctor" ? "/api/native-main-profiles/doctor" : "/api/native-main-profiles";
192
+ const result = await apiJson(deps, baseUrl, "GET", path);
193
+ if (result.status === 0) return proxyUnreachable();
194
+ if (result.status !== 200) return apiError(result.json, `failed to ${sub} native profiles`);
195
+ if (wantsJson || sub === "doctor") console.log(JSON.stringify(result.json, null, 2));
196
+ else printProfiles(Array.isArray(result.json.profiles) ? result.json.profiles as PublicProfile[] : []);
197
+ return 0;
198
+ }
199
+
200
+ if (sub === "register") {
201
+ const label = args.shift();
202
+ if (!label || args.length > 0 || confirmed || rollback) return reject(args);
203
+ const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/register", { label });
204
+ if (result.status === 0) return proxyUnreachable();
205
+ if (result.status !== 200) return apiError(result.json, "failed to register the current native login");
206
+ if (wantsJson) console.log(JSON.stringify(result.json, null, 2));
207
+ else console.log(`Registered '${label}' for ${effectiveCodexHome(result.json)}.`);
208
+ return 0;
209
+ }
210
+
211
+ if (sub === "add") {
212
+ const label = args.shift();
213
+ if (!label || args.length > 0 || wantsJson || confirmed || rollback) return reject(args);
214
+ const stage = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/stage", {});
215
+ if (stage.status === 0) return proxyUnreachable();
216
+ if (stage.status !== 200) return apiError(stage.json, "failed to prepare native login staging");
217
+ const stageId = typeof stage.json.stageId === "string" ? stage.json.stageId : "";
218
+ const writerToken = typeof stage.json.writerToken === "string" ? stage.json.writerToken : "";
219
+ const stagingHome = typeof stage.json.stagingCodexHome === "string" ? stage.json.stagingCodexHome : "";
220
+ const leaseExpiresAt = typeof stage.json.leaseExpiresAt === "number" ? stage.json.leaseExpiresAt : 0;
221
+ const heartbeatIntervalMs = typeof stage.json.heartbeatIntervalMs === "number"
222
+ ? Math.min(5 * 60_000, Math.max(deps.stageHeartbeatIntervalMinMs ?? 5_000, stage.json.heartbeatIntervalMs))
223
+ : 60_000;
224
+ const stageLeaseClock = deps.stageLeaseClock ?? systemStageLeaseClock;
225
+ const stagedEffectiveHome = effectiveCodexHome(stage.json);
226
+ let exitCode = 1;
227
+ let finished = false;
228
+ let leaseLost = false;
229
+ try {
230
+ if (!stageId || !writerToken || !isAbsolute(stagingHome) || leaseExpiresAt <= stageLeaseClock.now()) {
231
+ throw new Error("The proxy returned an invalid staging session.");
232
+ }
233
+ console.error(`Effective CODEX_HOME: ${stagedEffectiveHome}`);
234
+ console.error(`Starting official Codex login in restricted staging home: ${stagingHome}`);
235
+ const child = deps.spawnCodexLoginImpl
236
+ ? deps.spawnCodexLoginImpl(stagingHome)
237
+ : deps.runCodexLoginImpl
238
+ ? { exited: deps.runCodexLoginImpl(stagingHome), kill: () => {} }
239
+ : spawnOfficialCodexLogin(stagingHome);
240
+ const heartbeatAbort = new AbortController();
241
+ const heartbeat = maintainStageLease(
242
+ deps,
243
+ baseUrl,
244
+ stageId,
245
+ writerToken,
246
+ leaseExpiresAt,
247
+ heartbeatIntervalMs,
248
+ child,
249
+ heartbeatAbort.signal,
250
+ stageLeaseClock,
251
+ );
252
+ try {
253
+ exitCode = await child.exited;
254
+ } finally {
255
+ heartbeatAbort.abort();
256
+ leaseLost = (await heartbeat).lost;
257
+ }
258
+ if (leaseLost) throw new Error("The native-login staging lease was lost before login completed.");
259
+ if (exitCode !== 0) throw new Error("Official Codex login did not complete successfully.");
260
+ const finish = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/stage/finish", { stageId, writerToken, label });
261
+ if (finish.status === 0) return proxyUnreachable();
262
+ if (finish.status !== 200) return apiError(finish.json, "failed to encrypt the staged native login");
263
+ finished = true;
264
+ console.log(`Added encrypted native profile '${label}' for ${effectiveCodexHome(finish.json)}.`);
265
+ return 0;
266
+ } catch (error) {
267
+ console.error(`Error: ${error instanceof Error ? error.message : "Official Codex login failed."}`);
268
+ return exitCode === 0 ? 1 : exitCode;
269
+ } finally {
270
+ if (stageId && !finished) {
271
+ const cleanup = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/stage/cancel", { stageId, writerToken });
272
+ if (cleanup.status !== 200 || cleanup.json.plaintextMayRemain === true) {
273
+ console.error("Error: the proxy could not confirm native-login staging cleanup; run account main doctor.");
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ if (sub === "switch") {
280
+ const target = args.shift();
281
+ if (!target || args.length > 0 || rollback || !confirmed) {
282
+ if (!confirmed) console.error("Close Codex App/CLI, then pass --yes to confirm it is stopped.");
283
+ return reject(args);
284
+ }
285
+ const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/switch", { target, confirmedStopped: true });
286
+ if (result.status === 0) return proxyUnreachable();
287
+ if (result.status !== 200) return apiError(result.json, "failed to switch the native login");
288
+ if (wantsJson) console.log(JSON.stringify(result.json, null, 2));
289
+ else {
290
+ const profile = result.json.activeProfile as PublicProfile | undefined;
291
+ console.log(`Native Codex login for ${effectiveCodexHome(result.json)} is now '${profile?.label ?? target}'. Restart Codex App/CLI before continuing.`);
292
+ }
293
+ return 0;
294
+ }
295
+
296
+ if (sub === "recover") {
297
+ if (args.length > 0 || (rollback && !confirmed)) {
298
+ if (rollback && !confirmed) console.error("--rollback changes the native login and requires --yes.");
299
+ return reject(args);
300
+ }
301
+ const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/recover", rollback
302
+ ? { rollback: true, confirmedStopped: true }
303
+ : { rollback: false });
304
+ if (result.status === 0) return proxyUnreachable();
305
+ if (result.status !== 200) return apiError(result.json, "failed to recover the native-profile transaction");
306
+ if (wantsJson) console.log(JSON.stringify(result.json, null, 2));
307
+ else {
308
+ const home = effectiveCodexHome(result.json);
309
+ console.log(result.json.recovered === true
310
+ ? `Recovery completed for ${home}: ${String(result.json.action ?? "converged")}.`
311
+ : `No recovery journal is pending for ${home}.`);
312
+ }
313
+ return 0;
314
+ }
315
+
316
+ return reject(args);
317
+ }
@@ -29,6 +29,7 @@ const ACCOUNT_USAGE = `Usage:
29
29
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
30
30
  ocx account cancel <provider> [--flow <flow-id>] [--json]
31
31
  ocx account reset-credits <account-id|main> [--consume --yes] [--json]
32
+ ocx account main <doctor|list|register|add|switch|recover> ...
32
33
 
33
34
  List and switch provider accounts and API-key pools (masked output only).
34
35
  'main' selects the Codex App login for the openai account pool.`;
@@ -262,6 +263,10 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
262
263
  if (sub === "remove") return await cmdRemove(rest, deps);
263
264
  if (sub === "clear-cooldown") return await cmdClearCooldown(rest, deps);
264
265
  if (sub === "add-key") return await cmdAddKey(rest, deps);
266
+ if (sub === "main") {
267
+ const { cmdNativeMainAccount } = await import("./account-main");
268
+ return await cmdNativeMainAccount(rest, deps);
269
+ }
265
270
  if (["login", "reauth", "code", "cancel", "reset-credits"].includes(sub ?? "")) {
266
271
  const { handleAccountAuthCommand } = await import("./account-auth");
267
272
  return await handleAccountAuthCommand(sub!, rest, deps) ?? 1;
package/src/cli/claude.ts CHANGED
@@ -17,6 +17,7 @@ import type { OcxConfig } from "../types";
17
17
  import { configuredAdminToken } from "../lib/admin-secrets";
18
18
  import { PROXY_MARKER, ownAdmissionTokens, defaultAuthDetectDeps, detectClaudeAuth, type AuthDetectDeps } from "../claude/auth-detect";
19
19
  import { resolveClaudeAuthMode } from "../claude/auth-mode";
20
+ import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
20
21
 
21
22
  export interface ClaudeLaunchEnv {
22
23
  [key: string]: string | undefined;
@@ -205,7 +206,7 @@ async function ensureProxyForClaude(): Promise<number | null> {
205
206
  detached: true,
206
207
  stdio: "ignore",
207
208
  windowsHide: true,
208
- env: { ...process.env, OCX_SERVICE: "1" },
209
+ env: withProcessRuntimeProvenance({ ...process.env, OCX_SERVICE: "1" }),
209
210
  });
210
211
  child.unref();
211
212
  const deadline = Date.now() + 8_000;