@bitkyc08/opencodex 2.65.0 → 2.66.0-preview.20260925

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 (213) hide show
  1. package/README.md +14 -0
  2. package/gui/dist/assets/App-BVjA0T6O.js +51 -0
  3. package/gui/dist/assets/App-gkoJIEOV.css +1 -0
  4. package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-BCFtiWwD.js} +1 -1
  5. package/gui/dist/assets/index-ComVsdSk.css +1 -0
  6. package/gui/dist/assets/index-vO4dgfiU.js +86 -0
  7. package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-C4FiNaxk.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -2
  10. package/src/adapters/anthropic/beta-allowlist.ts +80 -0
  11. package/src/adapters/anthropic/passthrough.ts +221 -0
  12. package/src/adapters/anthropic-image-codec.ts +56 -19
  13. package/src/adapters/anthropic-image-normalize.ts +20 -10
  14. package/src/adapters/anthropic.ts +55 -24
  15. package/src/adapters/coding-agent/protocol.ts +29 -19
  16. package/src/adapters/cursor/live-transport.ts +18 -3
  17. package/src/adapters/cursor/native-exec-shell.ts +18 -63
  18. package/src/adapters/cursor/native-exec.ts +5 -2
  19. package/src/adapters/cursor/native-foreground-shell.ts +44 -0
  20. package/src/adapters/devin/cloud-direct/chat.ts +7 -1
  21. package/src/adapters/exec-tool-result-normalize.ts +4 -1
  22. package/src/adapters/google.ts +12 -0
  23. package/src/adapters/ollama-native.ts +32 -1
  24. package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
  25. package/src/adapters/openai-chat.ts +6 -4
  26. package/src/adapters/openai-responses/passthrough.ts +1 -0
  27. package/src/adapters/openai-responses/reasoning.ts +45 -1
  28. package/src/bridge/response-json.ts +10 -2
  29. package/src/chat/outbound.ts +104 -52
  30. package/src/claude/claude-code-block.ts +16 -0
  31. package/src/claude/context-windows.ts +52 -4
  32. package/src/claude/desktop-first-party.ts +25 -8
  33. package/src/claude/inbound.ts +22 -0
  34. package/src/claude/intercept/connect-proxy.ts +37 -4
  35. package/src/claude/intercept/proxy-auth.ts +166 -0
  36. package/src/claude/intercept/runtime.ts +21 -0
  37. package/src/claude/intercept/settings.ts +33 -12
  38. package/src/claude/outbound.ts +45 -27
  39. package/src/cli/account-extended.ts +47 -15
  40. package/src/cli/account-target.ts +115 -0
  41. package/src/cli/account.ts +20 -28
  42. package/src/cli/agent.ts +10 -1
  43. package/src/cli/api-protocols.ts +206 -0
  44. package/src/cli/capabilities.ts +86 -0
  45. package/src/cli/combo.ts +2 -2
  46. package/src/cli/connect.ts +127 -3
  47. package/src/cli/dispatch.ts +8 -0
  48. package/src/cli/doctor.ts +33 -0
  49. package/src/cli/help.ts +2 -0
  50. package/src/cli/link.ts +288 -0
  51. package/src/cli/registry.ts +23 -0
  52. package/src/cli/runtime-api.ts +79 -0
  53. package/src/cli/system-command.ts +21 -2
  54. package/src/client/connect.ts +125 -23
  55. package/src/client/hub-relay.ts +12 -8
  56. package/src/client/link-join.ts +323 -0
  57. package/src/client/link-relay.ts +241 -0
  58. package/src/client/link-state.ts +110 -0
  59. package/src/client/link-teardown.ts +63 -0
  60. package/src/client/link-tunnel.ts +465 -0
  61. package/src/client/machine-listener.ts +14 -5
  62. package/src/client/runtime.ts +91 -42
  63. package/src/client/state.ts +4 -0
  64. package/src/codex/catalog/build-entries.ts +4 -0
  65. package/src/codex/catalog/derive-entry.ts +5 -0
  66. package/src/codex/catalog/parsing.ts +4 -0
  67. package/src/codex/catalog/retained-sync.ts +4 -1
  68. package/src/codex/desktop-switches.ts +88 -8
  69. package/src/codex/inject/plan.ts +16 -3
  70. package/src/codex/inject.ts +3 -0
  71. package/src/codex/internal/catalog-writer.ts +13 -2
  72. package/src/combos/failover.ts +3 -2
  73. package/src/combos/index.ts +12 -0
  74. package/src/combos/jev.ts +646 -0
  75. package/src/combos/request.ts +5 -0
  76. package/src/combos/resolve.ts +17 -15
  77. package/src/combos/types.ts +47 -3
  78. package/src/config/atomic-write.ts +3 -0
  79. package/src/config/live-reconcile.ts +20 -4
  80. package/src/config/persist-unlocked.ts +20 -7
  81. package/src/config/schema/config-schema.ts +16 -0
  82. package/src/config/schema/leaf-validators.ts +38 -1
  83. package/src/generated/compatibility-version.json +416 -128
  84. package/src/integrations/omp-yaml-source.ts +109 -4
  85. package/src/lab/automation/orchestrator.ts +60 -4
  86. package/src/lab/events/limits.ts +1 -1
  87. package/src/lib/bounded-body.ts +55 -38
  88. package/src/lib/config-ownership.ts +11 -22
  89. package/src/lib/local-desktop-snapshot-capability.ts +56 -0
  90. package/src/lib/local-management-capability.ts +25 -3
  91. package/src/lib/optional-shutdown-hooks.ts +17 -0
  92. package/src/lib/state-store-registrations.ts +2 -2
  93. package/src/lib/windows-elevation.ts +62 -25
  94. package/src/lib/windows-secret-acl.ts +114 -23
  95. package/src/link/admission-wait.ts +73 -0
  96. package/src/link/compensation.ts +100 -0
  97. package/src/link/fingerprint.ts +21 -0
  98. package/src/link/paths.ts +19 -0
  99. package/src/link/ports.ts +12 -0
  100. package/src/link/routes.ts +32 -0
  101. package/src/link/ssh-argv.ts +170 -0
  102. package/src/link/ssh-config.ts +152 -0
  103. package/src/link/ssh-runner.ts +161 -0
  104. package/src/link/status-projection.ts +96 -0
  105. package/src/link/store.ts +139 -0
  106. package/src/link/supervisor.ts +404 -0
  107. package/src/link/tunnel-state.ts +91 -0
  108. package/src/protocols/baseline.ts +88 -0
  109. package/src/protocols/codecs/chat.ts +17 -0
  110. package/src/protocols/codecs/messages.ts +18 -0
  111. package/src/protocols/codecs/responses.ts +17 -0
  112. package/src/protocols/contract.ts +185 -0
  113. package/src/protocols/dto.ts +267 -0
  114. package/src/protocols/encoders/adapter-events.ts +876 -0
  115. package/src/protocols/encoders/chat.ts +243 -0
  116. package/src/protocols/encoders/messages.ts +462 -0
  117. package/src/protocols/envelope.ts +43 -0
  118. package/src/protocols/features.ts +295 -0
  119. package/src/protocols/guard.ts +33 -0
  120. package/src/protocols/opaque-state.ts +132 -0
  121. package/src/protocols/path.ts +39 -0
  122. package/src/protocols/plan-snapshot.ts +226 -0
  123. package/src/protocols/plan.ts +192 -0
  124. package/src/protocols/provider-summary.ts +101 -0
  125. package/src/protocols/settings.ts +118 -0
  126. package/src/protocols/shadow-plan.ts +36 -0
  127. package/src/protocols/shadow.ts +62 -0
  128. package/src/protocols/trace.ts +335 -0
  129. package/src/providers/model-rename-migration.ts +3 -3
  130. package/src/providers/registry/entries-core.ts +6 -1
  131. package/src/providers/registry/entries-extended.ts +15 -0
  132. package/src/providers/registry/model-ids.ts +1 -0
  133. package/src/providers/registry/types.ts +6 -0
  134. package/src/providers/registry.ts +1 -1
  135. package/src/remote-control/workspace-hub.ts +5 -4
  136. package/src/responses/apply-patch-envelope.ts +6 -1
  137. package/src/responses/code-mode-shell-input.ts +4 -2
  138. package/src/responses/freeform-wrapper-scan.ts +51 -24
  139. package/src/responses/progressive-freeform-input.ts +14 -20
  140. package/src/responses/spill-store.ts +187 -33
  141. package/src/responses/state/snapshot-select.ts +49 -0
  142. package/src/responses/state/spill-inspect.ts +46 -0
  143. package/src/responses/state/spill-queue.ts +16 -0
  144. package/src/responses/state.ts +51 -27
  145. package/src/server/audio-client.ts +8 -3
  146. package/src/server/audio-upstream.ts +9 -4
  147. package/src/server/auth-cors.ts +25 -8
  148. package/src/server/chat-completions.ts +71 -11
  149. package/src/server/chat-native-eligibility.ts +74 -0
  150. package/src/server/chat-native.ts +139 -74
  151. package/src/server/claude-messages.ts +140 -22
  152. package/src/server/hub-usage.ts +4 -3
  153. package/src/server/index/link-listener.ts +195 -0
  154. package/src/server/index/optional-listeners.ts +115 -0
  155. package/src/server/index/serve-options.ts +44 -10
  156. package/src/server/index.ts +10 -9
  157. package/src/server/inference/attempt.ts +51 -0
  158. package/src/server/inference/client-encoder-delivery.ts +235 -0
  159. package/src/server/inference/client-wire-log.ts +49 -0
  160. package/src/server/inference/client-wire.ts +69 -0
  161. package/src/server/inference/context.ts +15 -0
  162. package/src/server/inference/final-log.ts +37 -0
  163. package/src/server/local-desktop-snapshot-auth.ts +57 -0
  164. package/src/server/management/agent-settings-routes.ts +15 -13
  165. package/src/server/management/api-access.ts +11 -1
  166. package/src/server/management/config-routes.ts +3 -6
  167. package/src/server/management/context.ts +25 -0
  168. package/src/server/management/link-routes.ts +535 -0
  169. package/src/server/management/logs-usage-routes.ts +31 -1
  170. package/src/server/management/native-integration-routes.ts +6 -11
  171. package/src/server/management/oauth-account-routes.ts +42 -16
  172. package/src/server/management/protocol-routes.ts +159 -0
  173. package/src/server/management/protocol-settings-patch.ts +145 -0
  174. package/src/server/management/provider-patch-transaction.ts +61 -0
  175. package/src/server/management/provider-routes.ts +39 -18
  176. package/src/server/management/route-registry.ts +12 -0
  177. package/src/server/management/sidebar-routes.ts +8 -3
  178. package/src/server/management/system-routes.ts +18 -1
  179. package/src/server/management/usage-aggregate-cache.ts +206 -6
  180. package/src/server/management-api.ts +26 -1
  181. package/src/server/management-auth.ts +19 -5
  182. package/src/server/messages-native-eligibility.ts +161 -0
  183. package/src/server/messages-native-oauth.ts +149 -0
  184. package/src/server/messages-native.ts +790 -0
  185. package/src/server/relay.ts +9 -0
  186. package/src/server/request-log-filter.ts +92 -0
  187. package/src/server/request-log.ts +31 -70
  188. package/src/server/responses/adapter-delivery.ts +43 -16
  189. package/src/server/responses/agent-task-recovery.ts +90 -22
  190. package/src/server/responses/codex-ws-exchange.ts +4 -0
  191. package/src/server/responses/codex-ws-wire.ts +10 -1
  192. package/src/server/responses/core-combo-native.ts +323 -0
  193. package/src/server/responses/core-combo.ts +224 -13
  194. package/src/server/responses/core-options.ts +22 -0
  195. package/src/server/responses/core.ts +14 -26
  196. package/src/server/responses/request-sidecar-auth.ts +8 -3
  197. package/src/server/responses/run-turn-execution.ts +257 -63
  198. package/src/server/responses/sidecar-execution.ts +7 -4
  199. package/src/server/system-env.ts +7 -0
  200. package/src/service/diagnostics.ts +7 -1
  201. package/src/service/launchd.ts +26 -37
  202. package/src/service/windows-ops.ts +24 -20
  203. package/src/types/config.ts +58 -1
  204. package/src/types.ts +2 -0
  205. package/src/update/transactional-install.d.mts +0 -1
  206. package/src/update/transactional-install.mjs +17 -17
  207. package/src/usage/jev-stats.ts +495 -0
  208. package/src/usage/log.ts +24 -3
  209. package/src/web-search/run-turn-loop.ts +568 -0
  210. package/gui/dist/assets/App-8NMiZxT0.css +0 -1
  211. package/gui/dist/assets/App-DGLte6IR.js +0 -50
  212. package/gui/dist/assets/index--EWgGQvZ.css +0 -1
  213. package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
