@bitkyc08/opencodex 2.58.0 → 2.60.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
@@ -107,66 +107,96 @@ function usesUnicodePropertyEscape(pattern: string): boolean {
107
107
  * preserved: loosening a nested constraint can instead reject an input in those contexts.
108
108
  * Unsupported patterns there remain the destination's validation responsibility.
109
109
  *
110
- * Returns `node` itself when nothing was dropped. Uses an explicit stack for caller-controlled
111
- * nesting depth; the separate Responses-only encrypted-marker normalization is unchanged.
110
+ * Returns `node` itself when nothing was dropped. Traversal keeps only the active path and clones
111
+ * only ancestors of a removed constraint, so a broad no-op schema does not create an output tree
112
+ * or one pending closure per sibling. The explicit stack still handles caller-controlled nesting
113
+ * depth; the separate Responses-only encrypted-marker normalization is unchanged.
112
114
  */
113
115
  export function stripUnicodePropertyPatterns(node: unknown, inNameBag = false): unknown {
114
- type Assign = (value: unknown) => void;
115
- interface Frame { node: unknown; inNameBag: boolean; assign: Assign }
116
-
117
- let result: unknown;
118
- let dropped = 0;
119
- const stack: Frame[] = [{ node, inNameBag, assign: value => { result = value; } }];
116
+ interface Frame {
117
+ node: unknown[] | Record<string, unknown>;
118
+ inNameBag: boolean;
119
+ parent?: Frame;
120
+ parentKey?: string | number;
121
+ output?: unknown[] | Record<string, unknown>;
122
+ index?: number;
123
+ entries?: IterableIterator<[string, unknown]>;
124
+ }
120
125
 
121
- while (stack.length > 0) {
122
- const frame = stack.pop()!;
123
- const current = frame.node;
126
+ function * ownEntries(value: Record<string, unknown>): IterableIterator<[string, unknown]> {
127
+ // Unlike Object.entries(), this does not materialize every key/value pair before traversal.
128
+ for (const key in value) {
129
+ if (Object.prototype.hasOwnProperty.call(value, key)) yield [key, value[key]];
130
+ }
131
+ }
124
132
 
125
- if (Array.isArray(current)) {
126
- const out: unknown[] = new Array(current.length);
127
- frame.assign(out);
128
- // Array items are schemas in their own right, never a name bag.
129
- for (let i = current.length - 1; i >= 0; i--) {
130
- stack.push({ node: current[i], inNameBag: false, assign: value => { out[i] = value; } });
131
- }
132
- continue;
133
+ function cloneContainer(frame: Frame): unknown[] | Record<string, unknown> {
134
+ if (frame.output) return frame.output;
135
+ if (Array.isArray(frame.node)) {
136
+ frame.output = frame.node.slice();
137
+ return frame.output;
133
138
  }
134
- if (!current || typeof current !== "object") {
135
- frame.assign(current);
136
- continue;
139
+ const output: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
140
+ for (const key in frame.node) {
141
+ if (Object.prototype.hasOwnProperty.call(frame.node, key)) output[key] = frame.node[key];
137
142
  }
143
+ frame.output = output;
144
+ return output;
145
+ }
138
146
 
139
- // A schema name may be `__proto__`; a null-prototype record keeps it as data.
140
- const out: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
141
- frame.assign(out);
147
+ function finish(frame: Frame): void {
148
+ if (!frame.output || !frame.parent) return;
149
+ const parent = cloneContainer(frame.parent);
150
+ if (Array.isArray(parent)) parent[frame.parentKey as number] = frame.output;
151
+ else parent[frame.parentKey as string] = frame.output;
152
+ }
142
153
 
143
- for (const [key, value] of Object.entries(current as Record<string, unknown>)) {
144
- if (frame.inNameBag) {
145
- // Inside a name bag every key is a caller-chosen name, so `pattern` here is a property
146
- // name; its value is still a schema and is walked as one.
147
- stack.push({ node: value, inNameBag: false, assign: v => { out[key] = v; } });
148
- continue;
149
- }
150
- if (PRESERVED_PATTERN_SUBTREES.has(key)) {
151
- out[key] = value;
152
- continue;
153
- }
154
- if (key === "pattern" && typeof value === "string" && usesUnicodePropertyEscape(value)) {
155
- dropped++;
154
+ if (!node || typeof node !== "object") return node;
155
+ const root: Frame = { node: node as unknown[] | Record<string, unknown>, inNameBag };
156
+ const stack: Frame[] = [root];
157
+
158
+ while (stack.length > 0) {
159
+ const frame = stack[stack.length - 1]!;
160
+
161
+ if (Array.isArray(frame.node)) {
162
+ const index = frame.index ?? 0;
163
+ if (index >= frame.node.length) {
164
+ stack.pop();
165
+ finish(frame);
156
166
  continue;
157
167
  }
158
- if (SCHEMA_LITERAL_VALUE_KEYS.has(key)) {
159
- // Literal payloads are values, not schemas: a `pattern` key inside them is data.
160
- out[key] = value;
161
- continue;
168
+ frame.index = index + 1;
169
+ const child = frame.node[index];
170
+ if (child && typeof child === "object") {
171
+ stack.push({ node: child as unknown[] | Record<string, unknown>, inNameBag: false, parent: frame, parentKey: index });
162
172
  }
173
+ continue;
174
+ }
175
+
176
+ frame.entries ??= ownEntries(frame.node);
177
+ const next = frame.entries.next();
178
+ if (next.done) {
179
+ stack.pop();
180
+ finish(frame);
181
+ continue;
182
+ }
183
+ const [key, value] = next.value;
184
+ if (!frame.inNameBag && key === "pattern" && typeof value === "string" && usesUnicodePropertyEscape(value)) {
185
+ delete (cloneContainer(frame) as Record<string, unknown>)[key];
186
+ continue;
187
+ }
188
+ if (!frame.inNameBag && (PRESERVED_PATTERN_SUBTREES.has(key) || SCHEMA_LITERAL_VALUE_KEYS.has(key))) {
189
+ continue;
190
+ }
191
+ if (value && typeof value === "object") {
163
192
  stack.push({
164
- node: value,
165
- inNameBag: SCHEMA_NAME_BAG_KEYS.has(key),
166
- assign: v => { out[key] = v; },
193
+ node: value as unknown[] | Record<string, unknown>,
194
+ inNameBag: !frame.inNameBag && SCHEMA_NAME_BAG_KEYS.has(key),
195
+ parent: frame,
196
+ parentKey: key,
167
197
  });
168
198
  }
169
199
  }
170
200
 
171
- return dropped === 0 ? node : result;
201
+ return root.output ?? node;
172
202
  }
@@ -28,6 +28,7 @@ export interface AdapterEventPreflight {
28
28
  stream: AsyncIterable<AdapterEvent>;
29
29
  error?: Extract<AdapterEvent, { type: "error" }>;
30
30
  empty: boolean;
31
+ replayUnsafe: boolean;
31
32
  }
32
33
 
33
34
  async function* replay(
@@ -51,10 +52,12 @@ export async function preflightAdapterEvents(
51
52
  ): Promise<AdapterEventPreflight> {
52
53
  const iterator = source[Symbol.asyncIterator]();
53
54
  const buffered: AdapterEvent[] = [];
55
+ let replayUnsafe = false;
54
56
  while (true) {
55
57
  const next = await iterator.next();
56
- if (next.done) return { stream: replay(buffered, iterator), empty: true };
58
+ if (next.done) return { stream: replay(buffered, iterator), empty: true, replayUnsafe };
57
59
  if (next.value.type === "heartbeat") {
60
+ replayUnsafe ||= next.value.replayUnsafe === true;
58
61
  buffered.push(next.value);
59
62
  if (buffered.length > PREFLIGHT_HEARTBEAT_RETAIN_LIMIT) buffered.shift();
60
63
  continue;
@@ -62,9 +65,9 @@ export async function preflightAdapterEvents(
62
65
  buffered.push(next.value);
63
66
  if (next.value.type === "error") {
64
67
  await iterator.return?.();
65
- return { stream: replay(buffered, iterator), error: next.value, empty: false };
68
+ return { stream: replay(buffered, iterator), error: next.value, empty: false, replayUnsafe };
66
69
  }
67
- return { stream: replay(buffered, iterator), empty: false };
70
+ return { stream: replay(buffered, iterator), empty: false, replayUnsafe };
68
71
  }
69
72
  }
70
73
 
@@ -90,7 +93,17 @@ export function createAdapterEventQueue(opts?: {
90
93
  const coalesceIntoTail = (event: AdapterEvent): boolean => {
91
94
  const tail = queued[queued.length - 1];
92
95
  if (!tail) return false;
93
- if (event.type === "heartbeat") return tail.type === "heartbeat";
96
+ if (event.type === "heartbeat") {
97
+ if (tail.type !== "heartbeat") return false;
98
+ // Heartbeats carry no ordering between themselves, but the replay-unsafe
99
+ // marker is not ordering — it is a latch. Dropping the incoming event
100
+ // would discard the only record that Cursor already performed a local
101
+ // side effect, and preflight would then permit an OAuth replay of it.
102
+ if (event.replayUnsafe === true && tail.replayUnsafe !== true) {
103
+ queued[queued.length - 1] = { type: "heartbeat", replayUnsafe: true };
104
+ }
105
+ return true;
106
+ }
94
107
  if (event.type === "text_delta" && tail.type === "text_delta" && tail.phase === event.phase) {
95
108
  if (tail.text.length + event.text.length > COALESCE_MAX_CHUNK_LENGTH) return false;
96
109
  queued[queued.length - 1] = { type: "text_delta", text: tail.text + event.text, phase: tail.phase };
@@ -203,7 +203,7 @@ function buildResponseJSONWithBudget(
203
203
  ): string => {
204
204
  const helper = resolveCodeModeHelperName(codeModeHelperName, toolName, args, namespace, options?.declaredToolNames);
205
205
  return helper
206
- ? compileCodeModeHelperInput(args, helper)
206
+ ? compileCodeModeHelperInput(args, helper, codeModeHelperName ?? toolName)
207
207
  : repairFreeformToolInput(args, toolName, namespace);
208
208
  };
209
209
  const parseArgsObj = (args: string): Record<string, unknown> => {
package/src/bridge/sse.ts CHANGED
@@ -16,7 +16,11 @@ import {
16
16
  type OcxErrorPayload,
17
17
  } from "../lib/errors";
18
18
  import { redactSecretString } from "../lib/redact";
19
- import { mayBecomePatchEnvelope, repairFreeformToolInput } from "../responses/apply-patch-envelope";
19
+ import {
20
+ mayBecomePatchEnvelope,
21
+ repairFreeformToolInput,
22
+ } from "../responses/apply-patch-envelope";
23
+ import { progressiveFreeformInput } from "../responses/progressive-freeform-input";
20
24
  import { encodeCompactionSummary } from "../responses/compaction";
21
25
  import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
22
26
  import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
@@ -145,37 +149,9 @@ export function bridgeToResponsesSSE(
145
149
  ): string => {
146
150
  const helper = resolveCodeModeHelperName(codeModeHelperName, toolName, args, namespace, options?.declaredToolNames);
147
151
  return helper
148
- ? compileCodeModeHelperInput(args, helper)
152
+ ? compileCodeModeHelperInput(args, helper, codeModeHelperName ?? toolName)
149
153
  : repairFreeformToolInput(args, toolName, namespace);
150
154
  };
151
- // Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
152
- // (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
153
- // the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
154
- // buffers get their string value progressively unescaped; anything else streams raw.
155
- const FREEFORM_WRAP_PREFIX = '{"input":"';
156
- const freeformPartialInput = (args: string): string => {
157
- if (!args.startsWith(FREEFORM_WRAP_PREFIX)) return args;
158
- const body = args.slice(FREEFORM_WRAP_PREFIX.length);
159
- let out = "";
160
- for (let i = 0; i < body.length; i++) {
161
- const c = body[i];
162
- if (c === '"') break; // unescaped closing quote: value complete
163
- if (c === "\\") {
164
- const n = body[i + 1];
165
- if (n === undefined) break; // escape split across chunks: wait for more
166
- i++;
167
- if (n === "n") out += "\n";
168
- else if (n === "t") out += "\t";
169
- else if (n === "r") out += "\r";
170
- else if (n === "u") {
171
- const hex = body.slice(i + 1, i + 5);
172
- if (hex.length === 4 && /^[0-9a-fA-F]{4}$/.test(hex)) { out += String.fromCharCode(parseInt(hex, 16)); i += 4; }
173
- else break; // incomplete \uXXXX: wait for more
174
- } else out += n; // \" \\ \/ etc.
175
- } else out += c;
176
- }
177
- return out;
178
- };
179
155
  // tool_search_call carries arguments as a JSON object ({query, limit}); parse the model's arg string.
180
156
  const parseArgsObj = (args: string): Record<string, unknown> => {
181
157
  try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
@@ -1075,10 +1051,21 @@ export function bridgeToResponsesSSE(
1075
1051
  });
1076
1052
  }
1077
1053
  if (currentToolCall.freeform && !currentToolCall.codeModeHelperName) {
1078
- // Hold while the buffer is still an ambiguous prefix of the JSON wrapper,
1079
- // then stream only the unwrapped input suffix (never rewind on mode flips).
1080
- if (!FREEFORM_WRAP_PREFIX.startsWith(currentToolCall.args)) {
1081
- const full = freeformPartialInput(currentToolCall.args);
1054
+ // `progressiveFreeformInput` holds while the buffer is still an ambiguous prefix
1055
+ // of a JSON wrapper; otherwise stream only the unwrapped input suffix, never
1056
+ // rewinding on a mode flip.
1057
+ //
1058
+ // The name is dropped for a namespaced tool that does not own the apply-patch
1059
+ // grammar, because `repairFreeformToolInput` drops it at completion for the
1060
+ // same reason. Streaming under a vocabulary the completed item does not use
1061
+ // is the same disagreement in the other direction.
1062
+ const ownsFreeformGrammar = currentToolCall.namespace === undefined
1063
+ || currentToolCall.namespace === "functions";
1064
+ const full = progressiveFreeformInput(
1065
+ currentToolCall.args,
1066
+ ownsFreeformGrammar ? currentToolCall.name : "",
1067
+ );
1068
+ if (full !== null) {
1082
1069
  const emitted = currentToolCall.inputEmitted ?? "";
1083
1070
  // Also hold a buffer that could still become a complete patch envelope:
1084
1071
  // at completion such a body is recompiled into an apply_patch helper call,
@@ -1086,7 +1073,13 @@ export function bridgeToResponsesSSE(
1086
1073
  const mayCompile = declaresCodeModeExec(options?.declaredToolNames)
1087
1074
  && !currentToolCall.namespace
1088
1075
  && currentToolCall.name === "exec";
1089
- if (!(mayCompile && mayBecomePatchEnvelope(full)) && full.startsWith(emitted) && full.length > emitted.length) {
1076
+ // `apply_patch` holds for a different reason with the same shape:
1077
+ // `normalizeApplyPatchDelimiters` rewrites a decorated `*** Begin Patch ***`
1078
+ // envelope at completion, so streaming the decorated markers would be
1079
+ // replaced by the normalized ones.
1080
+ const mayNormalize = ownsFreeformGrammar && currentToolCall.name === "apply_patch";
1081
+ if (!((mayCompile || mayNormalize) && mayBecomePatchEnvelope(full))
1082
+ && full.startsWith(emitted) && full.length > emitted.length) {
1090
1083
  emit("response.custom_tool_call_input.delta", {
1091
1084
  item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
1092
1085
  delta: full.slice(emitted.length),
@@ -86,6 +86,28 @@ export function resolveAutoContext(claudeCode: AutoContextConfigSlice | undefine
86
86
  return { enabled: true, compactWindow };
87
87
  }
88
88
 
89
+ /**
90
+ * Config value -> ENABLE_TOOL_SEARCH wire value (#4838).
91
+ *
92
+ * Claude Code parses this variable itself and accepts more than a boolean:
93
+ * "true", "auto", "auto:N" (N is a percentage floor for non-deferred tools; its
94
+ * parser rejects anything under 100) and "force", which also overrides an
95
+ * OS-level managed-settings suppression. Passing a string through verbatim keeps
96
+ * that whole vocabulary reachable instead of flattening it to a boolean that
97
+ * would have to be re-expanded here later.
98
+ *
99
+ * `false`, absent and blank inject nothing rather than injecting "false". Every
100
+ * caller injects with user-wins semantics, so an operator's own export survives
101
+ * either way, and writing "false" would be the one path that could quietly
102
+ * disagree with it.
103
+ */
104
+ export function claudeToolSearchEnv(value: boolean | string | undefined): string | undefined {
105
+ if (value === true) return "true";
106
+ if (typeof value !== "string") return undefined;
107
+ const trimmed = value.trim();
108
+ return trimmed.length > 0 ? trimmed : undefined;
109
+ }
110
+
89
111
  /**
90
112
  * [1m]-marking predicate. Windows >= 1M always mark (CLI accounts exactly 1M).
91
113
  * Auto-context additionally marks windows > 200k that can safely host the compact
@@ -189,7 +189,27 @@ function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resultConte
189
189
  return { id, input, resultContent, completed };
190
190
  }
191
191
 
192
- function messageSnapshot(model: string, confirmedUsage?: Rec): Rec {
192
+ /**
193
+ * `inputTokenFloor` is this proxy's own count of the prompt it forwarded, used only when the
194
+ * upstream sent no confirmed usage before the first frame.
195
+ *
196
+ * Real Anthropic fills `message_start.message.usage.input_tokens` with the turn's prompt size,
197
+ * and third-party clients read it there — Paseo's context meter takes input from this frame and
198
+ * output from `message_delta`, so a hardcoded zero showed a nearly-empty ring for a turn whose
199
+ * `/context` reported ~97k (#4857). #4891 fixed the destinations that report usage up front;
200
+ * the internal bridge attaches `usage: null` to its lifecycle frames, so those paths had
201
+ * nothing to report and kept sending zero.
202
+ *
203
+ * A floor is a measurement, which is what makes it publishable here: it counts the prompt this
204
+ * proxy actually sent, the same estimate `claude-messages.ts` already trusts as a log floor. It
205
+ * is not a claim about upstream's tokenizer, and it is not final — `message_delta` carries the
206
+ * authoritative count for every reader that waits for it, exactly as before.
207
+ */
208
+ function messageSnapshot(model: string, confirmedUsage?: Rec, inputTokenFloor?: number): Rec {
209
+ const usage = confirmedUsage
210
+ ?? (typeof inputTokenFloor === "number" && Number.isFinite(inputTokenFloor) && inputTokenFloor > 0
211
+ ? { input_tokens: Math.trunc(inputTokenFloor), output_tokens: 0 }
212
+ : { input_tokens: 0, output_tokens: 0 });
193
213
  return {
194
214
  id: `msg_${uuid()}`,
195
215
  type: "message",
@@ -198,7 +218,7 @@ function messageSnapshot(model: string, confirmedUsage?: Rec): Rec {
198
218
  model,
199
219
  stop_reason: null,
200
220
  stop_sequence: null,
201
- usage: confirmedUsage ?? { input_tokens: 0, output_tokens: 0 },
221
+ usage,
202
222
  };
203
223
  }
204
224
 
@@ -228,7 +248,15 @@ interface OpenBlock {
228
248
  export function responsesSseToAnthropicSse(
229
249
  upstream: ReadableStream<Uint8Array>,
230
250
  model: string,
231
- opts: { pingIntervalMs?: number; translatorBudget: TranslatorBudget },
251
+ opts: {
252
+ pingIntervalMs?: number;
253
+ translatorBudget: TranslatorBudget;
254
+ /**
255
+ * This proxy's count of the prompt it forwarded, published on `message_start` only when the
256
+ * upstream sent no confirmed usage before the first frame. See `messageSnapshot` (#4857).
257
+ */
258
+ inputTokenFloor?: number;
259
+ },
232
260
  ): ReadableStream<Uint8Array> {
233
261
  const translatorBudget = opts.translatorBudget;
234
262
  const pingIntervalMs = opts?.pingIntervalMs ?? 20_000;
@@ -283,7 +311,10 @@ export function responsesSseToAnthropicSse(
283
311
  const ensureStarted = () => {
284
312
  if (started) return;
285
313
  started = true;
286
- emit("message_start", { type: "message_start", message: messageSnapshot(model, earlyAnthropicUsage) });
314
+ emit("message_start", {
315
+ type: "message_start",
316
+ message: messageSnapshot(model, earlyAnthropicUsage, opts.inputTokenFloor),
317
+ });
287
318
  emit("ping", { type: "ping" });
288
319
  };
289
320
  // Keepalive pings protect remote deployments behind LB/NAT idle timeouts even
@@ -370,9 +370,10 @@ async function fetchOAuthRows(
370
370
  email: a.email,
371
371
  active: a.active ?? a.id === activeId,
372
372
  needsReauth: a.needsReauth,
373
- // Forward the server's answer verbatim, including `null`. Collapsing null to "absent" here
374
- // would destroy the one distinction this field exists to make.
375
- plan: a.plan ?? null,
373
+ // Forward the server's answer verbatim. An absent key means the proxy predates tier
374
+ // reporting while `null` means it checked and found no tier — collapsing either
375
+ // direction would destroy the one distinction this field exists to make.
376
+ ...(Object.hasOwn(a, "plan") ? { plan: a.plan } : {}),
376
377
  ...(a.quota !== undefined ? { quota: a.quota } : {}),
377
378
  ...(a.quotaUnavailable !== undefined ? { quotaUnavailable: a.quotaUnavailable } : {}),
378
379
  ...(a.quotaUnavailable === true && parseQuotaFailureCode(a.quotaFailure)
@@ -403,16 +403,16 @@ export async function cmdAutoSwitch(args: string[], deps: AccountDeps): Promise<
403
403
  // shared kernel, and absent is a server that does not speak this field at all. Collapsing
404
404
  // false into absent would render the live feature as an unknown capability.
405
405
  const inert = typeof settings.inert === "boolean" ? settings.inert : null;
406
- // A stored threshold only steers selection once the pool consumes it, which is exactly
407
- // what `inert: false` reports.
408
- const enabled = inert === false && storedThreshold !== null;
406
+ // A positive stored threshold only steers selection once the pool consumes it, which is
407
+ // exactly what `inert: false` reports. Zero remains the explicit disabled value.
408
+ const enabled = inert === false && storedThreshold !== null && storedThreshold > 0;
409
409
  if (wantsJson) {
410
410
  console.log(JSON.stringify({ provider: name, autoSwitchThreshold: storedThreshold, enabled, poolEnabled, inert }, null, 2));
411
411
  } else {
412
412
  const value = storedThreshold === null ? "unset" : `${storedThreshold}%`;
413
413
  const state = inert === false ? (enabled ? "on" : "off") : inert === true ? "inactive" : "unavailable";
414
414
  const why = inert === false
415
- ? (enabled ? "applied by this pool" : "no threshold stored")
415
+ ? (enabled ? "applied by this pool" : storedThreshold === 0 ? "usage-based switching disabled" : "no threshold stored")
416
416
  : inert === true ? "not applied by this pool" : "threshold support is unknown";
417
417
  console.log(`auto-switch: ${state} (stored threshold ${value}; ${why})`);
418
418
  }
@@ -925,12 +925,32 @@ async function poolSetting(
925
925
  if (response.status !== 200) return apiError(response.json, `failed to read ${label}`, response.status);
926
926
  const strategy = response.json[transport.strategyKey];
927
927
  const sticky = response.json[transport.stickyKey];
928
+ const autoSwitchThreshold = typeof response.json.autoSwitchThreshold === "number"
929
+ ? response.json.autoSwitchThreshold
930
+ : undefined;
928
931
  if (wantsJson) {
929
932
  // Pool-neutral key names: the two routes spell the same two settings differently, and a
930
933
  // `--json` consumer should not have to branch on which pool answered.
931
- console.log(JSON.stringify({ ok: true, provider: name, strategy, stickyLimit: sticky }, null, 2));
934
+ const payload: Record<string, unknown> = { ok: true, provider: name, strategy, stickyLimit: sticky };
935
+ if (autoSwitchThreshold !== undefined) {
936
+ payload.autoSwitchThreshold = autoSwitchThreshold;
937
+ }
938
+ console.log(JSON.stringify(payload, null, 2));
932
939
  } else {
933
- console.log(`${name}: ${label} is ${String(field === "strategy" ? strategy : sticky)}`);
940
+ if (field === "strategy" && autoSwitchThreshold !== undefined) {
941
+ const thresholdSummary = strategy === "round-robin"
942
+ ? "threshold not used"
943
+ : autoSwitchThreshold > 0
944
+ ? (strategy === "fill-first"
945
+ ? `drain at ${autoSwitchThreshold}%`
946
+ : strategy === "reset-first"
947
+ ? `nearest reset below ${autoSwitchThreshold}%`
948
+ : `switch at ${autoSwitchThreshold}%`)
949
+ : "proactive switching off";
950
+ console.log(`${name}: ${label} is ${String(strategy)} (${thresholdSummary})`);
951
+ } else {
952
+ console.log(`${name}: ${label} is ${String(field === "strategy" ? strategy : sticky)}`);
953
+ }
934
954
  }
935
955
  return 0;
936
956
  }
@@ -0,0 +1,63 @@
1
+ import type { importOrcaAccounts } from "../codex/orca-import";
2
+
3
+ const USAGE = "Usage: ocx account import-orca --source <orca-data-directory> --registry <orca-data.json> [--apply] [--json]";
4
+
5
+ export interface OrcaImportCommandDeps {
6
+ importAccounts?: typeof importOrcaAccounts;
7
+ }
8
+
9
+ /** Local-only: never sends source paths or credentials to a management listener. */
10
+ export async function cmdOrcaImport(args: string[], deps: OrcaImportCommandDeps = {}): Promise<number> {
11
+ let sourceDir: string | undefined;
12
+ let registryPath: string | undefined;
13
+ let apply = false;
14
+ const wantsJson = args.includes("--json");
15
+ const seen = new Set<string>();
16
+ for (let index = 0; index < args.length; index++) {
17
+ const arg = args[index]!;
18
+ if (seen.has(arg) || !["--source", "--registry", "--apply", "--json"].includes(arg)) {
19
+ console.error(USAGE);
20
+ return 1;
21
+ }
22
+ seen.add(arg);
23
+ if (arg === "--source" || arg === "--registry") {
24
+ const value = args[++index];
25
+ if (!value || value.startsWith("--")) {
26
+ console.error(USAGE);
27
+ return 1;
28
+ }
29
+ if (arg === "--source") sourceDir = value;
30
+ else registryPath = value;
31
+ } else if (arg === "--apply") apply = true;
32
+ }
33
+ if (!sourceDir || !registryPath) {
34
+ console.error(USAGE);
35
+ return 1;
36
+ }
37
+ try {
38
+ const run = deps.importAccounts ?? (await import("../codex/orca-import")).importOrcaAccounts;
39
+ const result = await run({ sourceDir, registryPath, apply });
40
+ if (wantsJson) console.log(JSON.stringify({
41
+ mode: result.mode, discovered: result.discovered, eligible: result.eligible,
42
+ imported: result.imported, duplicates: result.duplicates, invalid: result.invalid,
43
+ invalidReasons: result.invalidReasons,
44
+ }));
45
+ else {
46
+ console.log(`Orca: ${result.discovered} discovered, ${result.eligible} eligible, ${result.imported} imported, ${result.duplicates} duplicates, ${result.invalid} invalid.`);
47
+ if (result.invalid > 0) {
48
+ const reasons = Object.entries(result.invalidReasons).map(([reason, count]) => `${reason}=${count}`).join(", ");
49
+ console.log(`Invalid reasons: ${reasons}`);
50
+ }
51
+ console.log(apply
52
+ ? "Start the proxy and use Refresh quotas in Codex Auth to validate new accounts. Orca retains refresh ownership."
53
+ : "Preview only. Stop the proxy before repeating with --apply. Orca authentication files stay read-only.");
54
+ }
55
+ return result.invalid > 0 && result.eligible === 0 ? 1 : 0;
56
+ } catch {
57
+ // Filesystem/JSON exceptions may contain source paths or token fragments.
58
+ const error = "Orca import failed. Check the source, destination config, and stopped proxy; no successful completion is confirmed.";
59
+ if (wantsJson) console.log(JSON.stringify({ error: "orca_import_failed" }));
60
+ else console.error(error);
61
+ return 1;
62
+ }
63
+ }
@@ -57,6 +57,7 @@ const ACCOUNT_USAGE = `Usage:
57
57
  ocx account clear-cooldown <provider> <account-id|main> [--json]
58
58
  ocx account add-key <provider> [--label <label>] [--json]
59
59
  ocx account import <provider> --format <format> (--file <path>|--stdin) [--json]
60
+ ocx account import-orca --source <orca-data-directory> --registry <orca-data.json> [--apply] [--json]
60
61
  ocx account login <provider> [--id <account-id>] [--reauth] [--code -] [--no-wait] [--json]
61
62
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
62
63
  ocx account cancel <provider> [--flow <flow-id>] [--json]
@@ -323,13 +324,36 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
323
324
  if (res.status === 0) return proxyUnreachable(res.transportError);
324
325
  if (res.status !== 200) return apiError(res.json, `failed to switch ${name}`, res.status);
325
326
 
326
- if (wantsJson) console.log(JSON.stringify({ ok: true, provider: name, type: c.type, activeId }, null, 2));
327
- else console.log(`${name}: active ${c.type === "api-key" ? "key" : "account"} is now ${displayId(activeId)}`);
327
+ // The route reports this only when routing would drop the pin it just recorded, so an
328
+ // absent field means the pin survives (#4521).
329
+ const pinDrainReason = typeof res.json.pinDrainReason === "string" ? res.json.pinDrainReason : undefined;
330
+ if (wantsJson) {
331
+ console.log(JSON.stringify({
332
+ ok: true,
333
+ provider: name,
334
+ type: c.type,
335
+ activeId,
336
+ ...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
337
+ }, null, 2));
338
+ } else {
339
+ console.log(`${name}: active ${c.type === "api-key" ? "key" : "account"} is now ${displayId(activeId)}`);
340
+ }
328
341
  if (c.type === "codex") {
329
342
  console.error("Takes effect immediately; running threads move on their next request, and in-flight requests keep the account they captured.");
330
343
  const active = await apiJson(deps, baseUrl, "GET", "/api/codex-auth/active");
331
- if (active.status === 200 && typeof active.json.autoSwitchThreshold === "number" && active.json.autoSwitchThreshold > 0) {
332
- console.error(`Note: auto-switch (threshold ${active.json.autoSwitchThreshold}%) may override this pin.`);
344
+ const threshold = active.status === 200 && typeof active.json.autoSwitchThreshold === "number"
345
+ ? active.json.autoSwitchThreshold
346
+ : undefined;
347
+ if (pinDrainReason !== undefined) {
348
+ // "may override" is the right caveat for a pin that is currently fine and could be
349
+ // overtaken later. It is the wrong sentence for one the next request will discard, and
350
+ // printing only that is what left the operator believing the account was pinned.
351
+ const because = pinDrainReason === "quota_threshold"
352
+ ? `is at or above the auto-switch threshold${threshold !== undefined ? ` (${threshold}%)` : ""}`
353
+ : `cannot currently be selected (${pinDrainReason})`;
354
+ console.error(`Note: ${displayId(activeId)} ${because}, so routing releases this pin on its next request.`);
355
+ } else if (threshold !== undefined && threshold > 0) {
356
+ console.error(`Note: auto-switch (threshold ${threshold}%) may override this pin.`);
333
357
  }
334
358
  }
335
359
  return 0;
@@ -360,6 +384,10 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
360
384
  if (sub === "clear-cooldown") return await cmdClearCooldown(rest, deps);
361
385
  if (sub === "add-key") return await cmdAddKey(rest, deps);
362
386
  if (sub === "import") return await cmdImport(rest, deps);
387
+ if (sub === "import-orca") {
388
+ const { cmdOrcaImport } = await import("./account-orca-import");
389
+ return await cmdOrcaImport(rest);
390
+ }
363
391
  if (sub === "main") {
364
392
  const { cmdNativeMainAccount } = await import("./account-main");
365
393
  return await cmdNativeMainAccount(rest, deps);