@bitkyc08/opencodex 2.57.0 → 2.59.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 (241) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-C5IebErG.js +136 -0
  3. package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
  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 +2 -2
  8. package/src/adapters/base.ts +11 -1
  9. package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
  10. package/src/adapters/command-code.ts +13 -4
  11. package/src/adapters/cursor/catalog.ts +11 -0
  12. package/src/adapters/cursor/cursor-errors.ts +15 -0
  13. package/src/adapters/cursor/discovery.ts +65 -1
  14. package/src/adapters/cursor/effort-map.ts +16 -2
  15. package/src/adapters/cursor/envelope-echo.ts +55 -2
  16. package/src/adapters/cursor/live-transport.ts +5 -1
  17. package/src/adapters/cursor/message-mapper.ts +3 -2
  18. package/src/adapters/cursor/protobuf-events.ts +110 -11
  19. package/src/adapters/cursor/protobuf-request.ts +27 -6
  20. package/src/adapters/cursor/request-builder.ts +14 -3
  21. package/src/adapters/cursor/text-toolcall.ts +230 -0
  22. package/src/adapters/cursor/thread-continuity.ts +141 -0
  23. package/src/adapters/cursor/tool-guidance.ts +5 -4
  24. package/src/adapters/cursor/types.ts +5 -0
  25. package/src/adapters/cursor.ts +97 -6
  26. package/src/adapters/devin/cloud-direct/chat.ts +11 -2
  27. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  28. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
  29. package/src/adapters/devin.ts +75 -13
  30. package/src/adapters/google-antigravity-wire.ts +29 -2
  31. package/src/adapters/google-http.ts +45 -13
  32. package/src/adapters/google.ts +23 -4
  33. package/src/adapters/mimo-free.ts +32 -17
  34. package/src/adapters/ollama-native.ts +42 -8
  35. package/src/adapters/openai-chat/response-events.ts +61 -0
  36. package/src/adapters/openai-chat.ts +5 -10
  37. package/src/adapters/openai-responses/passthrough.ts +40 -5
  38. package/src/adapters/openai-responses/request-strips.ts +43 -0
  39. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  40. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  41. package/src/adapters/physical-send.ts +50 -0
  42. package/src/adapters/responses-tool-schema.ts +76 -46
  43. package/src/adapters/run-turn-queue.ts +17 -4
  44. package/src/bridge/response-json.ts +2 -2
  45. package/src/bridge/sse.ts +166 -25
  46. package/src/claude/context-windows.ts +22 -0
  47. package/src/claude/outbound.ts +46 -5
  48. package/src/cli/account-api.ts +4 -3
  49. package/src/cli/account-extended.ts +22 -2
  50. package/src/cli/account-orca-import.ts +63 -0
  51. package/src/cli/account.ts +32 -4
  52. package/src/cli/capabilities.ts +40 -0
  53. package/src/cli/claude.ts +29 -1
  54. package/src/cli/codex-cli-update.ts +97 -2
  55. package/src/cli/config-command.ts +35 -18
  56. package/src/cli/dispatch.ts +71 -4
  57. package/src/cli/doctor.ts +197 -2
  58. package/src/cli/help.ts +4 -1
  59. package/src/cli/index.ts +132 -22
  60. package/src/cli/models-runtime.ts +33 -4
  61. package/src/cli/registry.ts +11 -1
  62. package/src/cli/runtime-api.ts +44 -0
  63. package/src/cli/start-args.ts +94 -0
  64. package/src/cli/system-command.ts +72 -1
  65. package/src/cli/uninstall-client-state.ts +12 -0
  66. package/src/client/machine-api.ts +4 -3
  67. package/src/client/machine-listener.ts +14 -1
  68. package/src/clients/config-export/constants.ts +2 -3
  69. package/src/clients/config-export.ts +5 -5
  70. package/src/codex/account-store.ts +81 -5
  71. package/src/codex/auth-api/pool-quota-probe.ts +14 -3
  72. package/src/codex/auth-api/routes.ts +17 -2
  73. package/src/codex/auth-context.ts +58 -20
  74. package/src/codex/catalog/build-entries.ts +25 -4
  75. package/src/codex/catalog/derive-entry.ts +8 -1
  76. package/src/codex/catalog/effort.ts +10 -6
  77. package/src/codex/catalog/gather-capture.ts +1 -0
  78. package/src/codex/catalog/model-hints.ts +37 -5
  79. package/src/codex/catalog/parsing.ts +83 -5
  80. package/src/codex/catalog/reserve-warn.ts +96 -0
  81. package/src/codex/catalog/retained-sync.ts +19 -0
  82. package/src/codex/catalog/routed-gather.ts +42 -3
  83. package/src/codex/cli-installation-identity.ts +210 -0
  84. package/src/codex/cli-installation-targets.ts +158 -0
  85. package/src/codex/convergence.ts +5 -0
  86. package/src/codex/desktop-switches.ts +145 -0
  87. package/src/codex/history-job.ts +5 -1
  88. package/src/codex/history-provider.ts +37 -5
  89. package/src/codex/history-state-open.ts +105 -0
  90. package/src/codex/history-worker.ts +14 -1
  91. package/src/codex/inject/config-toml.ts +44 -2
  92. package/src/codex/inject/remove.ts +145 -7
  93. package/src/codex/inject/restore.ts +204 -32
  94. package/src/codex/inject.ts +6 -9
  95. package/src/codex/lineage.ts +83 -32
  96. package/src/codex/loopback-target.ts +40 -0
  97. package/src/codex/main-account-hard-lock.ts +2 -1
  98. package/src/codex/main-account.ts +10 -3
  99. package/src/codex/main-device-reauth.ts +17 -9
  100. package/src/codex/model-entitlements.ts +60 -1
  101. package/src/codex/native-profile-startup.ts +64 -20
  102. package/src/codex/observed-model-denials.ts +137 -0
  103. package/src/codex/orca-auth-source.ts +94 -0
  104. package/src/codex/orca-import.ts +219 -0
  105. package/src/codex/prompt-text-probe.ts +282 -12
  106. package/src/codex/quota-401-recovery.ts +12 -0
  107. package/src/codex/quota-types.ts +65 -0
  108. package/src/codex/quota.ts +24 -19
  109. package/src/codex/routing/cooldown-math.ts +8 -47
  110. package/src/codex/routing/pin-drain.ts +57 -0
  111. package/src/codex/routing.ts +13 -15
  112. package/src/codex/subagent-model-fallback.ts +94 -0
  113. package/src/codex/windows-installation-files.ts +224 -0
  114. package/src/combos/failover.ts +122 -5
  115. package/src/config/atomic-write.ts +83 -8
  116. package/src/config/diagnostics.ts +21 -0
  117. package/src/config/load-degrade.ts +15 -0
  118. package/src/config/pending-teardown.ts +8 -0
  119. package/src/config/process-state.ts +36 -3
  120. package/src/config/provider-relative-send-path.ts +16 -0
  121. package/src/config/proxy-env.ts +23 -5
  122. package/src/config/schema/config-schema.ts +23 -0
  123. package/src/config/schema/leaf-validators.ts +65 -17
  124. package/src/generated/compatibility-version.json +337 -201
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/lib/bounded-body.ts +4 -2
  127. package/src/lib/bounded-subprocess.ts +62 -10
  128. package/src/lib/destination-policy.ts +48 -6
  129. package/src/lib/errors.ts +3 -15
  130. package/src/lib/local-destinations.ts +32 -5
  131. package/src/lib/provider-outbound.ts +3 -3
  132. package/src/lib/proxy-env.ts +70 -3
  133. package/src/lib/request-execution-budget.ts +11 -3
  134. package/src/lib/response-body-inactivity.ts +193 -0
  135. package/src/lib/retry-delay.ts +69 -0
  136. package/src/lib/socks5-fetch.ts +631 -0
  137. package/src/lib/spend-reservation-ledger.ts +115 -9
  138. package/src/lib/windows-secret-acl.ts +151 -15
  139. package/src/lib/windows-user-principal.ts +5 -1
  140. package/src/lib/workflow-budget.ts +145 -8
  141. package/src/oauth/account-quota-rank.ts +72 -15
  142. package/src/oauth/generic-account-failover.ts +40 -27
  143. package/src/oauth/orcarouter.ts +15 -2
  144. package/src/oauth/store.ts +8 -0
  145. package/src/providers/codex-capacity.ts +9 -0
  146. package/src/providers/derive.ts +6 -0
  147. package/src/providers/devin-provider-merge-migration.ts +33 -12
  148. package/src/providers/free-directory.ts +20 -2
  149. package/src/providers/key-failover.ts +261 -7
  150. package/src/providers/model-discovery.ts +19 -7
  151. package/src/providers/model-rename-migration.ts +1 -0
  152. package/src/providers/openai-sidecar.ts +4 -0
  153. package/src/providers/opencode-go-transport.ts +14 -5
  154. package/src/providers/quota/report-cache.ts +3 -0
  155. package/src/providers/registry/entries-core.ts +11 -0
  156. package/src/providers/registry/entries-extended.ts +146 -28
  157. package/src/providers/registry/model-seeds.ts +136 -29
  158. package/src/providers/registry/types.ts +9 -0
  159. package/src/responses/apply-patch-envelope.ts +44 -11
  160. package/src/responses/bridge-search-replay-cache.ts +152 -0
  161. package/src/responses/code-mode-helper-compat.ts +26 -16
  162. package/src/responses/custom-tool-compat.ts +1 -1
  163. package/src/responses/hosted-tool-policy.ts +85 -2
  164. package/src/responses/schema.ts +9 -2
  165. package/src/responses/spill-store.ts +17 -0
  166. package/src/responses/state/body-policy.ts +25 -0
  167. package/src/responses/state/spill-queue.ts +8 -6
  168. package/src/responses/state.ts +3 -22
  169. package/src/router.ts +4 -0
  170. package/src/server/auth-cors.ts +27 -0
  171. package/src/server/chat-completions.ts +9 -4
  172. package/src/server/chat-native-sse.ts +26 -9
  173. package/src/server/chat-native.ts +10 -4
  174. package/src/server/claude-messages.ts +24 -2
  175. package/src/server/gui-static.ts +36 -2
  176. package/src/server/inbound-body-admission.ts +187 -0
  177. package/src/server/index/websocket-handler.ts +48 -1
  178. package/src/server/index.ts +15 -19
  179. package/src/server/management/api-access.ts +3 -4
  180. package/src/server/management/config-routes.ts +57 -10
  181. package/src/server/management/provider-capability-config.ts +35 -7
  182. package/src/server/management/provider-routes.ts +70 -18
  183. package/src/server/models-capabilities.ts +24 -3
  184. package/src/server/proxy-liveness.ts +97 -2
  185. package/src/server/relay.ts +17 -24
  186. package/src/server/request-log.ts +25 -1
  187. package/src/server/responses/adapter-continuation.ts +71 -27
  188. package/src/server/responses/adapter-delivery.ts +39 -8
  189. package/src/server/responses/adapter-dispatch.ts +52 -24
  190. package/src/server/responses/codex-ws-exchange.ts +65 -4
  191. package/src/server/responses/combo-stream-preflight.ts +68 -5
  192. package/src/server/responses/compact.ts +60 -11
  193. package/src/server/responses/core-codex-account.ts +83 -22
  194. package/src/server/responses/core-combo.ts +26 -0
  195. package/src/server/responses/core-normalize.ts +12 -5
  196. package/src/server/responses/core-options.ts +3 -0
  197. package/src/server/responses/fetch-helpers.ts +72 -3
  198. package/src/server/responses/native-injection-protocol.ts +42 -0
  199. package/src/server/responses/native-injection-replay.ts +105 -0
  200. package/src/server/responses/native-injection.ts +242 -0
  201. package/src/server/responses/native-response-control.ts +56 -0
  202. package/src/server/responses/native-response-json.ts +14 -0
  203. package/src/server/responses/native-response-output.ts +37 -0
  204. package/src/server/responses/native-steering-log.ts +44 -0
  205. package/src/server/responses/native-steering-policy.ts +49 -0
  206. package/src/server/responses/native-steering-replay.ts +126 -0
  207. package/src/server/responses/native-steering-settings.ts +76 -0
  208. package/src/server/responses/native-steering.ts +400 -0
  209. package/src/server/responses/native-tool-results.ts +130 -0
  210. package/src/server/responses/passthrough-delivery.ts +21 -1
  211. package/src/server/responses/passthrough-dispatch.ts +146 -49
  212. package/src/server/responses/passthrough-execution.ts +11 -1
  213. package/src/server/responses/request-prepare.ts +70 -0
  214. package/src/server/responses/request-send-budget.ts +84 -7
  215. package/src/server/responses/request-sidecar-auth.ts +16 -8
  216. package/src/server/responses/request-spend.ts +38 -9
  217. package/src/server/responses/request-transport.ts +13 -10
  218. package/src/server/responses/run-turn-execution.ts +20 -5
  219. package/src/server/responses/sidecar-execution.ts +2 -0
  220. package/src/server/responses/ws-upstream.ts +23 -2
  221. package/src/server/responses-custom-tool-repair.ts +2 -2
  222. package/src/server/sse-frame-buffer.ts +12 -10
  223. package/src/server/sse-payload-rewrite.ts +36 -9
  224. package/src/server/stop-teardown.ts +8 -1
  225. package/src/server/system-env-shell.ts +5 -1
  226. package/src/server/system-env.ts +7 -1
  227. package/src/server/workflow-refusal.ts +56 -2
  228. package/src/server/ws-bridge.ts +16 -1
  229. package/src/service/cli.ts +29 -7
  230. package/src/service/guards.ts +10 -0
  231. package/src/service/health.ts +43 -0
  232. package/src/service/state.ts +7 -2
  233. package/src/types/accounts.ts +4 -0
  234. package/src/types/config.ts +104 -3
  235. package/src/types/provider.ts +32 -0
  236. package/src/types/request.ts +7 -1
  237. package/src/types/wire.ts +9 -1
  238. package/src/usage/expected-prices.ts +28 -0
  239. package/src/usage/log.ts +87 -4
  240. package/src/web-search/passthrough-bridge.ts +39 -5
  241. package/gui/dist/assets/index-Cz7CLdif.js +0 -128