@@ -0,0 +1,568 @@
1
+ // LOCAL PATCH (runturn-websearch): web-search sidecar loop for runTurn adapters.
2
+ //
3
+ // Upstream's runWithWebSearch (loop.ts) only drives the buildRequest/fetch/
4
+ // parseStream transport, so adapters that own their turn loop (devin, cursor,
5
+ // qoder, codebuddy) silently dropped the hosted `web_search` declaration. This
6
+ // module replays the same interception contract over the runTurn event stream:
7
+ // the synthetic `web_search` function tool is injected into each iteration's
8
+ // tool list, calls to it are intercepted and executed through the configured
9
+ // sidecar backend, and the assistant call + toolResult are appended to the
10
+ // OcxMessage history before the next runTurn dispatch. Everything else passes
11
+ // through untouched.
12
+ //
13
+ // The file is deliberately self-contained (small helpers duplicated from
14
+ // loop.ts instead of exported) so the patch stays a pure addition plus a few
15
+ // small call-site edits — easier to re-apply after `ocx update`.
16
+
17
+ import type {
18
+ AdapterEvent,
19
+ OcxAssistantContentPart,
20
+ OcxMessage,
21
+ OcxParsedRequest,
22
+ OcxProviderConfig,
23
+ } from "../types";
24
+ import type { SidecarPlan } from "./index";
25
+ import { WEB_SEARCH_TOOL_NAME, buildWebSearchTool } from "./synthetic-tool";
26
+ import { hasVisibleAssistantText, scanEventsForWebSearch } from "./loop";
27
+ import { isTruncatedStopReason } from "../responses/truncated-stop-reason";
28
+ import {
29
+ runWebSearch,
30
+ type SidecarOutcome,
31
+ type SidecarOutcomeRecorder,
32
+ } from "./executor";
33
+ import { runAnthropicWebSearch } from "./anthropic-executor";
34
+ import { runXaiWebSearch } from "./xai-executor";
35
+ import { runGeminiWebSearch } from "./gemini-executor";
36
+ import { runExaWebSearch } from "./exa-executor";
37
+ import { formatWebSearchResults } from "./format-result";
38
+ import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
39
+ import { redactSecretString } from "../lib/redact";
40
+ import { guardEmptyCompletionEventStream } from "../server/responses/empty-completion-guard";
41
+ import {
42
+ isTranslatorBudgetExceededError,
43
+ type TranslatorBudget,
44
+ } from "../lib/translator-budget";
45
+ import { jsonUtf8Bytes } from "../lib/json-byte-size";
46
+
47
+ export interface RunTurnWebSearchDeps {
48
+ parsed: OcxParsedRequest;
49
+ plan: SidecarPlan;
50
+ /** Required for the openai backend (ChatGPT forward path). */
51
+ forwardProvider?: OcxProviderConfig;
52
+ forwardHeaders?: Headers;
53
+ /** Operator key for the exa backend; read from config by the caller. */
54
+ exaApiKey?: string;
55
+ abortSignal?: AbortSignal;
56
+ recordSidecarOutcome?: SidecarOutcomeRecorder;
57
+ /** Preserve the request's opt-in empty-completion retry before the search cap. */
58
+ emptyCompletionRetry?: boolean;
59
+ /**
60
+ * Optional request translator budget. When present the loop charges what it
61
+ * retains — each iteration's buffered event batch and the replay history it
62
+ * appends to messages — with its own local byte counters, and releases
63
+ * exactly those totals when the retention ends. It deliberately does NOT use
64
+ * the shared event-ownership WeakMap helpers: other owners (queue, bridge,
65
+ * output collector) may hold independent leases on the same event objects.
66
+ */
67
+ translatorBudget?: TranslatorBudget;
68
+ /**
69
+ * Dispatch one more routed-model iteration. Iterations 2+ get a fresh event
70
+ * stream from the runTurn transport with the grown message history.
71
+ */
72
+ dispatch: (iterParsed: OcxParsedRequest) => AsyncIterable<AdapterEvent>;
73
+ }
74
+
75
+ /**
76
+ * Parsed request for the FIRST iteration when the runTurn web-search loop is
77
+ * active: same request plus the synthetic tool so the model can ask to search.
78
+ */
79
+ export function runTurnWebSearchInitialParsed(parsed: OcxParsedRequest): OcxParsedRequest {
80
+ return {
81
+ ...parsed,
82
+ context: {
83
+ ...parsed.context,
84
+ tools: [...(parsed.context.tools ?? []), buildWebSearchTool()],
85
+ },
86
+ };
87
+ }
88
+
89
+ // --- helpers duplicated from loop.ts (kept private there; see header note) ---
90
+
91
+ function normalizeQuery(q: string): string {
92
+ return q.trim().toLowerCase().replace(/\s+/g, " ");
93
+ }
94
+
95
+ /**
96
+ * Rebuild the assistant content parts that preceded a web_search call in this
97
+ * iteration: visible text AND thinking, in event order, so the replayed
98
+ * assistant message preserves everything the model emitted before the call.
99
+ * Thinking extraction mirrors loop.ts's rules (signed blocks keep their own
100
+ * signature, raw reasoning stays a separate unsigned part); text_delta parts
101
+ * are folded in as OcxTextContent — upstream's thinking-only version silently
102
+ * dropped visible text that preceded a tool call.
103
+ */
104
+ function extractIterationContent(events: AdapterEvent[]): OcxAssistantContentPart[] {
105
+ const parts: OcxAssistantContentPart[] = [];
106
+ let text = "";
107
+ let thinking = "";
108
+ let signature: string | undefined;
109
+ let rawReasoning = "";
110
+ const flushText = () => {
111
+ if (!text) return;
112
+ parts.push({ type: "text", text });
113
+ text = "";
114
+ };
115
+ const flushVisible = () => {
116
+ if (!thinking && !signature) return;
117
+ parts.push({ type: "thinking", thinking, ...(signature ? { signature } : {}) });
118
+ thinking = "";
119
+ signature = undefined;
120
+ };
121
+ const flushRaw = () => {
122
+ if (!rawReasoning) return;
123
+ parts.push({ type: "thinking", thinking: rawReasoning });
124
+ rawReasoning = "";
125
+ };
126
+ for (const e of events) {
127
+ if (e.type === "thinking_delta") {
128
+ flushText();
129
+ flushRaw();
130
+ thinking += e.thinking;
131
+ } else if (e.type === "reasoning_raw_delta") {
132
+ flushText();
133
+ flushVisible();
134
+ rawReasoning += e.text;
135
+ } else if (e.type === "thinking_signature") {
136
+ signature = e.signature;
137
+ flushVisible();
138
+ } else if (e.type === "redacted_thinking") {
139
+ flushText();
140
+ flushVisible();
141
+ flushRaw();
142
+ parts.push({ type: "thinking", thinking: "", redacted: [e.data] });
143
+ } else if (e.type === "text_delta") {
144
+ flushVisible();
145
+ flushRaw();
146
+ text += e.text;
147
+ }
148
+ }
149
+ flushText();
150
+ flushVisible();
151
+ flushRaw();
152
+ return parts;
153
+ }
154
+
155
+ function forcedAnswerNudge(): OcxMessage {
156
+ return {
157
+ role: "developer",
158
+ content:
159
+ "Answer the user's question now using the web search results already gathered above. " +
160
+ "Ground your answer in what those results actually say, and reference the relevant sources " +
161
+ "when they are available. Do not claim you lack information that the results contain, and do " +
162
+ "not invent sources that were not returned.",
163
+ timestamp: Date.now(),
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Transient developer-role nudge for the ONE recovery pass after a forced
169
+ * answer came back empty. The recovery also removes every tool (toolChoice:
170
+ * "none"), so the model has nothing to call and can only return text.
171
+ */
172
+ function forcedAnswerRetryNudge(): OcxMessage {
173
+ return {
174
+ role: "developer",
175
+ content:
176
+ "Your previous response contained no usable answer. Web search has finished for this turn and " +
177
+ "no tools are available for this response. Answer the user's question now in assistant text, " +
178
+ "using the web search results already gathered above. If those results are insufficient, say " +
179
+ "what is missing instead of returning an empty response.",
180
+ timestamp: Date.now(),
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Drive a runTurn adapter through the web-search loop. `first` is iteration
186
+ * 0's already-dispatched event stream (built from
187
+ * runTurnWebSearchInitialParsed); later iterations go through deps.dispatch.
188
+ * Intermediate iterations emit only heartbeats and the search cell begin/end
189
+ * pair — their text/thinking is folded back into the replayed history instead
190
+ * of reaching the client mid-turn, matching the fetch-path loop's ordering.
191
+ */
192
+ export async function* runTurnWebSearchLoop(
193
+ first: AsyncIterable<AdapterEvent>,
194
+ deps: RunTurnWebSearchDeps,
195
+ ): AsyncGenerator<AdapterEvent> {
196
+ const { parsed, plan, abortSignal } = deps;
197
+ const messages: OcxMessage[] = [...parsed.context.messages];
198
+ const allTools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
199
+ const toolsNoWebSearch = (parsed.context.tools ?? []).filter(t => !t.webSearch);
200
+ const failedQueries = new Set<string>();
201
+ let searchesExecuted = 0;
202
+ let executedSearchCount = 0;
203
+ const HARD_CAP = plan.maxSearches + 3;
204
+ let source = first;
205
+ const loopT0 = Date.now();
206
+
207
+ // Translator-budget ownership (deps.translatorBudget): the loop charges only
208
+ // what it retains, tracks both totals locally, and releases exactly those
209
+ // amounts — never a shared stage total another owner may also be releasing
210
+ // from. Iteration event buffers release when the iteration's events are
211
+ // consumed; the replay history appended to messages stays charged until the
212
+ // loop exits because every later dispatch rebuilds its request from it.
213
+ // Event objects are NOT leased through the shared ownership WeakMap: the
214
+ // queue, the bridge, and the buffered-path output collector may each hold
215
+ // their own independent accounting on the same objects.
216
+ let bufferedBytes = 0;
217
+ let historyBytes = 0;
218
+ const chargeBuffered = (event: AdapterEvent): void => {
219
+ if (!deps.translatorBudget) return;
220
+ // Same per-event convention as retainTranslatedEvent: the first event owns
221
+ // the array brackets, later events own their separator.
222
+ const bytes = jsonUtf8Bytes(event) + (bufferedBytes === 0 ? 2 : 1);
223
+ deps.translatorBudget.chargeRetained(bytes, { kind: "retained_collectors" });
224
+ bufferedBytes += bytes;
225
+ };
226
+ const releaseBuffered = (): void => {
227
+ if (!deps.translatorBudget || bufferedBytes === 0) return;
228
+ deps.translatorBudget.releaseRetained(bufferedBytes, { kind: "retained_collectors" });
229
+ bufferedBytes = 0;
230
+ };
231
+ const chargeHistory = (message: OcxMessage): void => {
232
+ if (!deps.translatorBudget) return;
233
+ const bytes = jsonUtf8Bytes(message) + 1;
234
+ deps.translatorBudget.chargeRetained(bytes, { kind: "request_copies" });
235
+ historyBytes += bytes;
236
+ };
237
+ const releaseHistory = (): void => {
238
+ if (!deps.translatorBudget || historyBytes === 0) return;
239
+ deps.translatorBudget.releaseRetained(historyBytes, { kind: "request_copies" });
240
+ historyBytes = 0;
241
+ };
242
+ // Shared budget-failure convention (loop.ts): a 502 upstream_error carrying
243
+ // the translation_buffer_limit code.
244
+ const budgetErrorEvent = (): AdapterEvent => ({
245
+ type: "error",
246
+ status: 502,
247
+ errorType: "upstream_error",
248
+ code: "translation_buffer_limit",
249
+ message: "upstream translation buffer exceeded the safe limit",
250
+ });
251
+ const appendMessage = (message: OcxMessage): void => {
252
+ chargeHistory(message);
253
+ messages.push(message);
254
+ };
255
+ const liveTypes = new Set<AdapterEvent["type"]>([
256
+ "text_delta", "thinking_delta", "reasoning_raw_delta", "thinking_signature",
257
+ "redacted_thinking", "kiro_redacted_reasoning",
258
+ ]);
259
+
260
+ const executeQuery = async (query: string, signal: AbortSignal | undefined): Promise<SidecarOutcome> => {
261
+ try {
262
+ switch (plan.backend) {
263
+ case "anthropic":
264
+ return plan.anthropicSidecar
265
+ ? await runAnthropicWebSearch(query, plan.anthropicSidecar.providerName, plan.anthropicSidecar.provider, plan.settings, signal)
266
+ : { text: "", sources: [], error: "anthropic backend selected without a resolved sidecar provider" };
267
+ case "xai":
268
+ return plan.xaiSidecar
269
+ ? await runXaiWebSearch(query, plan.xaiSidecar.providerName, plan.xaiSidecar.provider, plan.settings, plan.xaiSearchOptions ?? {}, signal)
270
+ : { text: "", sources: [], error: "xai backend selected without a resolved Grok OAuth provider" };
271
+ case "gemini":
272
+ return plan.geminiSidecar
273
+ ? await runGeminiWebSearch(query, plan.geminiSidecar.providerName, plan.geminiSidecar.provider, plan.settings, signal)
274
+ : { text: "", sources: [], error: "gemini backend selected without a resolved Antigravity provider" };
275
+ case "exa":
276
+ return deps.exaApiKey
277
+ ? await runExaWebSearch(query, deps.exaApiKey, plan.settings, signal)
278
+ : { text: "", sources: [], error: "exa backend selected without an exaApiKey" };
279
+ default:
280
+ return deps.forwardProvider
281
+ ? await runWebSearch(query, plan.hostedTool, deps.forwardProvider, deps.forwardHeaders ?? new Headers(), plan.settings, signal, deps.recordSidecarOutcome)
282
+ : { text: "", sources: [], error: "openai backend selected without a resolved forward sidecar" };
283
+ }
284
+ } catch (e) {
285
+ return { text: "", sources: [], error: `sidecar failed: ${redactSecretString(e instanceof Error ? e.message : String(e))}` };
286
+ }
287
+ };
288
+
289
+ async function* runSearchCall(
290
+ call: { id: string; queries: string[]; providerMetadata?: Parameters<typeof cloneProviderOpaqueToolCallMetadata>[0] },
291
+ precedingContent: OcxAssistantContentPart[],
292
+ ): AsyncGenerator<AdapterEvent> {
293
+ const results: { query: string; outcome: SidecarOutcome }[] = [];
294
+ let beganCell = false;
295
+ if (call.queries.length === 0) {
296
+ searchesExecuted++;
297
+ results.push({ query: "", outcome: { text: "", sources: [], error: "the model called web_search with an empty query" } });
298
+ }
299
+ for (const query of call.queries) {
300
+ if (abortSignal?.aborted) {
301
+ if (beganCell) yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: "failed" };
302
+ return;
303
+ }
304
+ yield { type: "heartbeat" };
305
+ if (abortSignal?.aborted) {
306
+ if (beganCell) yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: "failed" };
307
+ return;
308
+ }
309
+ let outcome: SidecarOutcome;
310
+ if (failedQueries.has(normalizeQuery(query))) {
311
+ outcome = { text: "", sources: [], error: "this query already failed earlier in the turn — do not call web_search again for it; answer from existing context" };
312
+ } else if (searchesExecuted >= plan.maxSearches) {
313
+ outcome = { text: "", sources: [], error: "web search limit reached for this turn — answer from results already gathered" };
314
+ } else {
315
+ if (!beganCell) {
316
+ beganCell = true;
317
+ yield { type: "web_search_call_begin", id: call.id };
318
+ }
319
+ if (abortSignal?.aborted) {
320
+ yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: "failed" };
321
+ return;
322
+ }
323
+ outcome = await executeQuery(query, abortSignal);
324
+ if (abortSignal?.aborted) {
325
+ // The client already saw this cell's begin — close it so the UI never
326
+ // shows a "Searching the web" spinner that never resolves.
327
+ yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: "failed" };
328
+ return;
329
+ }
330
+ searchesExecuted++;
331
+ executedSearchCount++;
332
+ if (outcome.error) failedQueries.add(normalizeQuery(query));
333
+ }
334
+ results.push({ query, outcome });
335
+ }
336
+ const now = Date.now();
337
+ const callArgs: Record<string, unknown> = call.queries.length > 1
338
+ ? { queries: call.queries }
339
+ : { query: call.queries[0] ?? "" };
340
+ appendMessage({
341
+ role: "assistant",
342
+ content: [
343
+ ...precedingContent,
344
+ {
345
+ type: "toolCall" as const,
346
+ id: call.id,
347
+ name: WEB_SEARCH_TOOL_NAME,
348
+ arguments: callArgs,
349
+ ...(cloneProviderOpaqueToolCallMetadata(call.providerMetadata)
350
+ ? { providerMetadata: cloneProviderOpaqueToolCallMetadata(call.providerMetadata) }
351
+ : {}),
352
+ },
353
+ ],
354
+ timestamp: now,
355
+ });
356
+ const allFailed = results.every(r => !!r.outcome.error);
357
+ appendMessage({
358
+ role: "toolResult", toolCallId: call.id, toolName: WEB_SEARCH_TOOL_NAME,
359
+ content: formatWebSearchResults(results, !!parsed._structuredOutput),
360
+ isError: allFailed, timestamp: now,
361
+ });
362
+ if (beganCell) {
363
+ const anySuccess = results.some(r => !r.outcome.error);
364
+ const sources: { url: string; title?: string }[] = [];
365
+ const seenSrc = new Set<string>();
366
+ for (const r of results) {
367
+ for (const s of r.outcome.sources) {
368
+ if (seenSrc.has(s.url)) continue;
369
+ seenSrc.add(s.url);
370
+ sources.push(s.title ? { url: s.url, title: s.title } : { url: s.url });
371
+ }
372
+ }
373
+ yield {
374
+ type: "web_search_call_end", id: call.id,
375
+ queries: call.queries,
376
+ status: anySuccess ? "completed" : "failed",
377
+ ...(sources.length > 0 ? { sources } : {}),
378
+ };
379
+ }
380
+ }
381
+
382
+ const abortEvent = (): AdapterEvent => ({
383
+ type: "error",
384
+ message: "client closed request during web-search",
385
+ });
386
+ const logDone = (iterations: number): void => {
387
+ if (executedSearchCount === 0) return;
388
+ const failedCount = failedQueries.size;
389
+ console.warn(
390
+ `[web-search-runturn] done — ${executedSearchCount} search${executedSearchCount > 1 ? "es" : ""}`
391
+ + (failedCount > 0 ? ` (${failedCount} failed)` : "")
392
+ + `, ${iterations + 1} iteration${iterations > 0 ? "s" : ""}, ${Date.now() - loopT0}ms`,
393
+ );
394
+ };
395
+ let emptyAnswerRetries = 0;
396
+ let ordinaryEmptyRetries = 0;
397
+ let currentParsed = runTurnWebSearchInitialParsed(parsed);
398
+
399
+ try {
400
+ for (let i = 0; i < HARD_CAP; i++) {
401
+ releaseBuffered();
402
+ if (abortSignal?.aborted) {
403
+ yield abortEvent();
404
+ return;
405
+ }
406
+ const events: AdapterEvent[] = [];
407
+ let liveWindowOpen = plan.streamRoutedModelOutput;
408
+ let streamedCount = 0;
409
+ // One identical-turn retry per request: once it is spent the guard is
410
+ // not re-applied, so a later empty terminal reaches the loop itself —
411
+ // at the search cap the forced-answer recovery below handles it, the
412
+ // same contract loop.ts keeps by running no inner guard at all. A
413
+ // maxRetries:0 wrap would instead convert that empty done into
414
+ // empty_completion_retry_failed and end the turn before recovery.
415
+ if (deps.emptyCompletionRetry && searchesExecuted < plan.maxSearches
416
+ && ordinaryEmptyRetries === 0) {
417
+ source = guardEmptyCompletionEventStream({
418
+ firstEvents: source,
419
+ continuation: () => {
420
+ ordinaryEmptyRetries++;
421
+ return deps.dispatch(currentParsed);
422
+ },
423
+ });
424
+ }
425
+ try {
426
+ for await (const e of source) {
427
+ if (abortSignal?.aborted) {
428
+ yield abortEvent();
429
+ return;
430
+ }
431
+ if (e.type === "error") {
432
+ yield e;
433
+ return;
434
+ }
435
+ if (e.type === "heartbeat") {
436
+ yield e;
437
+ continue;
438
+ }
439
+ chargeBuffered(e);
440
+ events.push(e);
441
+ if (liveWindowOpen && liveTypes.has(e.type)) {
442
+ streamedCount++;
443
+ yield e;
444
+ continue;
445
+ }
446
+ liveWindowOpen = false;
447
+ // Buffered text/reasoning/tool deltas are upstream activity too. Keep
448
+ // the bridge alive without exposing the synthetic call to the client.
449
+ if (e.type !== "done" && e.type !== "incomplete") yield { type: "heartbeat" };
450
+ }
451
+ } catch (e) {
452
+ if (isTranslatorBudgetExceededError(e)) throw e;
453
+ yield { type: "error", message: e instanceof Error ? e.message : String(e) };
454
+ return;
455
+ }
456
+
457
+ const terminals = events.filter(e => e.type === "done" || e.type === "incomplete");
458
+ if (terminals.length !== 1 || terminals[0] !== events.at(-1)) {
459
+ yield { type: "error", status: 502, errorType: "upstream_error",
460
+ message: "web-search runTurn stream must end with exactly one terminal event" };
461
+ return;
462
+ }
463
+ const split = scanEventsForWebSearch(events);
464
+ const terminal = terminals[0];
465
+ const replayEvents = split.passthrough.slice(streamedCount);
466
+ if (terminal.type === "incomplete") {
467
+ for (const e of replayEvents) yield e;
468
+ return;
469
+ }
470
+ const forceAnswer = searchesExecuted >= plan.maxSearches;
471
+ // Loop only when the model's actionable output is purely web_search calls:
472
+ // a real tool call belongs to Codex, and a budget-exhausted turn must
473
+ // answer from what it already gathered.
474
+ const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall && !forceAnswer;
475
+ if (!shouldLoop) {
476
+ // A forced-answer pass that ends `done` must have produced usable output —
477
+ // never a malformed tool call, and never silence. A truncated/refusal
478
+ // stop is authoritative and replays as-is; an empty one gets exactly one
479
+ // recovery pass with all tools removed (same contract as loop.ts #1001).
480
+ if (forceAnswer) {
481
+ const terminalEvent = split.passthrough.find(event => event.type === "done");
482
+ if (terminalEvent?.type === "done" && !split.hasMalformedToolCall
483
+ && isTruncatedStopReason(terminalEvent.stopReason)) {
484
+ logDone(i);
485
+ for (const e of replayEvents) yield e;
486
+ return;
487
+ }
488
+ if (terminalEvent?.type === "done"
489
+ && (split.hasMalformedToolCall
490
+ || (!split.hasRealToolCall && !hasVisibleAssistantText(split.passthrough)))) {
491
+ console.warn("[web-search-runturn] unusable forced answer", JSON.stringify({
492
+ model: parsed.modelId,
493
+ recoveryAttempt: emptyAnswerRetries,
494
+ searchCalls: split.calls.length,
495
+ malformed: split.hasMalformedToolCall,
496
+ stopReason: terminalEvent.stopReason,
497
+ eventTypes: [...new Set(split.passthrough.map(event => event.type))],
498
+ }));
499
+ if (!split.hasMalformedToolCall && !split.hasRealToolCall && emptyAnswerRetries === 0) {
500
+ emptyAnswerRetries++;
501
+ console.warn("[web-search-runturn] empty forced answer — retrying once without tools");
502
+ yield { type: "heartbeat" };
503
+ if (abortSignal?.aborted) { yield abortEvent(); return; }
504
+ source = deps.dispatch({
505
+ ...parsed,
506
+ options: { ...parsed.options, toolChoice: "none" as const },
507
+ context: {
508
+ ...parsed.context,
509
+ messages: [
510
+ ...messages,
511
+ ...(executedSearchCount > 0 ? [forcedAnswerNudge()] : []),
512
+ forcedAnswerRetryNudge(),
513
+ ],
514
+ tools: [],
515
+ },
516
+ });
517
+ continue;
518
+ }
519
+ yield {
520
+ type: "error",
521
+ status: 502,
522
+ errorType: "upstream_error",
523
+ message: "forced-answer pass produced no usable assistant output",
524
+ };
525
+ return;
526
+ }
527
+ }
528
+ logDone(i);
529
+ for (const e of replayEvents) yield e;
530
+ return;
531
+ }
532
+
533
+ const iterationContent = extractIterationContent(split.passthrough);
534
+ for (const [callIndex, call] of split.calls.entries()) {
535
+ if (abortSignal?.aborted) { yield abortEvent(); return; }
536
+ yield* runSearchCall(call, callIndex === 0 ? iterationContent : []);
537
+ }
538
+ if (abortSignal?.aborted) {
539
+ yield abortEvent();
540
+ return;
541
+ }
542
+
543
+ const nextForceAnswer = searchesExecuted >= plan.maxSearches;
544
+ const iterParsed: OcxParsedRequest = {
545
+ ...parsed,
546
+ context: {
547
+ ...parsed.context,
548
+ messages: nextForceAnswer && executedSearchCount > 0
549
+ ? [...messages, forcedAnswerNudge()]
550
+ : messages,
551
+ tools: nextForceAnswer ? toolsNoWebSearch : allTools,
552
+ },
553
+ };
554
+ currentParsed = iterParsed;
555
+ source = deps.dispatch(iterParsed);
556
+ }
557
+
558
+ // Safety net: the hard cap should be unreachable (forceAnswer ends the loop
559
+ // first), but never hang a client stream if an adapter misbehaves.
560
+ yield { type: "error", message: "web-search runTurn loop exceeded its iteration cap" };
561
+ } catch (error) {
562
+ if (!isTranslatorBudgetExceededError(error)) throw error;
563
+ yield budgetErrorEvent();
564
+ } finally {
565
+ releaseBuffered();
566
+ releaseHistory();
567
+ }
568
+ }
@@ -1 +0,0 @@
1
- .codex-account-controls{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:12px;min-width:0;display:flex}.codex-account-auto-switch{flex:none;align-items:center;gap:7px;min-width:0;display:flex}.codex-account-auto-switch-label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);white-space:nowrap}.codex-account-auto-switch-input-wrap{align-items:stretch;display:inline-flex;position:relative}.codex-account-auto-switch-input{width:76px;height:30px;font-size:var(--text-label);text-align:right;padding:4px 38px 4px 8px}.codex-account-auto-switch-unit{color:var(--muted);font-size:var(--text-label);pointer-events:none;position:absolute;top:50%;right:25px;transform:translateY(-50%)}.codex-account-auto-switch-input-wrap .ocx-stepper{width:22px}.codex-account-auto-switch-toggle{transform-origin:50%;margin-inline:-3px;transform:scale(.8)}.quota-summary-bar{z-index:var(--z-sticky);border-bottom:1px solid var(--border);background:var(--bg);min-height:34px;font-size:var(--text-label);align-items:center;gap:12px;padding:4px 16px;display:flex;position:sticky;top:0}.quota-summary-list{flex-wrap:wrap;flex:1;align-items:center;gap:4px;min-width:0;margin:0;padding:0;list-style:none;display:flex}.quota-summary-item{position:relative}.quota-summary-chip{color:var(--text);font:inherit;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:999px;align-items:center;gap:6px;padding:3px 8px;display:inline-flex}.quota-summary-chip:hover,.quota-summary-chip[aria-expanded=true]{border-color:var(--border)}.quota-summary-chip:focus-visible{outline:2px solid var(--text);outline-offset:1px}.quota-summary-name{color:var(--muted)}.quota-summary-pct{font-variant-numeric:tabular-nums;font-weight:600}.quota-summary-flag{width:14px;height:14px;color:var(--bg);font-size:var(--text-micro);border-radius:50%;place-items:center;font-weight:700;display:inline-grid}.quota-summary-item--warn .quota-summary-chip{background:var(--amber-soft)}.quota-summary-item--warn .quota-summary-pct{color:var(--amber)}.quota-summary-item--warn .quota-summary-flag{background:var(--amber)}.quota-summary-item--critical .quota-summary-chip{border-color:var(--red);background:var(--red-soft)}.quota-summary-item--critical .quota-summary-pct{color:var(--red)}.quota-summary-item--critical .quota-summary-flag{background:var(--red)}.quota-summary-popover{z-index:var(--z-popover);border:1px solid var(--border);background:var(--surface);border-radius:8px;min-width:260px;padding:10px 12px;position:absolute;top:calc(100% + 4px);left:0;box-shadow:0 8px 24px #00000024}.quota-summary-popover-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:6px;display:flex}.quota-summary-badge{font-size:var(--text-caption);border-radius:999px;padding:1px 6px}.quota-summary-badge--warn{color:var(--amber);background:var(--amber-soft)}.quota-summary-badge--critical{color:var(--red);background:var(--red-soft)}.quota-summary-table{border-collapse:collapse;width:100%}.quota-summary-table th,.quota-summary-table td{text-align:left;white-space:nowrap;padding:3px 0;font-weight:400}.quota-summary-table td.quota-summary-row-pct{font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap;padding:3px 12px;font-weight:600}.quota-summary-row-reset{color:var(--muted);white-space:nowrap}.quota-summary-table th[scope=row]{white-space:normal;overflow-wrap:anywhere}.quota-summary-row--warn .quota-summary-row-pct{color:var(--amber)}.quota-summary-row--critical .quota-summary-row-pct{color:var(--red)}.quota-summary-popover-foot{color:var(--muted);font-size:var(--text-caption);margin-top:6px}.quota-summary-updated{color:var(--muted);font-size:var(--text-caption);white-space:nowrap;flex-shrink:0}.quota-summary-updated--stale{color:var(--amber)}.main:has(>.quota-summary-bar):has(>.main-inner--combos .combos-workspace-shell){flex-direction:column;height:100dvh;display:flex}.main:has(>.quota-summary-bar)>.main-inner.main-inner--combos:has(.combos-workspace-shell){flex:auto;height:auto;min-height:0}@media (width<=760px){.quota-summary-bar{padding:4px 10px;position:static}.quota-summary-popover{min-width:0;margin-top:4px;position:fixed;top:auto;left:8px;right:8px}.main:has(>.quota-summary-bar):has(>.main-inner--combos .combos-workspace-shell){height:100%;min-height:0}}