@bitkyc08/opencodex 2.80.0 → 2.81.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 (227) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/App-7yhoys8a.js +54 -0
  3. package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
  4. package/gui/dist/assets/index-RsoaAxXY.js +94 -0
  5. package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/claude-green.svg +1 -0
  8. package/package.json +3 -2
  9. package/src/adapters/anthropic-model-contract.ts +8 -2
  10. package/src/adapters/anthropic.ts +7 -2
  11. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  12. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  13. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  14. package/src/adapters/coding-agent/protocol.ts +122 -17
  15. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  16. package/src/adapters/coding-agent/turn.ts +101 -44
  17. package/src/adapters/cursor/catalog.ts +8 -0
  18. package/src/adapters/cursor/effort-map.ts +2 -0
  19. package/src/adapters/devin/live-models.ts +2 -0
  20. package/src/adapters/kiro-retry.ts +9 -4
  21. package/src/adapters/openai-chat/messages.ts +7 -6
  22. package/src/adapters/physical-send.ts +8 -5
  23. package/src/adapters/qoder/adapter.ts +41 -6
  24. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  25. package/src/adapters/zed.ts +16 -1
  26. package/src/claude/inbound-cache-stabilize.ts +4 -9
  27. package/src/claude/inbound.ts +1 -0
  28. package/src/claude/intercept/model-bindings.ts +1 -0
  29. package/src/claude/intercept/picker-listener.ts +71 -4
  30. package/src/cli/account-anthropic-threshold.ts +14 -5
  31. package/src/cli/account-api.ts +3 -1
  32. package/src/cli/account-extended.ts +13 -9
  33. package/src/cli/account-policy-dto.ts +1 -1
  34. package/src/cli/account-policy.ts +13 -4
  35. package/src/cli/account.ts +3 -3
  36. package/src/cli/capabilities-accounts.ts +2 -2
  37. package/src/cli/capabilities-agents-routing.ts +24 -0
  38. package/src/cli/capabilities-integrations.ts +16 -0
  39. package/src/cli/codex-shim-autorestore.ts +3 -0
  40. package/src/cli/dispatch.ts +10 -1
  41. package/src/cli/help.ts +4 -1
  42. package/src/cli/index.ts +11 -5
  43. package/src/cli/integration-plan-dto.ts +1 -0
  44. package/src/cli/integration-preview.ts +8 -1
  45. package/src/cli/integrations.ts +32 -3
  46. package/src/cli/message-args.ts +34 -0
  47. package/src/cli/message-command.ts +43 -0
  48. package/src/cli/message-runtime.ts +6 -0
  49. package/src/cli/observe.ts +3 -1
  50. package/src/cli/provider.ts +29 -6
  51. package/src/cli/registry.ts +24 -2
  52. package/src/clients/config-export/commandcode.ts +131 -0
  53. package/src/clients/config-export/contracts.ts +12 -0
  54. package/src/clients/config-export.ts +64 -1
  55. package/src/codex/auth-api/account-list.ts +11 -3
  56. package/src/codex/auth-api/main-account-probe.ts +25 -8
  57. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  58. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  59. package/src/codex/auth-api/routes.ts +1 -0
  60. package/src/codex/catalog/auto-review.ts +13 -6
  61. package/src/codex/catalog/build-entries.ts +15 -7
  62. package/src/codex/catalog/control-plane.ts +45 -0
  63. package/src/codex/catalog/effort.ts +11 -1
  64. package/src/codex/catalog/gather-capture.ts +5 -3
  65. package/src/codex/catalog/parsing.ts +2 -1
  66. package/src/codex/catalog/provider-models.ts +15 -9
  67. package/src/codex/control-plane-models.ts +6 -0
  68. package/src/combos/jev-decision-contract.ts +25 -2
  69. package/src/combos/jev.ts +3 -2
  70. package/src/combos/types.ts +1 -1
  71. package/src/config/diagnostics.ts +27 -16
  72. package/src/config/load-degrade.ts +34 -0
  73. package/src/config/schema/anthropic-account-pool.ts +143 -0
  74. package/src/config/schema/config-schema.ts +7 -1
  75. package/src/config/schema/leaf-validators.ts +4 -0
  76. package/src/config.ts +2 -0
  77. package/src/generated/compatibility-version.json +320 -184
  78. package/src/generated/model-metadata.ts +5 -4
  79. package/src/images/loop.ts +2 -1
  80. package/src/integrations/catalog-refresh.ts +1 -1
  81. package/src/integrations/mutation-plan.ts +7 -0
  82. package/src/integrations/registry.ts +8 -0
  83. package/src/integrations/state.ts +9 -2
  84. package/src/integrations/writer.ts +8 -2
  85. package/src/lib/lab-live-route-production.ts +9 -1
  86. package/src/lib/request-execution-budget.ts +236 -58
  87. package/src/lib/spend-pool-alias-validation.ts +79 -0
  88. package/src/lib/spend-pool-continuity.ts +34 -0
  89. package/src/lib/spend-reservation-ledger.ts +412 -50
  90. package/src/lib/state-store-registrations.ts +20 -3
  91. package/src/lib/upstream-retry.ts +17 -5
  92. package/src/lib/workflow-budget.ts +53 -11
  93. package/src/messaging/budget.ts +45 -0
  94. package/src/messaging/discovery.ts +57 -0
  95. package/src/messaging/envelope.ts +49 -0
  96. package/src/messaging/input.ts +37 -0
  97. package/src/messaging/rpc.ts +196 -0
  98. package/src/messaging/send.ts +58 -0
  99. package/src/messaging/socket.ts +72 -0
  100. package/src/messaging/types.ts +29 -0
  101. package/src/oauth/anthropic-account-refusal.ts +51 -17
  102. package/src/oauth/anthropic-account-threshold.ts +8 -2
  103. package/src/oauth/anthropic-continuity.ts +15 -1
  104. package/src/oauth/anthropic-model-quota.ts +120 -48
  105. package/src/oauth/anthropic-model-routes.ts +11 -3
  106. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  107. package/src/oauth/anthropic-pool-config.ts +30 -0
  108. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  109. package/src/oauth/anthropic-routing.ts +1202 -866
  110. package/src/oauth/anthropic-send-ownership.ts +41 -0
  111. package/src/oauth/anthropic.ts +4 -1
  112. package/src/oauth/generic-account-failover.ts +3 -1
  113. package/src/oauth/health.ts +8 -6
  114. package/src/oauth/index.ts +90 -173
  115. package/src/oauth/kiro.ts +22 -9
  116. package/src/oauth/login-cli.ts +2 -0
  117. package/src/oauth/model-discovery-auth.ts +43 -0
  118. package/src/oauth/pool-kernel.ts +11 -5
  119. package/src/oauth/pool-settings-capability.ts +5 -3
  120. package/src/oauth/store-anthropic-instance.ts +87 -0
  121. package/src/oauth/store.ts +67 -9
  122. package/src/oauth/token-guardian.ts +2 -0
  123. package/src/oauth/types.ts +2 -0
  124. package/src/protocols/plan-snapshot.ts +5 -0
  125. package/src/protocols/settings.ts +20 -5
  126. package/src/providers/anthropic-instance-id.ts +47 -0
  127. package/src/providers/anthropic-instance.ts +48 -0
  128. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  129. package/src/providers/derive.ts +2 -0
  130. package/src/providers/kiro-models.ts +3 -0
  131. package/src/providers/label.ts +1 -1
  132. package/src/providers/model-rename-fields.ts +2 -0
  133. package/src/providers/quota/account-cache.ts +37 -11
  134. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  135. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  136. package/src/providers/quota/report-cache.ts +24 -1
  137. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  138. package/src/providers/quota.ts +22 -32
  139. package/src/providers/registry/entries-core.ts +35 -30
  140. package/src/providers/registry/model-ids.ts +1 -0
  141. package/src/providers/registry/model-seeds.ts +7 -4
  142. package/src/providers/registry/types.ts +3 -0
  143. package/src/providers/registry.ts +3 -3
  144. package/src/responses/spill-store.ts +5 -1
  145. package/src/responses/state/metrics.ts +87 -0
  146. package/src/responses/state/snapshot-codec.ts +6 -0
  147. package/src/responses/state/spill-queue.ts +1 -0
  148. package/src/responses/state/unforced-store-false.ts +55 -0
  149. package/src/responses/state.ts +50 -66
  150. package/src/router.ts +28 -4
  151. package/src/routing/compatibility/assemble.ts +1 -1
  152. package/src/routing/quota.ts +7 -3
  153. package/src/server/auth-cors.ts +5 -0
  154. package/src/server/chat-completions.ts +6 -1
  155. package/src/server/chat-native.ts +45 -25
  156. package/src/server/claude-messages.ts +16 -2
  157. package/src/server/gui-static.ts +38 -4
  158. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  159. package/src/server/index.ts +4 -2
  160. package/src/server/inference/context.ts +1 -1
  161. package/src/server/lifecycle.ts +1 -1
  162. package/src/server/management/agent-settings-routes.ts +30 -6
  163. package/src/server/management/anthropic-account-threshold.ts +12 -9
  164. package/src/server/management/anthropic-pool-settings.ts +72 -0
  165. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  166. package/src/server/management/config-routes.ts +27 -7
  167. package/src/server/management/decision-routes.ts +2 -1
  168. package/src/server/management/integration-routes.ts +25 -2
  169. package/src/server/management/oauth-account-routes.ts +79 -98
  170. package/src/server/management/provider-instance-ownership.ts +44 -0
  171. package/src/server/management/provider-routes.ts +21 -28
  172. package/src/server/management/route-registry.ts +3 -1
  173. package/src/server/management/routing-profile-routes.ts +4 -4
  174. package/src/server/management/system-restart.ts +44 -14
  175. package/src/server/management/system-routes.ts +24 -2
  176. package/src/server/management/vision-sidecar-options.ts +9 -6
  177. package/src/server/management/web-search-sidecar-options.ts +51 -2
  178. package/src/server/management-auth.ts +6 -0
  179. package/src/server/messages-native-eligibility.ts +3 -2
  180. package/src/server/messages-native-oauth.ts +86 -41
  181. package/src/server/messages-native-selector.ts +39 -0
  182. package/src/server/messages-native.ts +84 -25
  183. package/src/server/request-log.ts +5 -0
  184. package/src/server/responses/adapter-continuation.ts +29 -17
  185. package/src/server/responses/adapter-dispatch.ts +47 -17
  186. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  187. package/src/server/responses/compact.ts +25 -8
  188. package/src/server/responses/compaction-recovery.ts +1 -1
  189. package/src/server/responses/core-combo-native.ts +4 -1
  190. package/src/server/responses/core-combo.ts +16 -11
  191. package/src/server/responses/core-normalize.ts +1 -0
  192. package/src/server/responses/core-options.ts +3 -1
  193. package/src/server/responses/core-replay.ts +5 -0
  194. package/src/server/responses/passthrough-delivery.ts +7 -3
  195. package/src/server/responses/passthrough-dispatch.ts +21 -8
  196. package/src/server/responses/request-prepare.ts +37 -5
  197. package/src/server/responses/request-send-budget.ts +74 -37
  198. package/src/server/responses/request-spend.ts +325 -33
  199. package/src/server/responses/request-transport.ts +132 -52
  200. package/src/server/responses/run-turn-execution.ts +11 -3
  201. package/src/server/responses/sidecar-execution.ts +96 -14
  202. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  203. package/src/server/workflow-refusal.ts +30 -0
  204. package/src/service/ownership-mutation-lease.d.mts +1 -1
  205. package/src/service/ownership-mutation-lease.mjs +5 -7
  206. package/src/sidecar/anthropic-binding.ts +79 -0
  207. package/src/sidecar/auth.ts +76 -2
  208. package/src/types/anthropic-account-pool.ts +33 -0
  209. package/src/types/config.ts +12 -28
  210. package/src/types/provider.ts +5 -0
  211. package/src/types/tools.ts +7 -0
  212. package/src/types.ts +4 -0
  213. package/src/usage/cost.ts +3 -2
  214. package/src/usage/expected-prices.ts +66 -14
  215. package/src/vision/anthropic-describe.ts +16 -3
  216. package/src/vision/index.ts +3 -0
  217. package/src/vision/plan.ts +19 -6
  218. package/src/web-search/alpha-search.ts +7 -1
  219. package/src/web-search/anthropic-executor.ts +16 -4
  220. package/src/web-search/index.ts +7 -3
  221. package/src/web-search/loop.ts +27 -5
  222. package/src/web-search/passthrough-bridge.ts +4 -1
  223. package/src/web-search/sidecar-providers.ts +4 -3
  224. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  225. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  226. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  227. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -4,6 +4,7 @@ import { createAnthropicAdapter } from "./anthropic";