@@ -1,7 +1,7 @@
1
1
  import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs";
2
2
  import { delimiter, join } from "node:path";
3
3
  import { getConfigDir } from "../config";
4
- import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
4
+ import { claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
5
5
  import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens, type AuthDetectDeps } from "../claude/auth-detect";
6
6
  import { resolveClaudeAuthMode } from "../claude/auth-mode";
7
7
  import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context";
@@ -128,6 +128,10 @@ export function writeShellEnvFile(
128
128
  if (config.claudeCode?.alwaysEnableEffort === true) {
129
129
  lines.push(conditional("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1"));
130
130
  }
131
+ // Tool-search deferral (#4838): opt-in, conditional like every other lever so a
132
+ // value already exported in the user's shell wins.
133
+ const toolSearch = claudeToolSearchEnv(config.claudeCode?.toolSearch);
134
+ if (toolSearch !== undefined) lines.push(conditional("ENABLE_TOOL_SEARCH", toolSearch));
131
135
  const shellEnvPath = getShellEnvFilePath();
132
136
  recordOwnedConfigPath(getConfigDir(), shellEnvPath);
133
137
  mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { getConfigDir } from "../config";
5
- import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
5
+ import { claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
6
6
  import { PROXY_MARKER } from "../claude/auth-detect";
7
7
  import { isProxyAdmissionSecret } from "./auth-cors";
8
8
  import type { OcxConfig } from "../types";
@@ -33,6 +33,7 @@ const MANAGED_SYSTEM_ENV_NAMES = new Set<string>([
33
33
  "DISABLE_COMPACT",
34
34
  "CLAUDE_CODE_AUTO_COMPACT_WINDOW",
35
35
  "CLAUDE_CODE_ALWAYS_ENABLE_EFFORT",
36
+ "ENABLE_TOOL_SEARCH",
36
37
  ]);
37
38
 
38
39
  interface SystemEnvTracking {
@@ -309,6 +310,11 @@ export async function injectSystemEnv(
309
310
  if (config.claudeCode?.alwaysEnableEffort === true) {
310
311
  injectLever("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1");
311
312
  }
313
+ // Tool-search deferral (#4838): same opt-in contract as `ocx claude`, so a machine
314
+ // whose Claude Code is launched from Dock/launchd gets the setting it configured
315
+ // instead of only terminal sessions. injectLever keeps a user-owned launchd value.
316
+ const toolSearch = claudeToolSearchEnv(config.claudeCode?.toolSearch);
317
+ if (toolSearch !== undefined) injectLever("ENABLE_TOOL_SEARCH", toolSearch);
312
318
 
313
319
  // Shell-hook env file: works for new shells in already-running Terminal.app.
314
320
  writeShellEnvFile(port, config, modelEnv, auto, deps);
@@ -5,6 +5,10 @@
5
5
  * check in `src/server/index.ts` and the pre-dispatch ceiling check in
6
6
  * `src/server/responses/core.ts` -- had drifted into saying different things about the same
7
7
  * refusal, and because the non-obvious part below has to be stated once and not twice.
8
+ *
9
+ * The HTTP admission DECISION lives here too, for the same reason: the lane derivation and the
10
+ * refusal it produces are two halves of one answer, and keeping them in one file is what stops
11
+ * a denial from losing the detail that makes it legible on the way out.
8
12
  */
9
13
  import { formatErrorResponse } from "../bridge";
10
14
  import {
@@ -14,9 +18,13 @@ import {
14
18
  } from "./request-log";
15
19
  import {
16
20
  WORKFLOW_LOCAL_REFUSAL_HEADER,
21
+ admitWorkflowTurn,
17
22
  workflowDenialSummary,
18
23
  recordWorkflowRefusalEvent,
19
24
  type WorkflowDenial,
25
+ type WorkflowDecision,
26
+ type WorkflowLane,
27
+ type WorkflowSpendDenialDetail,
20
28
  } from "../lib/workflow-budget";
21
29
 
22
30
  /**
@@ -54,11 +62,16 @@ export function workflowRefusalResponse(
54
62
  logCtx?: RequestLogContext,
55
63
  refusalLog?: WorkflowRefusalLog,
56
64
  rootId?: string,
65
+ spend?: WorkflowSpendDenialDetail,
57
66
  ): Response {
58
- const summary = workflowDenialSummary(reason);
67
+ // The denial detail, when the caller has it, is what turns "a token ceiling refused this"
68
+ // into "the account ceiling of 100,000 is spent". It reaches the message, the event ring and
69
+ // the log row through this one function, so the three cannot drift into saying different
70
+ // things about the same refusal -- which is what put this module here in the first place.
71
+ const summary = workflowDenialSummary(reason, spend);
59
72
  // Only a caller that decided the refusal ITSELF passes a root id. admitWorkflowTurn already
60
73
  // records its own denials, so passing one there would double-count them.
61
- if (rootId) recordWorkflowRefusalEvent(rootId, reason);
74
+ if (rootId) recordWorkflowRefusalEvent(rootId, reason, Date.now(), spend);
62
75
  const recordOn = logCtx ?? refusalLog?.logCtx;
63
76
  if (recordOn) {
64
77
  markLocalRequestLogRefusal(recordOn, summary.code);
@@ -82,3 +95,44 @@ export function workflowRefusalResponse(
82
95
  refusal.headers.set("Access-Control-Expose-Headers", WORKFLOW_LOCAL_REFUSAL_HEADER);
83
96
  return refusal;
84
97
  }
98
+
99
+ /**
100
+ * Admit one HTTP turn against its root workflow budget.
101
+ *
102
+ * A fan-out shares the conversation it serves. Without a reserve, a worker burst takes every
103
+ * slot under its own root and the interactive turn that started it waits behind its own
104
+ * children. A request that names a parent is treated as that fan-out; a top-level request is
105
+ * the conversation and may use the reserved slots.
106
+ */
107
+ export function admitHttpWorkflowTurn(headers: Headers): WorkflowDecision | undefined {
108
+ const rootId = headers.get("x-codex-parent-thread-id")?.trim() || undefined;
109
+ const threadId = headers.get("thread-id")?.trim() || undefined;
110
+ const lane: WorkflowLane = rootId !== undefined && threadId !== undefined && threadId !== rootId
111
+ ? "worker"
112
+ : "interactive";
113
+ return admitWorkflowTurn(rootId, lane, undefined, threadId);
114
+ }
115
+
116
+ /**
117
+ * The refusal for a decision {@link admitHttpWorkflowTurn} declined.
118
+ *
119
+ * It exists so the denial's own detail survives the trip to the wire. A token-ceiling denial
120
+ * arrives carrying the scope that refused and the number it refused against, and a caller that
121
+ * forwarded only `reason` would answer with a 429 that names no ceiling -- which is the
122
+ * illegible refusal this whole path is built to avoid. No root id is passed on: the admission
123
+ * check already recorded its own event, and passing one would count the refusal twice.
124
+ */
125
+ export function workflowDecisionRefusalResponse(
126
+ decision: Extract<WorkflowDecision, { admitted: false }>,
127
+ logCtx?: RequestLogContext,
128
+ refusalLog?: WorkflowRefusalLog,
129
+ ): Response {
130
+ const spend = decision.spendScope !== undefined && decision.spendLimit !== undefined
131
+ ? {
132
+ scope: decision.spendScope,
133
+ limit: decision.spendLimit,
134
+ ...(decision.spendProjected !== undefined ? { projected: decision.spendProjected } : {}),
135
+ }
136
+ : undefined;
137
+ return workflowRefusalResponse(decision.reason, logCtx, refusalLog, undefined, spend);
138
+ }
@@ -1,3 +1,4 @@
1
+ import type { NativeResponseControl } from "./responses/native-response-control";
1
2
  import type { ServerWebSocket } from "bun";
2
3
  import { responsesJsonEventSequence } from "./responses-json-events";
3
4
  import { FORWARD_HEADERS } from "../adapters/openai-responses";
@@ -17,6 +18,9 @@ type ResponsesTerminalReporter = (status: ResponsesTerminalStatus) => void;
17
18
  type ResponsesPayloadObserver = (payload: string) => void;
18
19
 
19
20
  export interface WsData {
21
+ nativeControl?: NativeResponseControl;
22
+ /** Content-free per-turn explanation; never a model capability assertion. */
23
+ nativeSteeringUnavailable?: string;
20
24
  headers?: Headers; // base inbound forward headers only; per-turn auth refresh injects current pool tokens
21
25
  /**
22
26
  * Resolved once at the handshake. Auth is handshake-time only on this path, so
@@ -229,6 +233,7 @@ export async function pumpResponsesSseToWebSocket(
229
233
  sseStream: ReadableStream<Uint8Array>,
230
234
  options: {
231
235
  isCurrent?: () => boolean;
236
+ untilEof?: boolean;
232
237
  onTerminal?: ResponsesTerminalReporter;
233
238
  onSsePayload?: ResponsesPayloadObserver;
234
239
  } = {},
@@ -251,6 +256,7 @@ export async function pumpResponsesSseToWebSocket(
251
256
  const decoder = new TextDecoder();
252
257
  const framer = new BoundedSseFrameBuffer();
253
258
  let terminalSeen = false;
259
+ let lastTerminal: ResponsesTerminalStatus | undefined;
254
260
 
255
261
  const handlePayload = (payload: string): boolean => {
256
262
  if (!isCurrent()) return true;
@@ -270,8 +276,10 @@ export async function pumpResponsesSseToWebSocket(
270
276
  }
271
277
  if (terminalSeen) return true;
272
278
  sendTextFrame(ws, payload);
273
- const terminalStatus = terminalStatusFromType(type);
279
+ if (options.untilEof && type === "response.created") lastTerminal = undefined;
280
+ const terminalStatus = type === "error" && options.untilEof ? "failed" : terminalStatusFromType(type);
274
281
  if (terminalStatus) {
282
+ if (options.untilEof) { lastTerminal = terminalStatus; return false; }
275
283
  reportTerminal(terminalStatus);
276
284
  terminalSeen = true;
277
285
  void reader.cancel().catch(() => {});
@@ -294,6 +302,10 @@ export async function pumpResponsesSseToWebSocket(
294
302
  const payload = parseSseBlock(decoder.decode(tail));
295
303
  if (payload) handlePayload(payload);
296
304
  }
305
+ if (options.untilEof && lastTerminal && isCurrent() && !clientCancelled) {
306
+ reportTerminal(lastTerminal);
307
+ terminalSeen = true;
308
+ }
297
309
  if (!terminalSeen && isCurrent() && !clientCancelled) {
298
310
  reportTerminal("incomplete");
299
311
  sendProtocolError(ws, 502, "Upstream stream ended before response terminal event");
@@ -368,6 +380,7 @@ export async function sendResponseToWebSocket(
368
380
  response: Response,
369
381
  isCurrent: () => boolean,
370
382
  options: {
383
+ untilEof?: boolean;
371
384
  onTerminal?: ResponsesTerminalReporter;
372
385
  onSsePayload?: ResponsesPayloadObserver;
373
386
  } = {},
@@ -398,6 +411,7 @@ export async function sendResponseToWebSocket(
398
411
  if (contentType.includes("text/event-stream")) {
399
412
  await pumpResponsesSseToWebSocket(ws, response.body, {
400
413
  isCurrent,
414
+ untilEof: options.untilEof,
401
415
  onTerminal: options.onTerminal,
402
416
  onSsePayload: options.onSsePayload,
403
417
  });
@@ -420,6 +434,7 @@ export async function sendResponseToWebSocket(
420
434
  if (looksLikeSse(prefix)) {
421
435
  await pumpResponsesSseToWebSocket(ws, stream, {
422
436
  isCurrent,
437
+ untilEof: options.untilEof,
423
438
  onTerminal: options.onTerminal,
424
439
  onSsePayload: options.onSsePayload,
425
440
  });
@@ -1,6 +1,7 @@
1
1
  import { existsSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { restoreNativeCodexAsync } from "../codex/inject";
4
+ import { describeRetainedCodexProviderTable } from "../codex/inject/restore";
4
5
  import { stripGrokConfig } from "../grok/inject";
5
6
  import { serviceApiTokenFilePath } from "../lib/service-secrets";
6
7
  import { statusWinswRaw, type WinswStatus } from "../lib/winsw";
@@ -196,23 +197,28 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
196
197
  assertServiceAuthEnvironment();
197
198
  // A throw used to escape straight to the top level, so the one command that can
198
199
  // leave a macOS hub evicted never reached its own serving check (#4236, defect 1f).
199
- // Report the failure, then still ask whether anything is listening: on darwin the
200
- // rollback inside installLaunchd may have brought the previous job back, and on
201
- // Windows the preserve/restart protocol may have done the same. The operator needs
202
- // both halves of that answer, and the exit code stays non-zero either way.
200
+ // Still ask whether anything is listening: on darwin the rollback inside installLaunchd
201
+ // may have brought the previous job back, and on Windows the preserve/restart protocol
202
+ // may have done the same. The operator needs both halves of that answer, and the exit
203
+ // code stays non-zero either way.
204
+ //
205
+ // The failure text travels INTO that report rather than being printed here. Printing it
206
+ // here and then letting the report reach its success line stated both outcomes for one
207
+ // run — "❌ Service repair failed: ... exit code 199" beside "✅ opencodex service
208
+ // repaired and serving on port 10100" — and the checkmark was the false half: the
209
+ // existing registration had been restarted, not repaired (#4914).
203
210
  let repairError: unknown;
204
211
  try {
205
212
  await repairService({ verb });
206
213
  } catch (error) {
207
214
  repairError = error;
208
- console.error(`❌ Service ${verb} failed: ${error instanceof Error ? error.message : String(error)}`);
209
215
  process.exitCode = 1;
210
216
  }
211
217
  // All three platforms: a repair that reports success while nothing serves is the
212
218
  // defect class this unit exists to close. Windows bakes its port into the
213
219
  // scheduler wrapper or the WinSW XML, both of which installedServiceListenPort()
214
220
  // now reads.
215
- await reportServiceServing(verb === "restart" ? "restarted" : "repaired");
221
+ await reportServiceServing(verb === "restart" ? "restarted" : "repaired", {}, repairError);
216
222
  if (repairError !== undefined) process.exitCode = 1;
217
223
  return;
218
224
  }
@@ -264,6 +270,11 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
264
270
  await maybeShowStarPrompt();
265
271
  break;
266
272
  case "start":
273
+ // The installed launcher preserves the recorded CODEX_SQLITE_HOME: a
274
+ // changed sqlite_home/CODEX_SQLITE_HOME/CODEX_HOME would start the service
275
+ // on the recorded database while this shell resolves another, splitting
276
+ // native Codex history between databases. Same guard `stop` already runs.
277
+ assertServiceEnvironmentMatchesInstall();
267
278
  ops.start();
268
279
  await reportServiceServing("started");
269
280
  break;
@@ -291,7 +302,15 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
291
302
  break;
292
303
  }
293
304
  const restore = await restoreNativeCodexAsync();
294
- if (restore.success) console.log("✅ service stopped + native Codex restored.");
305
+ if (restore.success) {
306
+ console.log("✅ service stopped + native Codex restored.");
307
+ // Success is not the whole answer when routing came down but the provider table
308
+ // stayed. Saying only "restored" here is how a user finds an unexplained
309
+ // opencodex table in their config weeks later (#4812).
310
+ if (restore.retainedCodexProviderTable) {
311
+ console.log(` ${describeRetainedCodexProviderTable(restore.retainedCodexProviderTable)}`);
312
+ }
313
+ }
295
314
  else console.error(`⚠️ service stopped, but native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` (or check $CODEX_HOME/config.toml) before using native Codex.`);
296
315
  if (!restore.success) process.exitCode = 1;
297
316
  // The Grok fence is the other managed config this command owns. Leaving it behind
@@ -339,6 +358,9 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
339
358
  console.error(`⚠️ native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` before using native Codex.`);
340
359
  process.exitCode = 1;
341
360
  }
361
+ else if (restore.retainedCodexProviderTable) {
362
+ console.log(`↩️ ${describeRetainedCodexProviderTable(restore.retainedCodexProviderTable)}`);
363
+ }
342
364
  const grok = stripGrokConfig();
343
365
  if (grok.changed) console.log(`↩️ ${grok.message}`);
344
366
  else if (!grok.ok) {
@@ -10,6 +10,7 @@ import { isTestHomeGuardArmed } from "../lib/test-home-guard";
10
10
  import { diagnoseService } from "./diagnostics";
11
11
  import type { ServiceDiagnostic } from "./diagnostics";
12
12
  import { currentCodexHome, currentOpenCodexHome, normalizePathForCompare, readServiceInstallState } from "./state";
13
+ import { resolveCodexSqliteHome } from "../codex/paths";
13
14
  import { win32 } from "node:path";
14
15
 
15
16
  /**
@@ -61,6 +62,15 @@ export function assertServiceEnvironmentMatchesInstall(): void {
61
62
  "Run the service command from the same OpenCodex home so service state and secrets match.",
62
63
  );
63
64
  }
65
+ if (state.codexSqliteHome !== undefined) {
66
+ const actualCodexSqliteHome = resolveCodexSqliteHome({ codexHome: actualCodexHome });
67
+ if (normalizePathForCompare(state.codexSqliteHome) !== normalizePathForCompare(actualCodexSqliteHome)) {
68
+ throw new ServiceOwnershipError(
69
+ `Service was installed with Codex SQLite home=${state.codexSqliteHome}, but the current Codex SQLite home=${actualCodexSqliteHome}. ` +
70
+ "Run the service command with the same sqlite_home configuration and CODEX_SQLITE_HOME so native Codex history restore updates the correct database.",
71
+ );
72
+ }
73
+ }
64
74
  }
65
75
 
66
76
  function isLoopbackHostname(hostname: string | undefined): boolean {
@@ -148,6 +148,19 @@ export async function confirmServiceServing(
148
148
  return { ok: false, port };
149
149
  }
150
150
 
151
+ /**
152
+ * The operation, as a noun, for a sentence that has to say it did not complete.
153
+ *
154
+ * The verb form reads as an accomplished fact ("service repaired"), which is exactly the
155
+ * claim that must not be made when the operation threw.
156
+ */
157
+ const SERVICE_OPERATION_NOUN = {
158
+ installed: "install",
159
+ started: "start",
160
+ repaired: "repair",
161
+ restarted: "restart",
162
+ } as const;
163
+
151
164
  /**
152
165
  * Print the outcome of `install` / `start` / `repair` in terms of what the user cares
153
166
  * about — is it serving? — instead of whether the manager accepted the registration.
@@ -156,10 +169,18 @@ export async function confirmServiceServing(
156
169
  * worker reads the child's exit status, so a registered-but-silent service now makes it
157
170
  * fall back to a direct proxy start rather than reporting a successful update over a
158
171
  * dead port.
172
+ *
173
+ * `precedingFailure` is the error the operation itself threw, when it threw. The serving
174
+ * probe still runs — a rollback or a preserve/restart protocol may well have left the
175
+ * previous job answering, and the operator needs that half of the answer (#4236). But the
176
+ * two halves have to be ONE sentence. Printing the failure separately and then reaching
177
+ * the success line here reported both outcomes for the same run and credited work that did
178
+ * not happen: the registration was kept, not repaired (#4914).
159
179
  */
160
180
  export async function reportServiceServing(
161
181
  verb: "installed" | "started" | "repaired" | "restarted",
162
182
  deps: Parameters<typeof confirmServiceServing>[0] = {},
183
+ precedingFailure?: unknown,
163
184
  ): Promise<void> {
164
185
  const healthBudgetMs = deps.timeoutMs ?? serviceInstallHealthMs();
165
186
  // Timed here rather than reported from the budget. confirmServiceServing knocks once
@@ -171,6 +192,28 @@ export async function reportServiceServing(
171
192
  const startedAt = now();
172
193
  const serving = await confirmServiceServing({ ...deps, timeoutMs: healthBudgetMs });
173
194
  const waitedMs = Math.max(0, now() - startedAt);
195
+ const failureDetail = precedingFailure === undefined
196
+ ? null
197
+ : precedingFailure instanceof Error ? precedingFailure.message : String(precedingFailure);
198
+ const operation = SERVICE_OPERATION_NOUN[verb];
199
+ if (failureDetail !== null) {
200
+ // Serving or not, the operation did not complete, so neither branch may print a
201
+ // checkmark. What differs is whether anything is answering, which is the fact the
202
+ // operator acts on next.
203
+ console.error(
204
+ serving.ok
205
+ ? `⚠️ Service ${operation} did not complete: ${failureDetail}\n`
206
+ + ` A proxy is answering on port ${serving.port}, so the existing registration was kept rather than replaced.\n`
207
+ + ` The ${operation} did not take effect; rerun it once the reported cause is resolved.\n`
208
+ + ` Log: ${serviceLogPath()}`
209
+ : `❌ Service ${operation} failed: ${failureDetail}\n`
210
+ + ` No proxy answered on port ${serving.port} after ${Math.round(waitedMs / 1000)}s either.\n`
211
+ + ` Log: ${serviceLogPath()}\n`
212
+ + ` Meanwhile: ocx start (serves in the foreground)`,
213
+ );
214
+ process.exitCode = 1;
215
+ return;
216
+ }
174
217
  if (serving.ok) {
175
218
  console.log(`✅ opencodex service ${verb} and serving on port ${serving.port}.`);
176
219
  return;
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { delimiter, dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
4
4
  import { expandUserPath, getConfigDir } from "../config";
5
5
  import { resolveCodexHomeDir, type CodexHomeDeps } from "../codex/home";
6
+ import { resolveCodexSqliteHome } from "../codex/paths";
6
7
  import { durableBunRuntime, type BunRuntimeSource, type DurableBunRuntime } from "../lib/bun-runtime";
7
8
  import { WINSW_SHA256, WINSW_VERSION } from "../lib/winsw";
8
9
  import { hardenSecretPath } from "../lib/windows-secret-acl";
@@ -218,6 +219,8 @@ export interface ServiceInstallState {
218
219
  version: 1 | 2;
219
220
  codexHome: string;
220
221
  opencodexHome: string;
222
+ /** Effective Codex SQLite home used by this service's history integration. */
223
+ codexSqliteHome?: string;
221
224
  /** Baked at install; lets status flag paths gone stale after npm prefix/nvm moves. */
222
225
  bunPath?: string;
223
226
  cliPath?: string;
@@ -241,7 +244,7 @@ export function parseServiceInstallState(value: unknown): ServiceInstallState |
241
244
  if (state.version !== 1 && state.version !== 2) return null;
242
245
  if (typeof state.codexHome !== "string" || state.codexHome.length === 0) return null;
243
246
  if (typeof state.opencodexHome !== "string" || state.opencodexHome.length === 0) return null;
244
- for (const key of ["bunPath", "cliPath", "launcherPath", "winswVersion", "winswSha256"] as const) {
247
+ for (const key of ["codexSqliteHome", "bunPath", "cliPath", "launcherPath", "winswVersion", "winswSha256"] as const) {
245
248
  if (state[key] !== undefined && (typeof state[key] !== "string" || state[key].length === 0)) return null;
246
249
  }
247
250
  if (state.version === 1) {
@@ -254,10 +257,12 @@ export function parseServiceInstallState(value: unknown): ServiceInstallState |
254
257
 
255
258
  export function writeServiceInstallState(backend: ServiceBackend = "scheduler", launcherPath?: string | null): void {
256
259
  const { bun, cli } = cliEntry();
260
+ const codexHome = currentCodexHome();
257
261
  const state: ServiceInstallState = {
258
262
  version: 2,
259
- codexHome: currentCodexHome(),
263
+ codexHome,
260
264
  opencodexHome: currentOpenCodexHome(),
265
+ codexSqliteHome: resolveCodexSqliteHome({ codexHome }),
261
266
  bunPath: bun,
262
267
  cliPath: cli,
263
268
  ...(launcherPath ? { launcherPath } : {}),
@@ -19,6 +19,10 @@ export interface CodexAccount {
19
19
  }
20
20
 
21
21
  export interface CodexAccountCredentials {
22
+ /** Read-only Orca auth.json link; Orca exclusively owns OAuth refresh. */
23
+ sourceAuthPath?: string;
24
+ /** Bind the link to the originally imported OAuth subject. */
25
+ sourceSubject?: string;
22
26
  accessToken: string;
23
27
  refreshToken: string;
24
28
  expiresAt: number;
@@ -93,6 +93,31 @@ export interface OcxClaudeCodeConfig {
93
93
  * forcing effort on every request can leak reasoning params to non-reasoning routes.
94
94
  */
95
95
  alwaysEnableEffort?: boolean;
96
+ /**
97
+ * Opt-in ENABLE_TOOL_SEARCH injection for launched Claude Code sessions (#4838).
98
+ *
99
+ * Claude Code turns MCP tool deferral off whenever ANTHROPIC_BASE_URL names a
100
+ * non-first-party host, so an `ocx claude` session ships every tool schema in
101
+ * full on every request. Its own diagnostic states the precondition for turning
102
+ * that back on: "Set ENABLE_TOOL_SEARCH=true (or auto / auto:N) if your proxy
103
+ * forwards tool_reference blocks."
104
+ *
105
+ * Default OFF, because opencodex only forwards them on the NATIVE ANTHROPIC
106
+ * PASSTHROUGH route, where the body reaches Anthropic untouched. On a translated
107
+ * route the deferral shape is not representable: compatibility.ts marks
108
+ * tool_search/tool_reference/deferred_tools unsupported, and toolsToResponses
109
+ * drops the tool_search server tool while ignoring defer_loading — deferred tools
110
+ * still carry input_schema on the wire, so the routed provider receives every
111
+ * schema anyway while Claude Code stops accounting for them and therefore stops
112
+ * compacting. Under `claudeCode.compatibility: "enforce"` the same request is
113
+ * rejected with 400 instead.
114
+ *
115
+ * `true` injects "true"; a string is passed through verbatim so Claude Code's own
116
+ * vocabulary (`auto`, `auto:N`, `force`) stays reachable. `false` and absent
117
+ * inject nothing — they do not force the variable off, because a value the
118
+ * operator exported themselves always wins.
119
+ */
120
+ toolSearch?: boolean | string;
96
121
  /**
97
122
  * Subagent tier slots (devlog 260712 B2): injected as ANTHROPIC_DEFAULT_*_MODEL so
98
123
  * Claude Code's Agent-tool aliases (opus/sonnet/haiku/fable + parent-inherit) route
@@ -722,9 +747,11 @@ export interface OcxConfig {
722
747
  | { enabled: false }
723
748
  | { enabled: true; port?: number };
724
749
  /**
725
- * Outbound HTTP(S) proxy URL for provider requests (e.g. "http://user:pass@proxy:8080", or
726
- * "${HTTPS_PROXY}"-style env reference). Mirrored into HTTP_PROXY/HTTPS_PROXY at startup when
727
- * those are unset — Bun's fetch honors them for all outbound calls; localhost is excluded.
750
+ * Outbound proxy URL for provider requests. HTTP(S) example: "http://user:pass@proxy:8080"
751
+ * or "${HTTPS_PROXY}". SOCKS5 example: "socks5://127.0.0.1:10808" (`ocx start --socks5`).
752
+ * HTTP URLs are mirrored into HTTP_PROXY/HTTPS_PROXY when unset. SOCKS5 URLs are mirrored
753
+ * into ALL_PROXY, clear inherited HTTP(S)_PROXY, and use OpenCodex's SOCKS5 transport.
754
+ * Loopback stays in NO_PROXY.
728
755
  * The literal `"auto"` reads the Windows WinINET static proxy (`ProxyEnable`/`ProxyServer`)
729
756
  * once at process start; on other platforms, or when the system proxy is off, SOCKS-only,
730
757
  * or unreadable, it degrades to direct egress with one log line (#1525). PAC/WPAD and live
@@ -749,6 +776,10 @@ export interface OcxConfig {
749
776
  shutdownTimeoutMs?: number;
750
777
  /** Advertise supports_websockets so Codex opens the WS endpoint. Default false; set true to opt in. */
751
778
  websockets?: boolean;
779
+ /** Experimental single-lane native OpenAI WebSocket steering; default off. */
780
+ codexNativeSteering?: boolean;
781
+ /** Experimental, default-off saved function-result injection on native multi-agent WebSockets. */
782
+ codexNativeInjection?: boolean;
752
783
  /**
753
784
  * Opt-in auto-cleanup policy for archived Codex sessions (issue #42 Phase 3).
754
785
  * Default OFF (`enabled` false / unset). Never enabled implicitly.
@@ -774,6 +805,20 @@ export interface OcxConfig {
774
805
  * built-in `openai` provider, so Codex does not select native remote compaction. Default off.
775
806
  */
776
807
  codexClientCompaction?: boolean;
808
+ /**
809
+ * Label Codex shows for the injected `opencodex` provider. Defaults to `OpenCodex Proxy`.
810
+ *
811
+ * Presentation only. Routing is keyed on the provider id `opencodex` — the root
812
+ * `model_provider = "opencodex"` line and the `[model_providers.opencodex]` header — and this
813
+ * setting never touches either, so renaming the label cannot reroute or orphan a thread whose
814
+ * row already names that id.
815
+ *
816
+ * There is no way to emit an empty label: Codex rejects a provider with no name, so a blank,
817
+ * over-long, or control-character value falls back to the default rather than writing a config
818
+ * Codex would refuse to load. "Suppressing" the OpenCodex branding therefore means choosing a
819
+ * neutral label, not removing the field.
820
+ */
821
+ codexProviderDisplayName?: string;
777
822
  /**
778
823
  * Compatibility mode: temporarily rewrite Codex resume-history metadata while the proxy is active
779
824
  * so Codex App can show old OpenAI chats and opencodex-created exec chats under its default
@@ -807,6 +852,16 @@ export interface OcxConfig {
807
852
  * absence is the only default state this policy has.
808
853
  */
809
854
  codexPool?: OcxCodexPoolConfig;
855
+ /**
856
+ * Durable token ceilings for the spend-reservation ledger (#4546). Absent means the
857
+ * historical behaviour exactly: token spend is still accounted and journalled, and nothing
858
+ * is refused on it.
859
+ *
860
+ * Not in `getDefaultConfig()` on purpose, and deliberately shipped with no default figure.
861
+ * The ledger is on by default, so a default ceiling would start refusing real traffic on
862
+ * upgrade against a number nobody chose.
863
+ */
864
+ spend?: OcxSpendConfig;
810
865
  /** Opt-in per-account activation of newly reset Codex quota windows. */
811
866
  codexQuotaAutoRefresh?: Record<string, {
812
867
  fiveHour?: boolean;
@@ -1381,3 +1436,49 @@ export interface OcxCatalogAutoRefreshConfig {
1381
1436
  */
1382
1437
  intervalMinutes?: number;
1383
1438
  }
1439
+
1440
+ /**
1441
+ * One scope's token ceiling.
1442
+ *
1443
+ * An object rather than a bare number because the ledger's scope limit is already a record in
1444
+ * `SpendReservationPolicy`, and a config shape that mirrors the runtime one cannot drift from
1445
+ * it silently. Absent `maxTokens` is the same as an absent scope: observe only.
1446
+ */
1447
+ export interface OcxSpendScopeConfig {
1448
+ /**
1449
+ * Tokens the scope may hold at once, counting settled spend, open reservations and
1450
+ * unresolved spend. A reservation is the request's whole input plus its enforceable output
1451
+ * ceiling, so this is compared against a number that assumes every cached prefix misses.
1452
+ *
1453
+ * There is no default. A ceiling is a number only the operator knows -- it depends on the
1454
+ * plan, the account roster and what the install is for -- and the recorded lesson from the
1455
+ * observational phase of #4546 is that guessing one is worse than shipping none.
1456
+ */
1457
+ maxTokens?: number;
1458
+ }
1459
+
1460
+ /**
1461
+ * Durable spend ceilings (#4546).
1462
+ *
1463
+ * The three scopes intersect: a request is admitted only when its own root workflow, the
1464
+ * identity that would serve it, and the pool it would draw from all have room. That is what
1465
+ * makes the ceiling hold against a caller that mints a fresh root id per request -- the root
1466
+ * is new, the identity and pool are not.
1467
+ *
1468
+ * Every field is optional and an empty section is the same as no section at all.
1469
+ */
1470
+ export interface OcxSpendConfig {
1471
+ /** Ceiling for one root workflow -- the user-visible task, including its whole fan-out. */
1472
+ root?: OcxSpendScopeConfig;
1473
+ /** Ceiling for one authenticated identity, across every root it serves. */
1474
+ identity?: OcxSpendScopeConfig;
1475
+ /** Ceiling for one account pool, across every identity in it. */
1476
+ pool?: OcxSpendScopeConfig;
1477
+ /**
1478
+ * Days a dormant scope's accounting is retained. Default 7.
1479
+ *
1480
+ * A scope is dropped only when it is both idle and under its ceiling, so shortening this
1481
+ * cannot hand an exhausted scope a fresh allowance.
1482
+ */
1483
+ retentionDays?: number;
1484
+ }
@@ -311,6 +311,19 @@ export interface OcxProviderConfig {
311
311
  * preserved after it, and parallel calls stay together with the reasoning turn that produced them.
312
312
  */
313
313
  requiresAdjacentResponsesToolResults?: boolean;
314
+ /**
315
+ * Responses upstream whose parser also rejects a tool call that has no matching output
316
+ * anywhere in the replayed input, not merely one whose result sits out of order. A call left
317
+ * dangling by an interrupted stream is answered with an explicit unknown-status placeholder
318
+ * so the thread can continue.
319
+ *
320
+ * Separate from `requiresAdjacentResponsesToolResults` on purpose: adjacency reorders items a
321
+ * strict parser already accepts in some order, while this synthesizes an item the client never
322
+ * sent. Kimi accepts a dangling call (#4726), so it must not inherit the synthesis.
323
+ * `statelessResponses` implies this, because an upstream that stores nothing cannot resolve
324
+ * the missing half from its own history either.
325
+ */
326
+ requiresPairedResponsesToolResults?: boolean;
314
327
  /**
315
328
  * When enabled, a tool result that is present but empty (no usable text or content
316
329
  * part) is rewritten to an explicit annotation before it reaches the upstream wire,
@@ -605,6 +618,8 @@ export interface OcxProviderConfig {
605
618
  reasoningEfforts?: string[];
606
619
  /** Model-specific Codex-visible reasoning tiers. An empty array means “do not expose effort”. */
607
620
  modelReasoningEfforts?: Record<string, string[]>;
621
+ /** Catalog-only: do not synthesize a missing max rung for matching routed models. */
622
+ modelSuppressSyntheticMax?: Record<string, boolean>;
608
623
  /** Model-specific default Codex reasoning tier; must also be present in the visible tier list. */
609
624
  modelDefaultReasoningEfforts?: Record<string, string>;
610
625
  /** Operator-owned effort override; none omits effort and uses the provider default. */
@@ -664,6 +679,23 @@ export interface OcxProviderConfig {
664
679
  * apply_patch passthrough compatibility for OpenAI and unclassified gateways.
665
680
  */
666
681
  supportsResponsesCustomTools?: boolean;
682
+ /**
683
+ * Hosted tool declarations this Responses destination rejects, so they are stripped from
684
+ * the request instead of being forwarded and 400'd.
685
+ *
686
+ * This is how an OpenAI-compatible gateway with a narrower capability set than OpenAI
687
+ * describes itself. Before it existed, a destination that accepted plain Responses and
688
+ * `function` tools but rejected hosted `web_search` could only be handled by adding a
689
+ * hard-coded baseUrl rule to `src/responses/hosted-tool-policy.ts`, so every such gateway
690
+ * needed a proxy release; a text-only prompt like "Reply exactly with OK" failed before
691
+ * the model answered because the hosted declaration travelled with it (#5002).
692
+ *
693
+ * Values come from `DECLARABLE_HOSTED_TOOL_TYPES`. Spelling variants of one capability
694
+ * are aliased, so `["web_search"]` also denies `web_search_preview`. Pair this with
695
+ * `supportsResponsesCustomTools: false` for a gateway that also rejects native custom
696
+ * tools; the two capabilities are independent and denied independently.
697
+ */
698
+ unsupportedHostedTools?: string[];
667
699
  /**
668
700
  * Provider-local repair for Responses gateways whose lifecycle snapshots omit canonical
669
701
  * fields or closing events (#893). Disabled by default and applied only to client-facing
@@ -68,6 +68,12 @@ export interface OcxParsedRequest {
68
68
  _cursorConversationId?: string;
69
69
  /** Stable upstream client thread identity, used only to derive provider-scoped continuation ids. */
70
70
  _clientThreadId?: string;
71
+ /**
72
+ * This request's OWN Codex thread id (`thread-id`), as opposed to `_clientThreadId`, which
73
+ * carries `x-codex-parent-thread-id` and is therefore shared by every parallel child of one
74
+ * parent. Only a surface that must distinguish siblings should read it.
75
+ */
76
+ _codexOwnThreadId?: string;
71
77
  /** True when promptCacheKey identifies a shared cache cohort rather than one conversation. */
72
78
  _promptCacheKeyIsSharedCohort?: boolean;
73
79
  /** Cursor-only thread owner; may be an opaque process-local Desktop session/thread identity. */
@@ -312,7 +318,7 @@ export interface OcxProviderContinuationState {
312
318
  }
313
319
 
314
320
  export type AdapterEvent =
315
- | { type: "heartbeat" }
321
+ | { type: "heartbeat"; replayUnsafe?: true }
316
322
  | { type: "text_delta"; text: string; phase?: OcxMessagePhase }
317
323
  | { type: "thinking_delta"; thinking: string }
318
324
  // Anthropic extended-thinking round-trip: signature_delta for the current thinking block, and