@bitkyc08/opencodex 2.10.0 → 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -24,11 +24,13 @@
24
24
  * up to the drain window.
25
25
  */
26
26
 
27
- import { buildFailedTailPayload } from "./relay";
27
+ import { buildFailedTailPayload, createSseTerminalOutputBoundary } from "./relay";
28
28
  import {
29
29
  nextSseBlock,
30
+ payloadRewriteAsBlockRewrite,
30
31
  replaceSseDataPayload,
31
32
  sseDataPayload,
33
+ type SseBlockRewrite,
32
34
  type SsePayloadRewrite,
33
35
  } from "./sse-payload-rewrite";
34
36
  import type { TranslatorBudget } from "../lib/translator-budget";
@@ -43,6 +45,12 @@ export type EagerRelayHooks = {
43
45
  * Bun#32111-unsafe tee()+JS-pull chain (#864).
44
46
  */
45
47
  rewritePayload?: SsePayloadRewrite;
48
+ /**
49
+ * Optional block-level rewrite (zero or more blocks out per upstream
50
+ * block) for lifecycle event injection (#893). Takes precedence over
51
+ * rewritePayload when both are set.
52
+ */
53
+ rewriteBlocks?: SseBlockRewrite;
46
54
  /** Flush inspection at upstream end (createSseInspector.finish). */
47
55
  finishInspection: () => void;
48
56
  /** Drop inspector-owned frame/item state during producer teardown. */
@@ -92,9 +100,11 @@ export function relaySseEagerBounded(
92
100
  const now = opts?.now ?? Date.now;
93
101
 
94
102
  const reader = body.getReader();
95
- const rewrite = hooks.rewritePayload;
96
- const rewriteDecoder = rewrite ? new TextDecoder() : null;
97
- const rewriteEncoder = rewrite ? new TextEncoder() : null;
103
+ const terminalBoundary = createSseTerminalOutputBoundary();
104
+ const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks
105
+ ?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined);
106
+ const rewriteDecoder = activeRewrite ? new TextDecoder() : null;
107
+ const rewriteEncoder = activeRewrite ? new TextEncoder() : null;
98
108
  const rewriteBudget = opts?.rewriteBudget;
99
109
  let frameBuffer = "";
100
110
  let frameBufferBytes = 0;
@@ -121,15 +131,9 @@ export function relaySseEagerBounded(
121
131
  for (;;) {
122
132
  const next = nextSseBlock(frameBuffer);
123
133
  if (!next) break;
124
- const payload = sseDataPayload(next.block);
125
- const rewrittenPayload = payload === null ? null : rewrite!(payload);
126
- // Replace only on an actual change: replaceSseDataPayload collapses
127
- // multi-data-line events and normalizes newline style even when the
128
- // payload is identical, which corrupts valid streams.
129
- const block = payload !== null && rewrittenPayload !== payload
130
- ? replaceSseDataPayload(next.block, rewrittenPayload!)
131
- : next.block;
132
- out += block + next.delimiter;
134
+ for (const outBlock of activeRewrite!(next.block)) {
135
+ out += outBlock + next.delimiter;
136
+ }
133
137
  frameBuffer = next.rest;
134
138
  }
135
139
  if (rewriteBudget) {
@@ -143,14 +147,13 @@ export function relaySseEagerBounded(
143
147
  };
144
148
  /** Flush any trailing partial block at upstream end (rewrite applied, matching the pull relay). */
145
149
  const flushRewriteTail = (): Uint8Array => {
146
- if (!rewrite) return new Uint8Array(0);
150
+ if (!activeRewrite) return new Uint8Array(0);
147
151
  // Decoder-flushed bytes logically follow everything already decoded.
148
152
  let tail = frameBuffer + rewriteDecoder!.decode();
149
- const payload = sseDataPayload(tail);
150
- if (payload !== null) {
151
- const rewrittenPayload = rewrite(payload);
152
- if (rewrittenPayload !== payload) tail = replaceSseDataPayload(tail, rewrittenPayload);
153
- }
153
+ const rewritten = activeRewrite(tail);
154
+ // Multiple emitted blocks must stay separately framed (#893 review);
155
+ // join places the delimiter only between blocks, never after the last.
156
+ tail = rewritten.join(tail.includes("\r\n") ? "\r\n\r\n" : "\n\n");
154
157
  frameBuffer = "";
155
158
  if (rewriteBudget && frameBufferBytes > 0) {
156
159
  rewriteBudget.releaseRetained(frameBufferBytes, { kind: "live_transient" });
@@ -161,6 +164,7 @@ export function relaySseEagerBounded(
161
164
  let queuedBytes = 0;
162
165
  let cancelled = false;
163
166
  let done = false;
167
+ const terminalSentinel = new TextEncoder().encode("data: [DONE]\n\n");
164
168
  // Pause gate: resolved by client pull, client cancel, or upstream abort so a
165
169
  // paused producer ALWAYS resumes (audit blocker 2 — no deadlock; onDone and
166
170
  // turn unregistration stay reachable, drainAndShutdown never hangs).
@@ -194,33 +198,45 @@ export function relaySseEagerBounded(
194
198
  let syntheticKind: "incomplete" | "failed" | null = null;
195
199
  // reader.read() is not intrinsically tied to the upstream AbortController
196
200
  // (a fetch body usually rejects on abort, but that coupling is the fetch
197
- // implementation's, not the stream's). Race every read against the abort
198
- // signal so cancel-drain expiry and shutdown teardown ALWAYS break the
199
- // loop even on a silent upstream.
200
- const aborted: Promise<"aborted"> = new Promise(resolve => {
201
- if (upstream.signal.aborted) resolve("aborted");
202
- else upstream.signal.addEventListener("abort", () => resolve("aborted"), { once: true });
203
- });
201
+ // implementation's, not the stream's), so abort must break a parked read on
202
+ // a silent upstream. Cancelling the reader does that: the pending read
203
+ // settles and the loop observes the abort. This is deliberately NOT a
204
+ // shared `Promise.race([reader.read(), aborted])` companion racing every
205
+ // read against one never-settled promise retains a reaction per chunk, and
206
+ // that is the exact retention class relay.ts avoids at its own drain.
207
+ const wakeParkedRead = () => { reader.cancel(upstream.signal.reason).catch(() => {}); };
208
+ if (upstream.signal.aborted) wakeParkedRead();
209
+ else upstream.signal.addEventListener("abort", wakeParkedRead, { once: true });
204
210
  try {
205
211
  for (;;) {
206
- const result = await Promise.race([reader.read(), aborted]);
207
- if (result === "aborted") break;
212
+ const result = await reader.read();
208
213
  const { done: upstreamDone, value } = result;
214
+ // A chunk that already settled is INSPECTED before abort is honored. A read
215
+ // can settle with a real chunk in the same tick the signal fires (post-cancel
216
+ // drain: the terminal frame arrives, then the drain timer aborts upstream).
217
+ // Checking the signal first discarded that frame, so the terminal was never
218
+ // recorded and the turn was accounted as a plain cancel.
219
+ if (!upstreamDone && value !== undefined) hooks.inspectChunk(value);
220
+ if (upstream.signal.aborted) break;
209
221
  if (upstreamDone) {
210
222
  hooks.finishInspection();
211
- if (rewrite) {
212
- const tail = flushRewriteTail();
223
+ const boundedTail = terminalBoundary.finish();
224
+ if (activeRewrite) {
225
+ const rewritten = rewriteOutbound(boundedTail);
226
+ const tail = joinUint8Arrays(rewritten, flushRewriteTail());
213
227
  if (tail.byteLength > 0 && !cancelled) {
214
228
  queuedBytes += tail.byteLength;
215
229
  try { controllerRef?.enqueue(tail); } catch { /* client already gone */ }
216
230
  }
231
+ } else if (boundedTail.byteLength > 0 && !cancelled) {
232
+ queuedBytes += boundedTail.byteLength;
233
+ try { controllerRef?.enqueue(boundedTail); } catch { /* client already gone */ }
217
234
  }
218
235
  if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
219
236
  syntheticKind = "incomplete";
220
237
  }
221
238
  break;
222
239
  }
223
- hooks.inspectChunk(value);
224
240
  if (cancelled) {
225
241
  // Discard-drain: inspection only, nothing queued. Stop at terminal
226
242
  // or when the bounded window expires.
@@ -230,17 +246,30 @@ export function relaySseEagerBounded(
230
246
  }
231
247
  continue;
232
248
  }
233
- const outbound = rewrite ? rewriteOutbound(value) : value;
234
- if (outbound.byteLength === 0) continue;
235
- queuedBytes += outbound.byteLength;
236
- try {
237
- controllerRef?.enqueue(outbound);
238
- } catch {
239
- // Controller already torn down (client went away without cancel()).
240
- cancelled = true;
241
- drainDeadline = now() + drainMs;
242
- armDrainTimer();
243
- continue;
249
+ const terminalBounded = terminalBoundary.feed(value);
250
+ const outbound = activeRewrite ? rewriteOutbound(terminalBounded) : terminalBounded;
251
+ if (outbound.byteLength > 0) {
252
+ queuedBytes += outbound.byteLength;
253
+ try {
254
+ controllerRef?.enqueue(outbound);
255
+ } catch {
256
+ // Controller already torn down (client went away without cancel()).
257
+ cancelled = true;
258
+ drainDeadline = now() + drainMs;
259
+ armDrainTimer();
260
+ continue;
261
+ }
262
+ }
263
+ if (terminalBoundary.terminalSeen()) {
264
+ // The Responses terminal event ends the turn even when a compatible
265
+ // gateway keeps its HTTP connection alive. Add the conventional
266
+ // sentinel and stop the single-reader relay at that protocol boundary.
267
+ if (!terminalBoundary.doneSeen()) {
268
+ queuedBytes += terminalSentinel.byteLength;
269
+ try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
270
+ }
271
+ reader.cancel("Responses terminal event received").catch(() => {});
272
+ break;
244
273
  }
245
274
  while (queuedBytes > maxQueueBytes && !cancelled && !upstream.signal.aborted) {
246
275
  await paused();
@@ -272,6 +301,7 @@ export function relaySseEagerBounded(
272
301
  try { rewriteBudget.releaseRetained(frameBufferBytes, { kind: "live_transient" }); } catch { /* teardown must not throw */ }
273
302
  frameBufferBytes = 0;
274
303
  }
304
+ terminalBoundary.dispose();
275
305
  if (syntheticKind) hooks.onSynthetic(syntheticKind);
276
306
  if (cancelled && !hooks.sawTerminal()) {
277
307
  hooks.onClientCancel();
@@ -284,6 +314,7 @@ export function relaySseEagerBounded(
284
314
  try { controllerRef?.close(); } catch { /* already closed/errored */ }
285
315
  }
286
316
  try { hooks.disposeInspection?.(); } catch { /* inspection teardown must not block lifecycle cleanup */ }
317
+ try { activeRewrite?.dispose?.(); } catch { /* rewrite teardown must not block lifecycle cleanup */ }
287
318
  fireDone();
288
319
  }
289
320
  };
@@ -311,3 +342,12 @@ export function relaySseEagerBounded(
311
342
  },
312
343
  });
313
344
  }
345
+
346
+ function joinUint8Arrays(first: Uint8Array, second: Uint8Array): Uint8Array {
347
+ if (first.byteLength === 0) return second;
348
+ if (second.byteLength === 0) return first;
349
+ const joined = new Uint8Array(first.byteLength + second.byteLength);
350
+ joined.set(first);
351
+ joined.set(second, first.byteLength);
352
+ return joined;
353
+ }
@@ -95,6 +95,79 @@ export function buildFailedTailPayload(err: unknown): string {
95
95
  });
96
96
  }
97
97
 
98
+ export type SseTerminalOutputBoundary = {
99
+ feed(chunk: Uint8Array): Uint8Array;
100
+ finish(): Uint8Array;
101
+ terminalSeen(): boolean;
102
+ doneSeen(): boolean;
103
+ dispose(): void;
104
+ };
105
+
106
+ /**
107
+ * Frame-aware client output boundary shared by both native Responses relays.
108
+ * It buffers only the current incomplete SSE block, forwards complete blocks
109
+ * through the first Responses terminal, and drops every later block/byte.
110
+ */
111
+ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
112
+ let decoder: TextDecoder | null = new TextDecoder();
113
+ const encoder = new TextEncoder();
114
+ let buffer = "";
115
+ let terminal = false;
116
+ let done = false;
117
+ let disposed = false;
118
+
119
+ const process = (flush: boolean): Uint8Array => {
120
+ if (disposed || terminal) return new Uint8Array(0);
121
+ let output = "";
122
+ let responsesTerminal = false;
123
+ for (;;) {
124
+ const next = nextSseBlock(buffer);
125
+ if (!next) break;
126
+ buffer = next.rest;
127
+ const payload = sseDataPayload(next.block);
128
+ if (!responsesTerminal) output += next.block + next.delimiter;
129
+ if (payload === "[DONE]") {
130
+ done = true;
131
+ if (responsesTerminal) output += next.block + next.delimiter;
132
+ continue;
133
+ }
134
+ if (!responsesTerminal && payload && terminalStatusFromSsePayload(payload)) {
135
+ responsesTerminal = true;
136
+ }
137
+ }
138
+ if (responsesTerminal) {
139
+ terminal = true;
140
+ buffer = "";
141
+ }
142
+ if (flush && !terminal && buffer.length > 0) {
143
+ output += buffer;
144
+ buffer = "";
145
+ }
146
+ return encoder.encode(output);
147
+ };
148
+
149
+ return {
150
+ feed(chunk) {
151
+ if (disposed || terminal) return new Uint8Array(0);
152
+ buffer += decoder!.decode(chunk, { stream: true });
153
+ return process(false);
154
+ },
155
+ finish() {
156
+ if (disposed || terminal) return new Uint8Array(0);
157
+ buffer += decoder!.decode();
158
+ return process(true);
159
+ },
160
+ terminalSeen: () => terminal,
161
+ doneSeen: () => done,
162
+ dispose() {
163
+ if (disposed) return;
164
+ disposed = true;
165
+ decoder = null;
166
+ buffer = "";
167
+ },
168
+ };
169
+ }
170
+
98
171
  /**
99
172
  * Relay a passthrough SSE body like relayWithAbort, but convert a MID-STREAM failure (upstream
100
173
  * reset after headers) into a clean terminal: any partial block is closed off, then a synthetic
@@ -110,18 +183,57 @@ export function relaySseWithFailedTail(
110
183
  ): ReadableStream<Uint8Array> {
111
184
  const reader = body.getReader();
112
185
  const encoder = new TextEncoder();
186
+ const terminalBoundary = createSseTerminalOutputBoundary();
187
+ let closed = false;
188
+ const relayChunk = (
189
+ controller: ReadableStreamDefaultController<Uint8Array>,
190
+ value: Uint8Array,
191
+ ): "terminal" | "output" | "buffered" => {
192
+ const outbound = terminalBoundary.feed(value);
193
+ if (outbound.byteLength > 0) controller.enqueue(outbound);
194
+ if (!terminalBoundary.terminalSeen()) return outbound.byteLength > 0 ? "output" : "buffered";
195
+
196
+ // A Responses terminal frame is the protocol boundary. Some compatible
197
+ // gateways leave the HTTP connection open after response.completed, which
198
+ // otherwise leaves Codex waiting forever even though the model turn is done.
199
+ // Preserve through the terminal block only, add the conventional sentinel
200
+ // when there was no real [DONE] data event, then stop reading upstream.
201
+ if (!terminalBoundary.doneSeen()) {
202
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
203
+ }
204
+ closed = true;
205
+ controller.close();
206
+ const reason = "Responses terminal event received";
207
+ // Notify the tee inspection branch as well. It has already received the
208
+ // same terminal-bearing upstream chunk, so its bounded drain records the
209
+ // real terminal and then releases the turn/upstream keep-alive connection.
210
+ onClientGone?.(reason);
211
+ reader.cancel(reason).catch(() => {});
212
+ terminalBoundary.dispose();
213
+ return "terminal";
214
+ };
113
215
  return new ReadableStream<Uint8Array>({
114
216
  async pull(controller) {
115
217
  try {
116
- const { done, value } = await reader.read();
117
- if (done) {
118
- controller.close();
119
- return;
218
+ for (;;) {
219
+ const { done, value } = await reader.read();
220
+ if (done) {
221
+ const tail = terminalBoundary.finish();
222
+ if (tail.byteLength > 0) controller.enqueue(tail);
223
+ terminalBoundary.dispose();
224
+ controller.close();
225
+ return;
226
+ }
227
+ const result = relayChunk(controller, value);
228
+ if (result !== "buffered") return;
120
229
  }
121
- controller.enqueue(value);
122
230
  } catch (err) {
231
+ const partial = terminalBoundary.finish();
232
+ terminalBoundary.dispose();
233
+ if (closed) return;
123
234
  const payload = buildFailedTailPayload(err);
124
235
  try {
236
+ if (partial.byteLength > 0) controller.enqueue(partial);
125
237
  // Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
126
238
  controller.enqueue(encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\ndata: [DONE]\n\n`));
127
239
  controller.close();
@@ -130,6 +242,7 @@ export function relaySseWithFailedTail(
130
242
  }
131
243
  },
132
244
  cancel(reason) {
245
+ terminalBoundary.dispose();
133
246
  if (onClientGone) onClientGone(reason);
134
247
  else upstream.abort(reason);
135
248
  reader.cancel(reason).catch(() => {});
@@ -137,11 +250,12 @@ export function relaySseWithFailedTail(
137
250
  });
138
251
  }
139
252
 
140
- export function nextSseBlock(buffer: string): { block: string; rest: string } | null {
253
+ export function nextSseBlock(buffer: string): { block: string; delimiter: string; rest: string } | null {
141
254
  const match = buffer.match(/\r?\n\r?\n/);
142
255
  if (!match || match.index === undefined) return null;
143
256
  return {
144
257
  block: buffer.slice(0, match.index),
258
+ delimiter: match[0],
145
259
  rest: buffer.slice(match.index + match[0].length),
146
260
  };
147
261
  }
@@ -8,6 +8,7 @@ import {
8
8
  import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
9
9
  import { readCodexCatalogPath } from "../codex/catalog";
10
10
  import type { OcxUsage } from "../types";
11
+ import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
11
12
  import type { AdapterRequest } from "../adapters/base";
12
13
  import { redactSecretString } from "../lib/redact";
13
14
  import {
@@ -15,6 +16,7 @@ import {
15
16
  isKnownAdmissionKind,
16
17
  isKnownInboundProtocol,
17
18
  isKnownUsageSurface,
19
+ isValidReasoningWireValue,
18
20
  readRecentUsageEntries,
19
21
  usageForFinalLog,
20
22
  usageStatusForFinalLog,
@@ -58,7 +60,7 @@ export interface RequestLogContext {
58
60
  requestedEffort?: string;
59
61
  effectiveEffort?: string;
60
62
  reasoningWireField?: string;
61
- reasoningWireValue?: string | number;
63
+ reasoningWireValue?: string | number | boolean;
62
64
  requestedServiceTier?: string;
63
65
  requestedSpeedLabel?: string;
64
66
  configuredServiceTier?: string;
@@ -95,6 +97,8 @@ export interface RequestLogContext {
95
97
  affinity?: "reused" | "new_bind" | "rebound" | "cleared";
96
98
  transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
97
99
  terminalSource?: "upstream" | "synthetic";
100
+ /** Bounded route-decision trace (RI-01); never contains secrets. */
101
+ routeDecision?: RouteDecisionTraceV1;
98
102
  }
99
103
 
100
104
  export interface RequestLogEntry {
@@ -121,7 +125,7 @@ export interface RequestLogEntry {
121
125
  requestedEffort?: string;
122
126
  effectiveEffort?: string;
123
127
  reasoningWireField?: string;
124
- reasoningWireValue?: string | number;
128
+ reasoningWireValue?: string | number | boolean;
125
129
  requestedServiceTier?: string;
126
130
  requestedSpeedLabel?: string;
127
131
  configuredServiceTier?: string;
@@ -146,6 +150,8 @@ export interface RequestLogEntry {
146
150
  transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
147
151
  /** Whether the terminal came from a real upstream SSE event or a proxy synthetic tail. */
148
152
  terminalSource?: "upstream" | "synthetic";
153
+ /** Bounded route-decision trace (RI-01); never contains secrets. */
154
+ routeDecision?: RouteDecisionTraceV1;
149
155
  }
150
156
 
151
157
  const requestLog: RequestLogEntry[] = [];
@@ -214,6 +220,7 @@ function asCloseReason(value: string | undefined): RequestLogEntry["closeReason"
214
220
  export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): RequestLogEntry {
215
221
  const terminalStatus = asTerminalStatus(entry.terminalStatus);
216
222
  const closeReason = asCloseReason(entry.closeReason);
223
+ const routeDecision = normalizeRouteDecisionTraceForLog(entry.routeDecision);
217
224
  return {
218
225
  requestId: entry.requestId,
219
226
  timestamp: entry.timestamp,
@@ -246,9 +253,21 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
246
253
  ...(entry.usage ? { usage: entry.usage } : {}),
247
254
  ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
248
255
  ...(entry.attempts?.length ? { attempts: entry.attempts } : {}),
256
+ ...(routeDecision ? { routeDecision } : {}),
249
257
  };
250
258
  }
251
259
 
260
+ /**
261
+ * Hydration guard: persisted traces are re-normalized before they enter the
262
+ * in-memory ring buffer so a hand-edited or corrupt row cannot poison the DTO.
263
+ * A row that fails validation is dropped, never forwarded unvalidated.
264
+ */
265
+ function normalizeRouteDecisionTraceForLog(
266
+ entry: RouteDecisionTraceV1 | undefined,
267
+ ): RouteDecisionTraceV1 | null {
268
+ return entry ? normalizeRouteDecisionTrace(entry) : null;
269
+ }
270
+
252
271
  /**
253
272
  * Seed the in-memory Logs ring buffer from usage.jsonl so GUI /api/logs survives
254
273
  * `ocx stop` / `ocx start` (process restart). Idempotent per process; no-ops when
@@ -329,6 +348,7 @@ export function addRequestLog(entry: RequestLogEntry) {
329
348
  ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
330
349
  ...(entry.attempts?.length ? { attempts: entry.attempts } : {}),
331
350
  ...failureDiagnostics,
351
+ ...(entry.routeDecision ? { routeDecision: entry.routeDecision } : {}),
332
352
  });
333
353
  } catch {
334
354
  /* request logging must never fail a user request */
@@ -399,12 +419,11 @@ export function recordAdapterReasoning(
399
419
  const reasoning = raw as Record<string, unknown>;
400
420
  if (typeof reasoning.effectiveEffort !== "string" || !reasoning.effectiveEffort
401
421
  || (reasoning.wireField !== "reasoning_effort"
422
+ && reasoning.wireField !== "reasoning.enabled"
423
+ && reasoning.wireField !== "reasoning.effort"
402
424
  && reasoning.wireField !== "thinking_budget"
403
425
  && reasoning.wireField !== "thinking.type")
404
- || (!(typeof reasoning.wireValue === "string" && reasoning.wireValue)
405
- && !(typeof reasoning.wireValue === "number"
406
- && Number.isFinite(reasoning.wireValue)
407
- && reasoning.wireValue >= 0))) {
426
+ || !isValidReasoningWireValue(reasoning.wireField, reasoning.wireValue)) {
408
427
  return;
409
428
  }
410
429
 
@@ -817,6 +836,7 @@ export function addFinalRequestLog(
817
836
  ...(logCtx.affinity ? { affinity: logCtx.affinity } : {}),
818
837
  ...(logCtx.transportPhase ? { transportPhase: logCtx.transportPhase } : {}),
819
838
  ...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
839
+ ...(logCtx.routeDecision ? { routeDecision: logCtx.routeDecision } : {}),
820
840
  });
821
841
  if (isUsageDebugEnabled()) {
822
842
  appendUsageDebug({
@@ -96,7 +96,7 @@ import {
96
96
  sanitizePassthroughHeaders,
97
97
  } from "../relay";
98
98
  import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
99
- import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
99
+ import type { EffectiveSubagentModel, EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
100
100
  import type { TranslatorBudget } from "../../lib/translator-budget";
101
101
 
102
102
 
@@ -169,6 +169,7 @@ export function collabSurface(parsed: OcxParsedRequest): "v1" | "v2" | null {
169
169
 
170
170
  export interface MultiAgentGuidanceOptions {
171
171
  multiAgentGuidanceEnabled?: boolean;
172
+ codexAccountNamespace?: string;
172
173
  injectionModel?: string;
173
174
  injectionEffort?: string;
174
175
  subagentModels?: string[];
@@ -207,6 +208,16 @@ export async function resolveEffectiveSubagentRoster(
207
208
  return effectiveSubagentRoster(configuredModels, surface);
208
209
  }
209
210
 
211
+ /** Reuse one parsed catalog snapshot across every roster projection for this request. */
212
+ async function createRequestScopedSubagentRosterResolver(): Promise<NonNullable<
213
+ MultiAgentGuidanceDeps["resolveEffectiveSubagentRoster"]
214
+ >> {
215
+ const { effectiveSubagentRoster, readCatalog, readCodexCatalogPath } = await import("../../codex/catalog");
216
+ const catalogEntries = readCatalog(readCodexCatalogPath())?.models ?? [];
217
+ return (configuredModels, surface) =>
218
+ effectiveSubagentRoster(configuredModels, surface, catalogEntries);
219
+ }
220
+
210
221
 
211
222
 
212
223
  export async function multiAgentGuidanceText(
@@ -218,10 +229,14 @@ export async function multiAgentGuidanceText(
218
229
  const {
219
230
  injectionModel,
220
231
  injectionEffort,
232
+ codexAccountNamespace,
221
233
  subagentModels,
222
234
  subagentModelFallback,
223
235
  injectionPrompt,
224
236
  } = options;
237
+ const activeAccountNamespace = codexAccountNamespace?.length
238
+ ? codexAccountNamespace
239
+ : undefined;
225
240
  const surface = collabSurface(parsed);
226
241
  if (surface === null) return null;
227
242
 
@@ -242,15 +257,51 @@ export async function multiAgentGuidanceText(
242
257
  ...(subagentModels ?? []),
243
258
  ...(injectionModel ? [injectionModel] : []),
244
259
  ];
245
- const resolveRoster = deps.resolveEffectiveSubagentRoster ?? resolveEffectiveSubagentRoster;
260
+ const resolveRoster = deps.resolveEffectiveSubagentRoster
261
+ ?? await createRequestScopedSubagentRosterResolver();
246
262
  const effective = await resolveRoster(configuredForGuidance, "v2");
247
- const rosterModels = effective.advertised.filter(candidate =>
248
- (subagentModels ?? []).some(model => slugsEquivalent(model, candidate.model))
249
- );
263
+ // Resolve the roster and preferred roles independently so a bare native can project onto its
264
+ // generated account rows without making an unrelated provider/gpt-* row look equivalent.
265
+ // The intersection keeps both projections inside Codex's one global five-model window.
266
+ const candidateModels = new Set(effective.candidates.map(candidate => candidate.model));
267
+ const withinCandidateWindow = (candidate: EffectiveSubagentModel): boolean =>
268
+ candidateModels.has(candidate.model);
269
+ const configuredSubagents = subagentModels ?? [];
270
+ const subagentEffective = configuredSubagents.length > 0
271
+ ? injectionModel
272
+ ? await resolveRoster(configuredSubagents, "v2")
273
+ : effective
274
+ : undefined;
275
+ const preferredEffective = injectionModel
276
+ ? configuredSubagents.length > 0
277
+ ? await resolveRoster([injectionModel], "v2")
278
+ : effective
279
+ : undefined;
280
+ const explicitlyConfigured = (candidate: EffectiveSubagentModel): boolean =>
281
+ configuredSubagents.some(model =>
282
+ model.includes("/") && slugsEquivalent(model, candidate.model)
283
+ );
284
+ const allowedForCurrentRoute = (candidate: EffectiveSubagentModel): boolean =>
285
+ explicitlyConfigured(candidate)
286
+ || !candidate.model.includes("/")
287
+ || (activeAccountNamespace !== undefined
288
+ && candidate.model.startsWith(`${activeAccountNamespace}/`));
289
+ const rosterModels = (subagentEffective?.advertised ?? [])
290
+ .filter(withinCandidateWindow)
291
+ .filter(allowedForCurrentRoute);
250
292
  const roster = subagentRosterText(rosterModels);
251
- const preferred = injectionModel
252
- ? effective.candidates.find(candidate => slugsEquivalent(injectionModel, candidate.model))
293
+ const preferredCandidates = (preferredEffective?.advertised ?? []).filter(withinCandidateWindow);
294
+ const soleBarePreferred = preferredCandidates.length === 1
295
+ && !preferredCandidates[0]!.model.includes("/")
296
+ ? preferredCandidates[0]
253
297
  : undefined;
298
+ const preferred = injectionModel?.includes("/")
299
+ ? preferredCandidates[0]
300
+ : activeAccountNamespace
301
+ ? preferredCandidates.find(candidate =>
302
+ candidate.model.startsWith(`${activeAccountNamespace}/`)
303
+ ) ?? soleBarePreferred
304
+ : soleBarePreferred;
254
305
 
255
306
  if (isInjectionDebugEnabled() && effective.excluded.length > 0) {
256
307
  injectionDebugLog(`[opencodex] multi-agent guidance excluded: ${effective.excluded
@@ -260,7 +311,11 @@ export async function multiAgentGuidanceText(
260
311
  const fallbackGuidance = subagentFallbackGuidanceText({ subagentModelFallback } as OcxConfig);
261
312
  if (!injectionModel && roster === "" && fallbackGuidance === "") return null;
262
313
  if (injectionPrompt) {
263
- return `<multi_agent_mode>${applyInjectionPlaceholders(injectionPrompt, injectionModel, injectionEffort, roster, fallbackGuidance)}</multi_agent_mode>`;
314
+ // Bare ids must resolve to a unique/current-route candidate. Preserve the legacy raw
315
+ // fallback only for explicit routed/account-qualified ids.
316
+ const promptModel = preferred?.model
317
+ ?? (injectionModel?.includes("/") ? injectionModel : undefined);
318
+ return `<multi_agent_mode>${applyInjectionPlaceholders(injectionPrompt, promptModel, injectionEffort, roster, fallbackGuidance)}</multi_agent_mode>`;
264
319
  }
265
320
  if (!preferred && roster === "" && fallbackGuidance === "") return null;
266
321
  let text = "When the active spawn_agent tool supports optional \"model\" or \"reasoning_effort\" overrides, "