4
4
  import { createGoogleAdapter } from "./google";
5
5
  import { createOpenAIChatAdapter } from "./openai-chat";
6
6
  import { createResponsesPassthroughAdapter } from "./openai-responses";
7
+ import { createAdapterPhysicalSend } from "./physical-send";
7
8
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types";
8
9
  import { createTranslatorBudget, releaseTranslatedEvent, retainTranslatedEvent, type TranslatorBudget } from "../lib/translator-budget";
9
10
  import { releaseCompactionCiphertextLease } from "../responses/compaction";
@@ -13,6 +14,7 @@ import {
13
14
  resolveZedModels,
14
15
  scrubZedCredentials,
15
16
  zedLlmFetch,
17
+ ZED_CLOUD_BASE_URL,
16
18
  ZED_HEADERS,
17
19
  type ZedCredentials,
18
20
  } from "../providers/zed";
@@ -340,8 +342,21 @@ export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
340
342
  async fetchResponse(request, ctx) {
341
343
  if (!credentials) throw new Error("Zed request credentials were not initialized");
342
344
  const fetchFn = ctx?.executor ?? globalThis.fetch;
345
+ const send = createAdapterPhysicalSend(ctx);
346
+ let completionAttempts = 0;
343
347
  return zedLlmFetch(credentials, "/completions", {
344
- fetchFn,
348
+ fetchFn: (async (input, init) => {
349
+ const url = input instanceof Request ? input.url : new URL(String(input)).href;
350
+ // Token exchange/account lookup are support traffic; both completion attempts
351
+ // share admission and physical ordinals, including the provider's 401 replay.
352
+ if (url === `${ZED_CLOUD_BASE_URL}/client/users/me`
353
+ || url === `${ZED_CLOUD_BASE_URL}/client/llm_tokens`) return fetchFn(input, init);
354
+ const recovery = completionAttempts++ > 0;
355
+ return send({ url, sendClass: recovery ? "auth-recovery" : "initial",
356
+ ...(recovery ? { recovery: "oauth-401" } : {}),
357
+ dispatch: executor => executor(input, init),
358
+ });
359
+ }) as typeof globalThis.fetch,
345
360
  signal: ctx?.abortSignal,
346
361
  baseUrl: provider.baseUrl,
347
362
  fetchInit: {
@@ -3,10 +3,10 @@
3
3
  * occasional TaskCreate nudges) into system text that becomes Responses
4
4
  * `instructions`. That churn breaks Muse/Go prefix cache on the instructions
5
5
  * prefix even when tools stay stable. Strip dynamics from instructions;
6
- * surface the latest notice on `input` instead.
6
+ * drop token footers and surface the latest TaskCreate notice on `input` instead.
7
7
  *
8
- * Relocation is identified by harness shape: only a trailing, unfenced,
9
- * canonical notice at the end of instructions is moved. An unmatched fence
8
+ * Peeling is identified by harness shape: only a trailing, unfenced,
9
+ * canonical notice at the end of instructions is removed. An unmatched fence
10
10
  * opener covers through EOF. No match → the original string is returned
11
11
  * byte-for-byte. The matcher is content identity only; `translateAnthropicRequest`
12
12
  * requires `claudeCode.stabilizePromptCache: true` before this helper runs. Claude Code
@@ -86,7 +86,6 @@ export function stabilizeClaudeInstructionsForPromptCache(
86
86
  const ranges = fencedRanges(instructions);
87
87
  let fenceIndex = ranges.length - 1;
88
88
  let end = instructions.length;
89
- let latestTotal: string | null = null;
90
89
  let latestNudge: string | null = null;
91
90
  let peeled = false;
92
91
 
@@ -110,7 +109,6 @@ export function stabilizeClaudeInstructionsForPromptCache(
110
109
 
111
110
  while (fenceIndex >= 0 && ranges[fenceIndex]!.start > contentStart) fenceIndex--;
112
111
  if (fenceIndex >= 0 && contentStart < ranges[fenceIndex]!.end) break;
113
- if (total && latestTotal === null) latestTotal = notice;
114
112
  if (nudge && latestNudge === null) latestNudge = notice;
115
113
  peeled = true;
116
114
 
@@ -123,8 +121,5 @@ export function stabilizeClaudeInstructionsForPromptCache(
123
121
  }
124
122
 
125
123
  if (!peeled) return { instructions, dynamicNotice: null };
126
- const noticeParts: string[] = [];
127
- if (latestTotal) noticeParts.push(latestTotal);
128
- if (latestNudge) noticeParts.push(latestNudge);
129
- return { instructions: instructions.slice(0, end), dynamicNotice: noticeParts.join("\n\n") };
124
+ return { instructions: instructions.slice(0, end), dynamicNotice: latestNudge };
130
125
  }
@@ -445,6 +445,7 @@ function translateAnthropicRequest(
445
445
  // a matching suffix or metadata.user_id.
446
446
  const stabilized = stabilizeClaudeInstructionsForPromptCache(joinedSystem);
447
447
  if (stabilized.instructions) body.instructions = stabilized.instructions;
448
+ // Token footers are dropped; append nothing if no other notice remains.
448
449
  if (stabilized.dynamicNotice) {
449
450
  input.push({
450
451
  type: "message",
@@ -35,6 +35,7 @@ export const DESKTOP_PICKER_ID_SUGGESTIONS: readonly string[] = [
35
35
  "claude-sonnet-5-5",
36
36
  "claude-sonnet-5",
37
37
  "claude-fable-5-1",
38
+ "claude-haiku-5-5", // 261008 preemptive suggestion; Desktop owns the live picker.
38
39
  "claude-haiku-4-5",
39
40
  "claude-opus-5",
40
41
  "claude-fable-5",
@@ -11,7 +11,7 @@
11
11
  * HTTP/1.1 server, unchanged. Upstream is one HTTP/1.1 request per client request on both paths.
12
12
  * Only the bounded bootstrap response is held; other bodies and upgraded sockets relay as streams.
13
13
  */
14
- import { createSecureServer } from "node:http2";
14
+ import { constants as h2Constants, createSecureServer } from "node:http2";
15
15
  import type { Http2ServerRequest, Http2ServerResponse, ServerHttp2Session } from "node:http2";
16
16
  import { createServer, request as httpsRequest } from "node:https";
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
@@ -184,9 +184,35 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
184
184
  // reason phrase differs, and sendHead branches on it.
185
185
  const res = relayRes as ServerResponse;
186
186
  const h2 = req.httpVersionMajor === 2;
187
+ // Bun's h2 compat response emits finish only after both stream halves close.
188
+ const responseWritable = h2 ? (req as Http2ServerRequest).stream : res;
189
+ // Framing, not method, determines whether a peer can keep sending a body. A bodyless h2
190
+ // request carries END_STREAM on its headers; HTTP/1.1 needs length or chunked framing.
191
+ const uploading = h2 ? !(req as Http2ServerRequest).stream.endAfterHeaders
192
+ : req.headers["transfer-encoding"] !== undefined || Number(req.headers["content-length"] ?? 0) > 0;
193
+ const closeInput = (completeReply = false) => {
194
+ if (h2) {
195
+ const stream = (req as Http2ServerRequest).stream;
196
+ if (!stream.closed && !stream.destroyed) {
197
+ stream.close(completeReply ? h2Constants.NGHTTP2_NO_ERROR : h2Constants.NGHTTP2_CANCEL);
198
+ }
199
+ } else if (!req.destroyed) {
200
+ if (completeReply) {
201
+ // finish is not peer receipt: destroy can truncate bytes still queued in TLS/TCP.
202
+ // Discard further input and flush a graceful FIN without waiting for body completion.
203
+ req.resume();
204
+ req.socket.end();
205
+ } else req.destroy();
206
+ }
207
+ };
187
208
  // Refusals answer with an empty response and a fixed log line that carries no request data.
188
209
  const refuse = (status: 400 | 503) => {
189
- res.writeHead(status, { "Content-Length": "0" });
210
+ // Do not leave a refused, still-uploading stream behind after delivering its empty reply.
211
+ if (uploading && !req.complete) {
212
+ req.once("error", () => res.destroy());
213
+ responseWritable.once("finish", () => closeInput(true));
214
+ }
215
+ res.writeHead(status, { "Content-Length": "0", ...(!h2 && uploading && !req.complete ? { Connection: "close" } : {}) });
190
216
  res.end();
191
217
  options.log?.(`picker request refused ${status}`);
192
218
  };
@@ -209,12 +235,16 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
209
235
  const fail = () => {
210
236
  if (clientGone) return;
211
237
  if (res.headersSent) res.destroy();
212
- else { res.writeHead(502, { "Content-Length": "0" }); res.end(); log(502); }
238
+ else {
239
+ res.writeHead(502, { "Content-Length": "0", ...(!h2 && uploading && !req.complete ? { Connection: "close" } : {}) });
240
+ res.end(); log(502);
241
+ }
213
242
  };
214
243
  const omit = bootstrap ? new Set(["accept-encoding"]) : new Set<string>();
215
244
  const headers = upstreamRequestHeaders(req, omit);
216
245
  if (bootstrap) headers.push("Accept-Encoding", narrowBootstrapAcceptEncoding());
217
246
  let upReq: ReturnType<typeof httpsRequest>;
247
+ let upstreamResponded = false;
218
248
  try {
219
249
  // The HTTP/1.1 client validates the method, path and header values synchronously.
220
250
  upReq = httpsRequest({
@@ -222,13 +252,17 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
222
252
  ca: upstream.ca, rejectUnauthorized: true, agent: false,
223
253
  method, path: req.url, headers, maxHeaderSize: PICKER_MAX_HEADER_BYTES,
224
254
  }, upRes => {
255
+ upstreamResponded = true;
225
256
  const status = upRes.statusCode ?? 502;
226
257
  const originalHeaders = filteredHeaders(upRes.rawHeaders);
227
258
  const sendHead = (raw: string[]) => {
228
259
  if (res.headersSent) return;
229
260
  // HTTP/2 has no reason phrase; its compat writeHead takes the same flat raw header array.
230
261
  if (h2) (relayRes as Http2ServerResponse).writeHead(status, raw as unknown as Record<string, string>);
231
- else res.writeHead(status, upRes.statusMessage, raw);
262
+ else {
263
+ if (uploading && !req.complete) raw.push("Connection", "close");
264
+ res.writeHead(status, upRes.statusMessage, raw);
265
+ }
232
266
  log(status);
233
267
  };
234
268
  upRes.on("error", fail);
@@ -293,6 +327,39 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
293
327
  // Bun's compat response skips its close event for a HEAD reset before end(); the stream's own
294
328
  // close always fires. Destroying an upstream request that already completed is a no-op.
295
329
  if (h2) (req as Http2ServerRequest).stream.once("close", onClientClose);
330
+ if (uploading) {
331
+ let uploadFinished = false;
332
+ const finishUpload = () => {
333
+ if (uploadFinished) return;
334
+ uploadFinished = true;
335
+ req.off("end", finishUpload);
336
+ req.off("close", finishUpload);
337
+ upReq.off("close", onUpstreamClose);
338
+ responseWritable.off("finish", onResponseFinish);
339
+ res.off("close", stopUnfinishedUpload);
340
+ if (h2) (req as Http2ServerRequest).stream.off("close", finishUpload);
341
+ };
342
+ // Upstream close can precede draining a successful reply to a slow downstream.
343
+ // Stop relaying input only once that reply's writable finishes or closes.
344
+ const stopUnfinishedUpload = (completeReply = false) => {
345
+ if (!uploadFinished) {
346
+ req.unpipe(upReq);
347
+ closeInput(completeReply);
348
+ finishUpload();
349
+ upReq.destroy();
350
+ }
351
+ };
352
+ const onResponseFinish = () => stopUnfinishedUpload(true);
353
+ const onUpstreamClose = () => {
354
+ if (!upstreamResponded && !res.writableEnded) stopUnfinishedUpload();
355
+ };
356
+ req.once("end", finishUpload);
357
+ req.once("close", finishUpload);
358
+ upReq.once("close", onUpstreamClose);
359
+ responseWritable.once("finish", onResponseFinish);
360
+ res.once("close", stopUnfinishedUpload);
361
+ if (h2) (req as Http2ServerRequest).stream.once("close", finishUpload);
362
+ }
296
363
  req.pipe(upReq);
297
364
  };
298
365
  server.on("request", relay);
@@ -1,7 +1,13 @@
1
+ import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
1
2
  import { apiError, apiJson, proxyUnreachable, resolveBaseUrl, type AccountDeps } from "./account-api";
2
3
 
4
+ /** A 200 that names another pool is refused locally; there is no upstream status to forward. */
5
+ export function anthropicProviderIdentityError(json: Record<string, unknown>, provider: AnthropicInstanceId, label: string): number | undefined {
6
+ return json.provider === provider ? undefined : apiError({}, `invalid ${label} identity`, 400);
7
+ }
8
+
3
9
  /** A separate account selector prevents accidentally changing the whole Anthropic pool. */
4
- export async function cmdAnthropicAccountThreshold(args: string[], action: string, wantsJson: boolean, deps: AccountDeps): Promise<number> {
10
+ export async function cmdAnthropicAccountThreshold(args: string[], action: string, wantsJson: boolean, deps: AccountDeps, provider: AnthropicInstanceId = "anthropic"): Promise<number> {
5
11
  const selector = args.indexOf("--account");
6
12
  const accountId = selector >= 0 ? args[selector + 1] : undefined;
7
13
  if (selector >= 0) args.splice(selector, 2);
@@ -15,8 +21,8 @@ export async function cmdAnthropicAccountThreshold(args: string[], action: strin
15
21
  const base = await resolveBaseUrl(deps);
16
22
  if (!base) return proxyUnreachable();
17
23
  const response = action === "status"
18
- ? await apiJson(deps, base, "GET", "/api/oauth/accounts?provider=anthropic")
19
- : await apiJson(deps, base, "PUT", "/api/oauth/accounts/auto-switch", { provider: "anthropic", accountId, threshold });
24
+ ? await apiJson(deps, base, "GET", `/api/oauth/accounts?provider=${provider}`)
25
+ : await apiJson(deps, base, "PUT", "/api/oauth/accounts/auto-switch", { provider, accountId, threshold });
20
26
  if (response.status === 0) return proxyUnreachable(response.transportError);
21
27
  if (response.status !== 200) return apiError(response.json, "failed to update account threshold", response.status);
22
28
  if (!response.json || typeof response.json !== "object" || Array.isArray(response.json)) return apiError({}, "invalid account threshold response", 400);
@@ -28,7 +34,10 @@ export async function cmdAnthropicAccountThreshold(args: string[], action: strin
28
34
  const validPercent = (value: unknown) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 100;
29
35
  if ((result.autoSwitchThresholdOverride !== null && !validPercent(result.autoSwitchThresholdOverride))
30
36
  || !validPercent(result.effectiveAutoSwitchThreshold)) return apiError({}, "invalid account threshold response", 400);
31
- const payload = { provider: "anthropic", accountId, autoSwitchThresholdOverride: result.autoSwitchThresholdOverride,
37
+ const responseProvider = action === "status" ? response.json.provider : result.provider;
38
+ if ((provider === "anthropic2" || responseProvider !== undefined) && responseProvider !== provider) return apiError({}, "invalid account threshold identity", 400);
39
+ if (action !== "status" && (provider === "anthropic2" || result.accountId !== undefined) && result.accountId !== accountId) return apiError({}, "invalid account threshold identity", 400);
40
+ const payload = { provider, accountId, autoSwitchThresholdOverride: result.autoSwitchThresholdOverride,
32
41
  effectiveAutoSwitchThreshold: result.effectiveAutoSwitchThreshold };
33
42
  if (wantsJson) console.log(JSON.stringify(payload, null, 2));
34
43
  else console.log(`auto-switch: ${payload.autoSwitchThresholdOverride === null ? "inherited" : "custom"} (${payload.effectiveAutoSwitchThreshold === 0 ? "usage-based switching disabled" : `${payload.effectiveAutoSwitchThreshold}%`})`);
@@ -36,6 +45,6 @@ export async function cmdAnthropicAccountThreshold(args: string[], action: strin
36
45
  }
37
46
 
38
47
  function invalid(): number {
39
- console.error("Usage: ocx account auto-switch anthropic <status|inherit|on|off|threshold <0-100>> --account <id> [--json]");
48
+ console.error("Usage: ocx account auto-switch <anthropic|anthropic2> <status|inherit|on|off|threshold <0-100>> --account <id> [--json]");
40
49
  return 2;
41
50
  }
@@ -1,3 +1,4 @@
1
+ import { configuredAnthropicInstance, isAnthropicOAuthInstance } from "../providers/anthropic-instance";
1
2
  import { parseQuotaFailureCode, type QuotaFailureCode, type ProviderQuota, type AccountQuotaMode } from "../providers/quota-types";
2
3
  /**
3
4
  * Data-access layer for `ocx account` (issue #180) — live-proxy HTTP client and
@@ -110,6 +111,7 @@ export function classifyAccount(config: OcxConfig, name: string): ClassifyResult
110
111
  if (provider && !provider.authMode && (provider.apiKey || (provider.apiKeyPool?.length ?? 0) > 0)) {
111
112
  return { type: "api-key" };
112
113
  }
114
+ if (name === "anthropic2" && !configuredAnthropicInstance(config, name)) return { error: "Anthropic pool instance is not configured" };
113
115
  if (isPublicOAuthProvider(name)) return { type: "oauth" };
114
116
  if (provider) return { type: "api-key" };
115
117
  return { error: `unknown provider "${name}"` };
@@ -408,7 +410,7 @@ async function fetchOAuthRows(
408
410
  needsReauth: a.needsReauth,
409
411
  ...projectAccountHealth(a, name, a.id),
410
412
  ...(a.paused === true ? { paused: true } : {}),
411
- ...(name === "anthropic" && Object.hasOwn(a, "autoSwitchThresholdOverride") ? { autoSwitchThresholdOverride: a.autoSwitchThresholdOverride } : {}),
413
+ ...(isAnthropicOAuthInstance(name) && Object.hasOwn(a, "autoSwitchThresholdOverride") ? { autoSwitchThresholdOverride: a.autoSwitchThresholdOverride } : {}),
412
414
  ...(a.needsReauthReason === "verify_account" ? { needsReauthReason: a.needsReauthReason } : {}),
413
415
  ...(name === "kiro" && typeof a.autoSelectable === "boolean"
414
416
  ? { autoSelectable: a.autoSelectable } : {}),
@@ -1,5 +1,6 @@
1
+ import { isAnthropicOAuthInstance } from "../providers/anthropic-instance";
1
2
  import { loadConfig } from "../config";
2
- import { cmdAnthropicAccountThreshold } from "./account-anthropic-threshold";
3
+ import { anthropicProviderIdentityError, cmdAnthropicAccountThreshold } from "./account-anthropic-threshold";
3
4
  import { isReservedCodexAccountWord, reportCodexAccountTargetError, resolveCodexAccountTarget } from "./account-target";
4
5
  import { hasPassiveAccountQuota } from "../providers/quota";
5
6
  import { closeSync, openSync, readSync, readFileSync, statSync } from "node:fs";
@@ -41,7 +42,7 @@ const AUTO_NOTE = "auto (no pin — lowest-usage account is selected per request
41
42
  const EXTENDED_USAGE = `Usage:
42
43
  ocx account refresh <provider> [--json]
43
44
  ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
44
- ocx account auto-switch anthropic <on|off|status|inherit|threshold <0-100>> --account <id> [--json]
45
+ ocx account auto-switch <anthropic|anthropic2> <on|off|status|inherit|threshold <0-100>> --account <id> [--json]
45
46
  ocx account alias <provider> <id|alias|main> <display-name|-> [--json]
46
47
  ocx account priority <provider> <id|alias|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
47
48
  ocx account pause <provider> <id|alias|main> [--json]
@@ -49,7 +50,7 @@ const EXTENDED_USAGE = `Usage:
49
50
  ocx account pause-exhausted <provider> [--json]
50
51
  ocx account strategy <provider> [<quota|round-robin|fill-first|least-loaded|reset-first>] [--json]
51
52
  ocx account sticky <provider> [<1-100>] [--json]
52
- ocx account routes anthropic [--file <json-file>|--clear] [--json]
53
+ ocx account routes <anthropic|anthropic2> [--file <json-file>|--clear] [--json]
53
54
  ocx account remove <provider> <id|alias|main> --yes [--json]
54
55
  ocx account clear-cooldown <provider> <id|alias|main> [--json]
55
56
  ocx account add-key <provider> [--label <label>] [--json]
@@ -360,8 +361,8 @@ export async function cmdAutoSwitch(args: string[], deps: AccountDeps): Promise<
360
361
  const classified = configAndType(deps, name);
361
362
  // Anthropic keeps its threshold on its own pool contract; generic OAuth providers (#695)
362
363
  // and the Codex pool are accepted here.
363
- if (!("error" in classified) && classified.type === "oauth" && name === "anthropic") return cmdAnthropicAccountThreshold(args, action, wantsJson, deps);
364
- if ("error" in classified || classified.type === "api-key" || name === "anthropic") {
364
+ if (!("error" in classified) && classified.type === "oauth" && isAnthropicOAuthInstance(name)) return cmdAnthropicAccountThreshold(args, action, wantsJson, deps, name);
365
+ if ("error" in classified || classified.type === "api-key" || isAnthropicOAuthInstance(name)) {
365
366
  return usage("Error: auto-switch only applies to the openai Codex account pool or a generic OAuth provider pool");
366
367
  }
367
368
  const genericPool = classified.type === "oauth";
@@ -638,7 +639,7 @@ export async function cmdClearCooldown(args: string[], deps: AccountDeps): Promi
638
639
  if (!name || !requestedId || args.length) return usage();
639
640
  const classified = configAndType(deps, name);
640
641
  if ("error" in classified) return usage(`Error: ${classified.error}`);
641
- if (classified.type !== "codex" && !(classified.type === "oauth" && name === "anthropic")) {
642
+ if (classified.type !== "codex" && !(classified.type === "oauth" && isAnthropicOAuthInstance(name))) {
642
643
  return usage(`Error: ${name} has no operator-clearable account cooldown`);
643
644
  }
644
645
  const baseUrl = await resolveBaseUrl(deps);
@@ -1108,7 +1109,8 @@ export async function cmdRoutes(args: string[], deps: AccountDeps): Promise<numb
1108
1109
  const wantsJson = flag(args, "--json");
1109
1110
  const file = flagValue(args, "--file");
1110
1111
  const clear = flag(args, "--clear");
1111
- if (args.shift() !== "anthropic" || args.length || (file.found && (!file.value || clear))) return usage();
1112
+ const provider = args.shift();
1113
+ if (!isAnthropicOAuthInstance(provider) || args.length || (file.found && (!file.value || clear))) return usage();
1112
1114
  const baseUrl = await resolveBaseUrl(deps);
1113
1115
  if (!baseUrl) return proxyUnreachable();
1114
1116
  let routes: unknown;
@@ -1123,10 +1125,12 @@ export async function cmdRoutes(args: string[], deps: AccountDeps): Promise<numb
1123
1125
  }
1124
1126
  const writing = file.found || clear;
1125
1127
  const response = await apiJson(deps, baseUrl, writing ? "PUT" : "GET",
1126
- writing ? "/api/pool/settings" : "/api/pool/settings?provider=anthropic",
1127
- writing ? { provider: "anthropic", routes: clear ? null : routes } : undefined);
1128
+ writing ? "/api/pool/settings" : `/api/pool/settings?provider=${provider}`,
1129
+ writing ? { provider, routes: clear ? null : routes } : undefined);
1128
1130
  if (response.status === 0) return proxyUnreachable(response.transportError);
1129
1131
  if (response.status !== 200) return apiError(response.json, "failed to manage Anthropic routes", response.status);
1132
+ const identityError = anthropicProviderIdentityError(response.json, provider, "Anthropic routes");
1133
+ if (identityError !== undefined) return identityError;
1130
1134
  if (wantsJson) console.log(JSON.stringify(response.json, null, 2));
1131
1135
  else console.log(JSON.stringify(response.json.routes ?? [], null, 2));
1132
1136
  return 0;
@@ -51,7 +51,7 @@ const grant = z.object({ id: grantId, label: text, resetsTotal: z.number().int()
51
51
  percentUsed: z.object({ five_hour: threshold.optional(), seven_day: threshold.optional(),
52
52
  seven_day_overage_included: threshold.optional() }),
53
53
  }).refine(value => value.resetsLeft <= value.resetsTotal);
54
- export const grantsSchema = z.object({ accountId: text.min(1), eligible: z.boolean(),
54
+ export const grantsSchema = z.object({ provider: z.enum(["anthropic", "anthropic2"]).optional(), accountId: text.min(1), eligible: z.boolean(),
55
55
  ineligibleReason: z.enum(["config_off", "tier", "seat", "mobile", "surface", "cli_version", "no_grant", "tenure", "other_experiment", "unavailable", "unknown"]).nullable(),
56
56
  atLimit: z.boolean(), grants: z.array(grant).refine(values => new Set(values.map(value => value.id)).size === values.length), nextGrantId: grantId.nullable(), weeklyResetsAt: date, cooldownUntil: date,
57
57
  pendingOperation: z.object({ operationId: z.string().uuid(), grantId, createdAt: z.number().finite(),
@@ -1,3 +1,4 @@
1
+ import { isAnthropicInstanceId } from "../providers/anthropic-instance-id";
1
2
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/account-id";
2
3
  import { isValidProviderName } from "../config/provider-name";
3
4
  import { resolveCodexAccountTargetFromRows } from "./account-target";
@@ -13,7 +14,7 @@ const USAGE = "Usage: ocx account pool <provider> [--enabled on|off] [--threshol
13
14
  + " ocx account auto-switch openai <status|on|off|inherit|threshold N> --account ID [--json]\n"
14
15
  + " ocx account credits openai <ID on|off|--all on|off> [--json]\n"
15
16
  + " ocx account quota-activation openai ID --window <fiveHour|weekly> <on|off> [--json]\n"
16
- + " ocx account anthropic-reset-grants [ID] [--json]";
17
+ + " ocx account anthropic-reset-grants [ID] [--provider anthropic|anthropic2] [--json]";
17
18
  const READ_ERRORS = { no_account: "No matching Anthropic OAuth account.", auth_failed: "Sign in to this Anthropic account again.",
18
19
  upstream_unavailable: "Anthropic did not return the reset-grant status.", ledger_unavailable: "The reset journal is unavailable.",
19
20
  ledger_busy: "The reset journal is busy. No grant was consumed." };
@@ -67,14 +68,14 @@ async function pool(args: string[], json: boolean, deps: RuntimeApiDeps): Promis
67
68
  patch.strategy = strategy;
68
69
  }
69
70
  if (window !== undefined) {
70
- if (provider !== "anthropic" || !["five-hour", "weekly", "max-utilization"].includes(window)) usage();
71
+ if (!isAnthropicInstanceId(provider) || !["five-hour", "weekly", "max-utilization"].includes(window)) usage();
71
72
  patch.quotaWindow = window;
72
73
  }
73
74
  done(args);
74
75
  const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
75
76
  const before = parseDto(poolSchema, await request(`/api/pool/settings?provider=${encodeURIComponent(provider)}`, pinned));
76
77
  requireEqual(before.provider, provider);
77
- requireEqual(before.kind, provider === "openai" ? "codex" : provider === "anthropic" ? "anthropic" : "generic");
78
+ requireEqual(before.kind, provider === "openai" ? "codex" : isAnthropicInstanceId(provider) ? "anthropic" : "generic");
78
79
  if (Object.keys(patch).some(key => !(before.supported as string[]).includes(key))
79
80
  || (before.kind === "codex" && enabled !== undefined)) throw new CliUsageError("The selected pool does not support a requested field.");
80
81
  if (!Object.keys(patch).length) return output(before, json);
@@ -157,10 +158,18 @@ export async function handleAccountPolicyCommand(sub: Subcommand, args: string[]
157
158
  if (rest.includes("--json")) usage();
158
159
  if (sub === "pool") return pool(rest, json, deps);
159
160
  if (sub !== "anthropic-reset-grants") return codex(sub, rest, json, deps);
161
+ const selectedProvider = option(rest, "--provider");
162
+ const provider = selectedProvider ?? "anthropic";
163
+ if (!isAnthropicInstanceId(provider)) usage();
160
164
  const id = rest.length ? selector(rest.shift()) : undefined;
161
165
  done(rest);
162
166
  const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
163
- const result = parseDto(grantsSchema, await request(`/api/anthropic/reset-grants${id ? `?accountId=${encodeURIComponent(id)}` : ""}`, pinned));
167
+ // Omission preserves legacy A request syntax; explicit calls verify the echoed instance.
168
+ const query = new URLSearchParams();
169
+ if (id) query.set("accountId", id);
170
+ if (selectedProvider !== undefined) query.set("provider", provider);
171
+ const result = parseDto(grantsSchema, await request(`/api/anthropic/reset-grants${query.size ? `?${query}` : ""}`, pinned));
172
+ if (selectedProvider !== undefined || result.provider !== undefined) requireEqual(result.provider, provider);
164
173
  if (id) requireEqual(result.accountId, id);
165
174
  return output(result, json, "Reset-grant status read; no grant was consumed.");
166
175
  }, READ_ERRORS);
@@ -52,7 +52,7 @@ const ACCOUNT_USAGE = `Usage:
52
52
  ocx account clear <provider> [--json]
53
53
  ocx account refresh <provider> [--json]
54
54
  ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
55
- ocx account auto-switch anthropic <on|off|status|inherit|threshold <0-100>> --account <id> [--json]
55
+ ocx account auto-switch <anthropic|anthropic2> <on|off|status|inherit|threshold <0-100>> --account <id> [--json]
56
56
  ocx account alias <provider> <account-or-key-id|alias> <display-name|-> [--json]
57
57
  ocx account priority <provider> <account-id|alias|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
58
58
  ocx account pause <provider> <account-id|alias|main> [--json]
@@ -60,7 +60,7 @@ const ACCOUNT_USAGE = `Usage:
60
60
  ocx account pause-exhausted <provider> [--json]
61
61
  ocx account strategy <provider> [<quota|round-robin|fill-first|least-loaded|reset-first>] [--json]
62
62
  ocx account sticky <provider> [<1-100>] [--json]
63
- ocx account routes anthropic [--file <json-file>|--clear] [--json]
63
+ ocx account routes <anthropic|anthropic2> [--file <json-file>|--clear] [--json]
64
64
  ocx account remove <provider> <account-or-key-id|alias|main> --yes [--json]
65
65
  ocx account clear-cooldown <provider> <account-id|alias|main> [--json]
66
66
  ocx account add-key <provider> [--label <label>] [--json]
@@ -70,7 +70,7 @@ const ACCOUNT_USAGE = `Usage:
70
70
  ocx account pool <provider> [--enabled on|off] [--threshold N] [--strategy NAME] [--sticky N] [--quota-window W] [--json]
71
71
  ocx account credits openai <ID on|off|--all on|off> [--json]
72
72
  ocx account quota-activation openai ID --window fiveHour|weekly <on|off> [--json]
73
- ocx account anthropic-reset-grants [ID] [--json]
73
+ ocx account anthropic-reset-grants [ID] [--provider anthropic|anthropic2] [--json]
74
74
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
75
75
  ocx account cancel <provider> [--flow <flow-id>] [--json] (--flow required for codex)
76
76
  ocx account reset-credits <account-id|main> [--consume --yes] [--json]
@@ -184,10 +184,10 @@ export const ACCOUNT_CAPABILITIES: readonly Capability[] = [
184
184
  },
185
185
  {
186
186
  command: ["account","anthropic-reset-grants"],
187
- usage: "ocx account anthropic-reset-grants [account-id] [--json]",
187
+ usage: "ocx account anthropic-reset-grants [account-id] [--provider anthropic|anthropic2] [--json]",
188
188
  summary: "Read Anthropic reset-grant eligibility and pending status without consuming a grant.",
189
189
  routes: [{"method":"GET","path":"/api/anthropic/reset-grants"}],
190
- flags: [{"name":"--json","value":"boolean","summary":"Emit the validated task result as one JSON document."}],
190
+ flags: [{ name: "--provider", value: "string", summary: "Anthropic OAuth instance: anthropic or anthropic2; omission keeps the legacy Anthropic pool." }, {"name":"--json","value":"boolean","summary":"Emit the validated task result as one JSON document."}],
191
191
  mutates: false,
192
192
  json: "envelope",
193
193
  details: ["Optional identity is the exact Anthropic account ID. Omission delegates active/fallback account choice to the server, not the Codex alias resolver.","This GET can refresh access/status information upstream. Empty grants, unavailable status, pending operation and unavailable journal remain distinct. Consume remains a GUI-session-only human handoff; no session acquisition or retry is performed."],
@@ -1,6 +1,30 @@
1
1
  import type { Capability } from "./capability-types";
2
2
 
3
3
  export const AGENT_ROUTING_CAPABILITIES: readonly Capability[] = [
4
+ {
5
+ command: ["message", "sessions"],
6
+ summary: "Discover already-loaded local Codex sessions without reading conversation history.",
7
+ routes: [], flags: [{ name: "--json", value: "boolean", summary: "Emit a versioned local session directory." }],
8
+ mutates: false, json: "envelope",
9
+ bannerLines: ["ocx message <sub> Loaded local Codex sessions and queued peer messages (sessions|send)"],
10
+ details: ["Linux/macOS Unix socket only. Uses effective CODEX_HOME; starts no daemon and returns no partial directory."],
11
+ },
12
+ {
13
+ command: ["message", "send"],
14
+ summary: "Submit one correlated peer message to an exact loaded local Codex destination.",
15
+ routes: [], mutates: true, json: "envelope",
16
+ flags: [
17
+ { name: "--thread", value: "string", summary: "Exact loaded UUID; choose this or --name." },
18
+ { name: "--name", value: "string", summary: "Unique exact loaded name; choose this or --thread." },
19
+ { name: "--stdin", value: "boolean", required: true, summary: "Read at most 16 KiB of UTF-8 message text." },
20
+ { name: "--kind", value: "string", summary: "request (default), response or notification." },
21
+ { name: "--in-reply-to", value: "string", summary: "Request message UUID; required only for a response." },
22
+ { name: "--json", value: "boolean", summary: "Emit a receipt with not_sent, queued or unknown status." },
23
+ ],
24
+ details: ["Requires daemon support for experimental thread/queue/add. Sender context comes from CODEX_THREAD_ID, not an authentication claim.",
25
+ "queued means submitted, not processed. unknown must not be replayed; no automatic retry, daemon start or thread resume.",
26
+ "Exit 0: queued; 1: not sent; 3: unknown; 64: invalid usage. No remote/Claude transport or skill installation."],
27
+ },
4
28
  {
5
29
  command: ["agent", "status"],
6
30
  usage: "ocx agent status [--json]",
@@ -111,6 +111,22 @@ export const INTEGRATION_CAPABILITIES: readonly Capability[] = [
111
111
  json: "payload",
112
112
  details: ["With --client, restore requires --profile; only Aside profiles accept that selector. No automatic drift confirmation or retry.","Preview preserves exact opId/profile/confirm-drift intent. Generic restore derives the client on the server; the returned plan does not embed opId or complete command input.","A refused or no-op preview is completed inspection, not applied work. A stale bound mutation exits5 with a fixed re-preview hint; no replacement token is automatically adopted."],
113
113
  },
114
+ {
115
+ command: ["commandcode", "restore"],
116
+ usage: "ocx commandcode restore --op <opId> [--confirm-drift] [--preview | --plan-fingerprint <token>] [--json]",
117
+ summary: "Restore a Command Code rollback operation, retaining client ownership and drift checks.",
118
+ routes: [{ method: "POST", path: "/api/client-integrations/commandcode/restore" }, { method: "POST", path: "/api/client-integrations/commandcode/restore/preview" }],
119
+ flags: [
120
+ { name: "--op", value: "string", summary: "Command Code operation ID; --op-id is an alias.", required: true },
121
+ { name: "--confirm-drift", value: "boolean", summary: "Explicitly allow replacing edits made after the snapshot." },
122
+ { name: "--preview", value: "boolean", summary: "Inspect restoration without mutating; exclusive with a commit fingerprint." },
123
+ { name: "--plan-fingerprint", value: "string", summary: "Optional versioned token from a matching preview; stale intent is refused without retry." },
124
+ { name: "--json", value: "boolean", summary: "Emit the result as JSON." },
125
+ ],
126
+ mutates: true,
127
+ json: "payload",
128
+ details: ["cmd restore is an alias. --client and --profile are rejected; the dedicated route binds the operation to Command Code.", "Older proxies without the dedicated route fail without a generic restore fallback. Redirects are refused.", "Preview preserves the operation and drift intent. A stale bound mutation requires a new explicit preview."],
129
+ },
114
130
  {
115
131
  command: ["claude", "config", "status"],
116
132
  usage: "ocx claude config status [--json]",
@@ -15,11 +15,14 @@ const DEFAULT_DEPS: CodexShimAutoRestoreCliDeps = {
15
15
  readConfig: readConfigDiagnostics,
16
16
  };
17
17
 
18
+ /** Identify command namespaces whose execution must not trigger automatic shim repair. */
18
19
  export function skipsCodexShimAutoRestore(command: string | undefined, args: string[]): boolean {
19
20
  if (command === "uninstall" || command === "remove") return true;
20
21
  if (command === "status" || command === "doctor") return true;
21
22
  // `lab` is read-only inspection; it must not trigger shim side effects.
22
23
  if (command === "lab") return true;
24
+ // Local messaging owns its helpers; even malformed usage must not repair/start anything.
25
+ if (command === "message") return true;
23
26
  // `resolve` is read-only inspection for embedding shells: a lookup made to populate
24
27
  // a consent surface must not trigger a shim repair side effect first.
25
28
  if (command === "resolve") return true;
@@ -145,6 +145,7 @@ const commandRunners: Record<string, CommandRunner> = {
145
145
  if (!deps.head.resolveArgs) return 64;
146
146
  return await deps.handleResolve(deps.head.resolveArgs);
147
147
  },
148
+ message: async deps => (await import("./message-command")).runMessageCommand(deps.args.slice(1)),
148
149
  restore: async deps => {
149
150
  const restoreArgs = deps.args.slice(1);
150
151
  const restoreJson = takeFlag(restoreArgs, "--json");
@@ -465,7 +466,7 @@ const commandRunners: Record<string, CommandRunner> = {
465
466
  },
466
467
  config,
467
468
  port: live.port,
468
- }, ["mcode", "pi", "raycast", "omo", "cline", "droid", "opencode", "kilo"]));
469
+ }, ["mcode", "pi", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"]));
469
470
  } catch (error) {
470
471
  console.warn(`Client integrations were not refreshed: ${error instanceof Error ? error.message : String(error)}`);
471
472
  }
@@ -961,6 +962,14 @@ const commandRunners: Record<string, CommandRunner> = {
961
962
  const { handleZcodeCommand } = await import("./integrations");
962
963
  return await handleZcodeCommand(deps.args.slice(1));
963
964
  },
965
+ commandcode: async deps => {
966
+ const { handleCommandcodeCommand } = await import("./integrations");
967
+ return await handleCommandcodeCommand(deps.args.slice(1));
968
+ },
969
+ cmd: async deps => {
970
+ const { handleCommandcodeCommand } = await import("./integrations");
971
+ return await handleCommandcodeCommand(deps.args.slice(1));
972
+ },
964
973
  help: async () => {
965
974
  printUsage();
966
975
  return 0;
package/src/cli/help.ts CHANGED
@@ -79,6 +79,7 @@ Usage:
79
79
  ocx alias <sub> Short names for providers and models (list, set, rm, defaults)
80
80
  ocx combo <sub> Combo routing strategies and failover
81
81
  ocx agent <sub> Subagents, injection, effort caps, and sidecars
82
+ ocx message <sub> Loaded local Codex sessions and queued peer messages (sessions|send)
82
83
  ocx effort [sub] Inspect and configure reasoning effort caps and defaults
83
84
  ocx observe <sub> Logs, usage, storage, memory, and debug data
84
85
  ocx inspect <sub> Effective config, catalog, analytics, pacing, client-config
@@ -91,7 +92,7 @@ Usage:
91
92
  ocx api-key <sub> Alias of ocx access key
92
93
  ocx access <sub> External API keys and endpoint information
93
94
  ocx api <sub> Protocol paths: vocabulary, request-path preview, and policy
94
- ocx export --client <id> Print a client config wired to the running proxy (17 clients)
95
+ ocx export --client <id> Print a client config wired to the running proxy (18 clients)
95
96
  ocx integration client <sub> Enable, disable, inspect or roll back a client integration
96
97
  ocx grok <sub> Grok Build model selection and apply
97
98
  ocx system <sub> Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection
@@ -105,6 +106,8 @@ Usage:
105
106
  ocx mcode [args...] Launch MiniMax Code through its managed provider
106
107
  ocx mmx text <sub> [args] Launch MiniMax CLI text through the proxy
107
108
  ocx zcode [sub] Connect ZCode to the proxy (managed provider)
109
+ ocx commandcode [sub] Connect Command Code CLI to the proxy (managed provider)
110
+ ocx cmd [sub] Alias of ocx commandcode
108
111
  ocx help [command] Show help
109
112
  ocx --version | -v Print version
110
113
 
package/src/cli/index.ts CHANGED
@@ -210,11 +210,17 @@ initializeNodeLauncherContext();
210
210
  // The compiled executable is also the capture-only MCP server's launcher.
211
211
  // Handle this private entrypoint before CLI preflight or command dispatch.
212
212
  if (process.argv[2] === "__keyring-load-check") { console.log(JSON.stringify((await import("../lib/keyring-native")).inspectKeyringBinding())); process.exit(0); }
213
- if (process.argv[2] === "__codebuddy-mcp") {
214
- const { runCodeBuddyMcpServer } = await import("../adapters/codebuddy/mcp-server");
215
- await runCodeBuddyMcpServer(process.argv[3] ?? "");
216
- // The MCP stdio loop owns this process until stdin closes; do not fall through
217
- // to ordinary CLI dispatch or exit after the handshake completes.
213
+
214
+ // The compiled executable also launches the isolated MCP servers.
215
+ // Handle these private entrypoints before CLI preflight or command dispatch.
216
+ if (process.argv[2] === "__codebuddy-mcp" || process.argv[2] === "__qoder-mcp") {
217
+ if (process.argv[2] === "__codebuddy-mcp") {
218
+ const { runCodeBuddyMcpServer } = await import("../adapters/codebuddy/mcp-server");
219
+ await runCodeBuddyMcpServer(process.argv[3] ?? "");
220
+ } else {
221
+ const { runCodingAgentMcpServer } = await import("../adapters/coding-agent/mcp-server");
222
+ await runCodingAgentMcpServer(process.argv[3] ?? "");
223
+ }
218
224
  await new Promise<never>(() => {});
219
225
  }
220
226