@bitkyc08/opencodex 2.87.0 → 2.89.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.
- package/README.md +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-BnRSujOq.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
- package/gui/dist/assets/index-uchD-ngf.js +94 -0
- package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +86 -48
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +52 -30
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/passthrough.ts +17 -12
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +17 -3
- package/src/bridge/response-json.ts +4 -4
- package/src/bridge/sse.ts +17 -10
- package/src/claude/inbound-content-options.ts +66 -0
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +18 -5
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/cross-home-owner.ts +16 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/loopback-family.ts +31 -0
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +30 -3
- package/src/codex/auth-api/account-list.ts +3 -0
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-context.ts +9 -4
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-gather.ts +3 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/combos/failover.ts +2 -1
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/opencode-zen-upgrade.ts +9 -8
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +411 -175
- package/src/images/loop.ts +101 -27
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/request-failure-model.ts +2 -0
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/generic-account-failover.ts +7 -4
- package/src/oauth/index.ts +5 -35
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/protocols/encoders/adapter-events.ts +5 -5
- package/src/protocols/encoders/messages.ts +2 -2
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +5 -0
- package/src/providers/key-failover.ts +11 -11
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +12 -73
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +39 -6
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +3 -0
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +3 -2
- package/src/providers/registry/entries-extended.ts +8 -52
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-replay-cache.ts +50 -4
- package/src/responses/web-search-replay.ts +138 -0
- package/src/routing/analytics.ts +7 -15
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +28 -13
- package/src/server/claude-messages.ts +4 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +11 -5
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +7 -7
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +10 -1
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +34 -7
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +7 -2
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +34 -18
- package/src/server/responses/adapter-delivery.ts +53 -6
- package/src/server/responses/adapter-dispatch.ts +104 -29
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +6 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +8 -5
- package/src/server/responses/compaction-recovery.ts +2 -0
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-codex-account.ts +9 -3
- package/src/server/responses/core-combo.ts +34 -7
- package/src/server/responses/core-lifetime.ts +3 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +5 -1
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +30 -33
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +1 -0
- package/src/server/responses/request-transport.ts +32 -3
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +59 -10
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/server/ws-bridge.ts +2 -0
- package/src/service/definition.ts +9 -1
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/opencode-zen.ts +9 -3
- package/src/types/provider.ts +9 -5
- package/src/types/request.ts +30 -4
- package/src/types/wire.ts +4 -2
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +16 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +212 -73
- package/src/web-search/progress-stream.ts +14 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-ZfYbLHkB.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/index-CfYDMj34.js +0 -94
package/src/web-search/loop.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { bindAnthropicRejectionResponse } from "../adapters/anthropic-rejection-memo";
|
|
2
|
+
import { anthropicDeclaredTools } from "../adapters/anthropic-tool-catalog";
|
|
3
|
+
import { enableAnthropicReplayCapture, takeAnthropicReplay, bindAnthropicReplay, type AnthropicReplaySnapshot } from "../adapters/anthropic-replay";
|
|
4
|
+
import { anthropicSearchAssistantContent } from "./anthropic-history";
|
|
5
|
+
import type { createAnthropicThinkingRecovery } from "../server/responses/anthropic-thinking-recovery";
|
|
1
6
|
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/base";
|
|
2
7
|
import type { AdapterEvent, OcxConfig, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxProviderOpaqueToolCallMetadata, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
|
|
3
8
|
import { namespacedToolName, toolChoiceToolPredicate } from "../types";
|
|
@@ -9,6 +14,7 @@ import { bridgeToResponsesSSE } from "../bridge";
|
|
|
9
14
|
import { isTerminalRefusalIncomplete } from "../bridge/terminal-refusal";
|
|
10
15
|
import { guardEmptyToolCatalogStream, type ClientToolCatalogRestriction } from "../lib/empty-tool-catalog";
|
|
11
16
|
import { mergeUsage } from "../server/responses/empty-completion-guard";
|
|
17
|
+
import { markSidecarContextOverflow, sidecarLoopTerminalEvent, upstreamErrorCode, type SidecarLoopFailure } from "../server/responses/context-overflow";
|
|
12
18
|
import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
|
|
13
19
|
import { runAnthropicWebSearch } from "./anthropic-executor";
|
|
14
20
|
import { runXaiWebSearch, type XaiSearchOptions } from "./xai-executor";
|
|
@@ -18,16 +24,18 @@ import type { WebSearchBackendId } from "./index";
|
|
|
18
24
|
import { pacingHeaderDeadline } from "./header-deadline";
|
|
19
25
|
import { redactSecretString } from "../lib/redact";
|
|
20
26
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
21
|
-
import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait } from "../lib/upstream-retry";
|
|
27
|
+
import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait, isNonReplayableResponse, SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
22
28
|
import { rateLimitRetryDelayMs } from "../providers/key-failover";
|
|
29
|
+
import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
|
|
23
30
|
import {
|
|
24
31
|
isTranslatorBudgetExceededError,
|
|
25
32
|
TRANSLATOR_MAX_TURN_BYTES,
|
|
26
33
|
TranslatorBudgetExceededError,
|
|
27
34
|
} from "../lib/translator-budget";
|
|
28
35
|
import { formatWebSearchResults } from "./format-result";
|
|
29
|
-
import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError } from "./progress-stream";
|
|
36
|
+
import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError, WebSearchAdapterEventError } from "./progress-stream";
|
|
30
37
|
import { WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
|
|
38
|
+
import { newSidecarSearchCellId, rememberSidecarSearch, restoreSidecarSearchResults } from "./sidecar-search-replay";
|
|
31
39
|
|
|
32
40
|
const SSE_HEADERS = {
|
|
33
41
|
"Content-Type": "text/event-stream",
|
|
@@ -204,6 +212,9 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
|
|
|
204
212
|
};
|
|
205
213
|
|
|
206
214
|
for (const e of events) {
|
|
215
|
+
// Match the bridge: consecutive signature snapshots update one block. The next
|
|
216
|
+
// semantic event (including an empty thinking-block start) closes that block.
|
|
217
|
+
if (signature !== undefined && e.type !== "thinking_signature" && e.type !== "heartbeat") flushVisible();
|
|
207
218
|
if (e.type === "text_delta" && e.anthropicThinkingSignature) {
|
|
208
219
|
flushVisible();
|
|
209
220
|
flushRaw();
|
|
@@ -216,7 +227,6 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
|
|
|
216
227
|
rawReasoning += e.text;
|
|
217
228
|
} else if (e.type === "thinking_signature") {
|
|
218
229
|
signature = e.signature;
|
|
219
|
-
flushVisible();
|
|
220
230
|
} else if (e.type === "redacted_thinking") {
|
|
221
231
|
flushVisible();
|
|
222
232
|
flushRaw();
|
|
@@ -234,8 +244,8 @@ function normalizeQuery(q: string): string {
|
|
|
234
244
|
}
|
|
235
245
|
|
|
236
246
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
247
|
+
* Developer nudge for the forced-answer pass. Anthropic keeps its first sent position in the
|
|
248
|
+
* request-local history; other providers append it only for that iteration. It tells the model to ground its final answer in the web results already
|
|
239
249
|
* gathered this turn. Citation wording is conditional — a failed/empty search still wants an answer,
|
|
240
250
|
* just without fabricated sources.
|
|
241
251
|
*/
|
|
@@ -252,10 +262,8 @@ function forcedAnswerNudge(): OcxMessage {
|
|
|
252
262
|
}
|
|
253
263
|
|
|
254
264
|
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* this turn says so explicitly rather than relying on the removal alone. Like {@link forcedAnswerNudge}
|
|
258
|
-
* it is iteration-local and never touches the persisted `messages`.
|
|
265
|
+
* Developer nudge for the ONE recovery pass after an empty forced answer. Tool calls are disabled;
|
|
266
|
+
* Anthropic retains its declared catalog and this nudge in request-local history for prefix fidelity.
|
|
259
267
|
*/
|
|
260
268
|
function forcedAnswerRetryNudge(): OcxMessage {
|
|
261
269
|
return {
|
|
@@ -269,22 +277,31 @@ function forcedAnswerRetryNudge(): OcxMessage {
|
|
|
269
277
|
};
|
|
270
278
|
}
|
|
271
279
|
|
|
272
|
-
function jsonError(status: number, message: string): Response {
|
|
273
|
-
return new Response(JSON.stringify({ error: { message, type: "upstream_error", code: null } }), {
|
|
280
|
+
function jsonError(status: number, message: string, event?: Extract<AdapterEvent, { type: "error" }>): Response {
|
|
281
|
+
return new Response(JSON.stringify({ error: { message, type: event?.errorType ?? "upstream_error", code: event?.code ?? null, ...(event?.retryable === false ? { retryable: false } : {}) } }), {
|
|
274
282
|
status,
|
|
275
|
-
headers: { "Content-Type": "application/json" },
|
|
283
|
+
headers: { "Content-Type": "application/json", ...(event?.retryable === false ? { "x-should-retry": "false" } : {}) },
|
|
276
284
|
});
|
|
277
285
|
}
|
|
278
286
|
|
|
279
287
|
/** Hard provider/parse failure inside an iteration. The eager first iteration converts it to a
|
|
280
288
|
* non-200 jsonError; later (already-streaming) iterations surface it as an in-stream error event. */
|
|
281
289
|
class LoopError extends Error {
|
|
282
|
-
constructor(
|
|
290
|
+
constructor(
|
|
291
|
+
readonly status: number,
|
|
292
|
+
message: string,
|
|
293
|
+
readonly event?: Extract<AdapterEvent, { type: "error" }>,
|
|
294
|
+
readonly detail?: Pick<SidecarLoopFailure, "upstreamCode" | "nonReplayable">,
|
|
295
|
+
) {
|
|
283
296
|
super(message);
|
|
284
297
|
this.name = "LoopError";
|
|
285
298
|
}
|
|
286
299
|
}
|
|
287
300
|
|
|
301
|
+
function loopFailure(e: LoopError): SidecarLoopFailure {
|
|
302
|
+
return { status: e.status, message: e.message, ...(e.event ? { event: e.event } : {}), ...e.detail };
|
|
303
|
+
}
|
|
304
|
+
|
|
288
305
|
/**
|
|
289
306
|
* Dependencies for one web-search loop iteration: parsed request, active adapter,
|
|
290
307
|
* incoming metadata, and the configured search executor.
|
|
@@ -351,7 +368,8 @@ export interface WebSearchLoopDeps {
|
|
|
351
368
|
/** Observe the exact adapter request selected for each routed-model iteration. */
|
|
352
369
|
onRequestBuilt?: (request: AdapterRequest) => void;
|
|
353
370
|
/** Request-scoped executor retains the core's selection binding across loop retries. */
|
|
354
|
-
|
|
371
|
+
thinkingRecovery?: ReturnType<typeof createAnthropicThinkingRecovery>;
|
|
372
|
+
fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest, adapter: ProviderAdapter, recovery?: AttemptRecoveryKind) => typeof globalThis.fetch;
|
|
355
373
|
/** Called before each routed-model dispatch in the loop, for attempt telemetry. Same-target 429 replays pass the `rate-limit-429` recovery kind. */
|
|
356
374
|
onAttemptSend?: (recovery?: AttemptRecoveryKind) => void;
|
|
357
375
|
/**
|
|
@@ -411,9 +429,14 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
411
429
|
? Math.floor(deps.stallTimeoutSec * 1000)
|
|
412
430
|
: 300_000;
|
|
413
431
|
|
|
432
|
+
// Put recorded results back into replayed search pairs before the first iteration (#7269).
|
|
433
|
+
restoreSidecarSearchResults(parsed);
|
|
414
434
|
const messages: OcxMessage[] = [...parsed.context.messages];
|
|
435
|
+
const replaySnapshots = new Set<AnthropicReplaySnapshot>();
|
|
415
436
|
const loopT0 = Date.now();
|
|
416
|
-
const allTools = parsed.context.tools ?? [];
|
|
437
|
+
const allTools = adapter.name === "anthropic" ? anthropicDeclaredTools(parsed) : parsed.context.tools ?? [];
|
|
438
|
+
let forcedNudgeAdded = false;
|
|
439
|
+
let emptyNudgeAdded = false;
|
|
417
440
|
let searchesExecuted = 0;
|
|
418
441
|
let executedSearchCount = 0;
|
|
419
442
|
// Queries whose search already failed this turn — repeats are short-circuited so a model that keeps
|
|
@@ -448,8 +471,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
448
471
|
interface IterationResponse {
|
|
449
472
|
response: Response;
|
|
450
473
|
responseAdapter: ProviderAdapter;
|
|
474
|
+
recover?: (error: WebSearchAdapterEventError) => Promise<IterationResponse | undefined>;
|
|
451
475
|
}
|
|
452
476
|
type IterationSplit = ReturnType<typeof scanEventsForWebSearch> & {
|
|
477
|
+
events: AdapterEvent[];
|
|
478
|
+
anthropicReplay?: AnthropicReplaySnapshot;
|
|
453
479
|
/**
|
|
454
480
|
* How many leading passthrough events were already delivered live this iteration. They are
|
|
455
481
|
* exactly the first N passthrough entries (live delivery stops before the first event that
|
|
@@ -464,6 +490,42 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
464
490
|
const rateLimitRetryPolicy = deps.retryOn429Policy ?? null;
|
|
465
491
|
let rateLimitRetries = 0;
|
|
466
492
|
|
|
493
|
+
const requireSuccessfulHeaders = async (prepared: IterationResponse): Promise<void> => {
|
|
494
|
+
if (!prepared.response.ok) {
|
|
495
|
+
if (deps.thinkingRecovery && prepared.responseAdapter.name === "anthropic") {
|
|
496
|
+
const event = await deps.thinkingRecovery.projectHttpFailure(prepared.response, signal);
|
|
497
|
+
throw new LoopError(event.status ?? prepared.response.status, event.message, event);
|
|
498
|
+
}
|
|
499
|
+
let body: Awaited<ReturnType<typeof readBoundedResponseBody>>;
|
|
500
|
+
try {
|
|
501
|
+
body = await readBoundedResponseBody(prepared.response, { signal });
|
|
502
|
+
} catch {
|
|
503
|
+
// The response status is authoritative even when its untrusted error body fails while
|
|
504
|
+
// being read (including a synchronous getReader() failure). Never route that failure
|
|
505
|
+
// through the adapter formatter or the generic transport error, which could expose its
|
|
506
|
+
// raw message. A parent/client cancellation still owns the request lifecycle as 499.
|
|
507
|
+
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
508
|
+
throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}`, undefined,
|
|
509
|
+
{ nonReplayable: isNonReplayableResponse(prepared.response) });
|
|
510
|
+
}
|
|
511
|
+
let formatted = "";
|
|
512
|
+
let upstreamCode: string | undefined;
|
|
513
|
+
if (body.displaySafe && !body.truncated && body.text.trim() && prepared.responseAdapter.formatErrorBody) {
|
|
514
|
+
try {
|
|
515
|
+
formatted = prepared.responseAdapter.formatErrorBody(
|
|
516
|
+
prepared.response.status,
|
|
517
|
+
prepared.response.headers,
|
|
518
|
+
body.text,
|
|
519
|
+
).trim();
|
|
520
|
+
} catch { /* formatter hooks are best-effort; unsafe raw text is never the fallback */ }
|
|
521
|
+
}
|
|
522
|
+
if (body.displaySafe && !body.truncated) upstreamCode = upstreamErrorCode(body.text);
|
|
523
|
+
const suffix = formatted ? `: ${formatted.slice(0, 400)}` : "";
|
|
524
|
+
throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}${suffix}`, undefined,
|
|
525
|
+
{ upstreamCode, nonReplayable: isNonReplayableResponse(prepared.response) });
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
467
529
|
// Acquire one iteration's final response headers. The first call is drained eagerly so an initial
|
|
468
530
|
// connect/header/HTTP failure stays a non-2xx JSON response. Its successful BODY is deliberately
|
|
469
531
|
// left unread until the downstream Responses SSE bridge exists.
|
|
@@ -473,24 +535,30 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
473
535
|
* restart) before the `on429` key rotation.
|
|
474
536
|
*/
|
|
475
537
|
const prepareIterationEvents = async function* (forceAnswer: boolean): AsyncGenerator<AdapterEvent, IterationResponse> {
|
|
476
|
-
// On the forced-answer pass the model is asked to answer from the results already in
|
|
477
|
-
// `messages`. A weak model can still produce a thin answer that
|
|
478
|
-
// ignores what the search found, which reads to the user as "the search did nothing". Nudge it
|
|
479
|
-
// (iteration-locally — never mutate the shared `messages`) to actually use the gathered results.
|
|
480
|
-
// Only when a REAL search ran (executedSearchCount, not empty-query/limit/repeat placeholders).
|
|
481
|
-
let iterMessages: OcxMessage[] = forceAnswer && executedSearchCount > 0
|
|
482
|
-
? [...messages, forcedAnswerNudge()]
|
|
483
|
-
: messages;
|
|
484
|
-
// #1001 follow-up: the recovery pass for an empty forced answer. Removing every tool leaves the
|
|
485
|
-
// model nothing to call, and the extra developer turn asks it for the text it just failed to
|
|
486
|
-
// produce. `toolChoice: "none"` is what drops those definitions in the adapter, so the retry
|
|
487
|
-
// cannot repeat the same empty or tool-shaped response.
|
|
488
538
|
const recoveringEmptyAnswer = forceAnswer && emptyAnswerRetries > 0;
|
|
489
|
-
|
|
539
|
+
const preserveAnthropicPrefix = adapter.name === "anthropic";
|
|
540
|
+
let iterMessages = messages;
|
|
541
|
+
if (preserveAnthropicPrefix) {
|
|
542
|
+
// Once sent, each nudge is part of subsequent signatures' bound prefix. Keep its
|
|
543
|
+
// original position even when a forced-answer response makes another tool call.
|
|
544
|
+
if (forceAnswer && executedSearchCount > 0 && !forcedNudgeAdded) {
|
|
545
|
+
const nudge = forcedAnswerNudge();
|
|
546
|
+
if (nudge.role === "developer" && typeof nudge.content === "string") nudge.content = [{ type: "text", text: nudge.content }];
|
|
547
|
+
messages.push(nudge); forcedNudgeAdded = true;
|
|
548
|
+
}
|
|
549
|
+
if (recoveringEmptyAnswer && !emptyNudgeAdded) {
|
|
550
|
+
const nudge = forcedAnswerRetryNudge();
|
|
551
|
+
if (nudge.role === "developer" && typeof nudge.content === "string") nudge.content = [{ type: "text", text: nudge.content }];
|
|
552
|
+
messages.push(nudge); emptyNudgeAdded = true;
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
if (forceAnswer && executedSearchCount > 0) iterMessages = [...messages, forcedAnswerNudge()];
|
|
556
|
+
if (recoveringEmptyAnswer) iterMessages = [...iterMessages, forcedAnswerRetryNudge()];
|
|
557
|
+
}
|
|
490
558
|
const iterParsed: OcxParsedRequest = {
|
|
491
559
|
...parsed, stream: true,
|
|
492
560
|
...(recoveringEmptyAnswer ? { options: { ...parsed.options, toolChoice: "none" as const } } : {}),
|
|
493
|
-
context: { ...parsed.context, messages: iterMessages, tools: recoveringEmptyAnswer ? [] : allTools },
|
|
561
|
+
context: { ...parsed.context, messages: iterMessages, tools: recoveringEmptyAnswer && !preserveAnthropicPrefix ? [] : allTools },
|
|
494
562
|
};
|
|
495
563
|
// One cumulative header deadline spans key rotations and reset sends, excluding local pacing.
|
|
496
564
|
// clear() stops only its timer after final headers; the direct turn signal remains attached to
|
|
@@ -528,7 +596,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
528
596
|
cachedRequest = request;
|
|
529
597
|
cachedAdapter = requestAdapter;
|
|
530
598
|
}
|
|
531
|
-
const requestFetch = headerDeadline.pacedFetch(deps.fetchForRequest?.(request, iterParsed, requestAdapter) ?? routedProviderFetch);
|
|
599
|
+
const requestFetch = headerDeadline.pacedFetch(deps.fetchForRequest?.(request, iterParsed, requestAdapter, recovery) ?? routedProviderFetch);
|
|
532
600
|
let response: Response;
|
|
533
601
|
try {
|
|
534
602
|
if (requestAdapter.fetchResponse) {
|
|
@@ -571,9 +639,33 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
571
639
|
} finally {
|
|
572
640
|
request.releaseBodyObservation?.();
|
|
573
641
|
}
|
|
642
|
+
bindAnthropicRejectionResponse(request, response);
|
|
574
643
|
return { response, responseAdapter: requestAdapter };
|
|
575
644
|
};
|
|
576
645
|
|
|
646
|
+
const recover = async (error: WebSearchAdapterEventError): Promise<IterationResponse | undefined> => {
|
|
647
|
+
if (signal.aborted || !error.replaySafe || isNonReplayableResponse(prepared.response)
|
|
648
|
+
|| !deps.thinkingRecovery?.prepareEvent(error.event.message, prepared.responseAdapter.name, iterParsed, prepared.response)) return;
|
|
649
|
+
cachedRequest = undefined;
|
|
650
|
+
cachedAdapter = undefined;
|
|
651
|
+
// The successful headers ended the previous timer; the replacement owns a fresh deadline.
|
|
652
|
+
headerDeadline = pacingHeaderDeadline(connectTimeoutMs, signal);
|
|
653
|
+
try {
|
|
654
|
+
const replacement = await fetchOnce(prepared.responseAdapter, "opaque-blob-rejection");
|
|
655
|
+
headerDeadline.clear();
|
|
656
|
+
await requireSuccessfulHeaders(replacement);
|
|
657
|
+
return replacement;
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
660
|
+
if (headerDeadline.didExpire()) throw new LoopError(504,
|
|
661
|
+
`Provider response-header timeout after ${connectTimeoutMs}ms during web-search`);
|
|
662
|
+
if (error instanceof LoopError || error instanceof SendBudgetExhaustedError || isTranslatorBudgetExceededError(error)) throw error;
|
|
663
|
+
throw new LoopError(502, `Provider unreachable: ${error instanceof Error ? error.message : String(error)}`);
|
|
664
|
+
} finally {
|
|
665
|
+
headerDeadline.clear();
|
|
666
|
+
deps.onIterationEnd?.();
|
|
667
|
+
}
|
|
668
|
+
};
|
|
577
669
|
let prepared = await fetchOnce(adapter);
|
|
578
670
|
// Same-target 429 wait-and-retry (opt-in `retryOn429`) BEFORE key rotation: a primary-key
|
|
579
671
|
// rate-limit blip replays on the SAME key; rotation only runs after attempts exhaust.
|
|
@@ -644,41 +736,25 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
644
736
|
prepared = replacement;
|
|
645
737
|
}
|
|
646
738
|
|
|
739
|
+
if (await deps.thinkingRecovery?.prepareHttp(prepared.response, prepared.responseAdapter.name, iterParsed)) {
|
|
740
|
+
try { void prepared.response.body?.cancel().catch(() => {}); } catch { /* closed */ }
|
|
741
|
+
cachedRequest = undefined;
|
|
742
|
+
cachedAdapter = undefined;
|
|
743
|
+
prepared = await fetchOnce(adapter, "opaque-blob-rejection");
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
|
|
647
747
|
// Final headers have arrived. Clear only the deadline timer before ANY body read.
|
|
648
748
|
headerDeadline.clear();
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
try {
|
|
652
|
-
body = await readBoundedResponseBody(prepared.response, { signal });
|
|
653
|
-
} catch {
|
|
654
|
-
// The response status is authoritative even when its untrusted error body fails while
|
|
655
|
-
// being read (including a synchronous getReader() failure). Never route that failure
|
|
656
|
-
// through the adapter formatter or the generic transport error, which could expose its
|
|
657
|
-
// raw message. A parent/client cancellation still owns the request lifecycle as 499.
|
|
658
|
-
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
659
|
-
throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}`);
|
|
660
|
-
}
|
|
661
|
-
let formatted = "";
|
|
662
|
-
if (body.displaySafe && !body.truncated && body.text.trim() && prepared.responseAdapter.formatErrorBody) {
|
|
663
|
-
try {
|
|
664
|
-
formatted = prepared.responseAdapter.formatErrorBody(
|
|
665
|
-
prepared.response.status,
|
|
666
|
-
prepared.response.headers,
|
|
667
|
-
body.text,
|
|
668
|
-
).trim();
|
|
669
|
-
} catch { /* formatter hooks are best-effort; unsafe raw text is never the fallback */ }
|
|
670
|
-
}
|
|
671
|
-
const suffix = formatted ? `: ${formatted.slice(0, 400)}` : "";
|
|
672
|
-
throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}${suffix}`);
|
|
673
|
-
}
|
|
674
|
-
return prepared;
|
|
749
|
+
await requireSuccessfulHeaders(prepared);
|
|
750
|
+
return { ...prepared, recover };
|
|
675
751
|
} catch (error) {
|
|
676
|
-
if (isTranslatorBudgetExceededError(error)) throw error;
|
|
752
|
+
if (isTranslatorBudgetExceededError(error) || error instanceof SendBudgetExhaustedError) throw error;
|
|
677
753
|
if (headerDeadline.didExpire()) {
|
|
678
754
|
throw new LoopError(504, `Provider response-header timeout after ${connectTimeoutMs}ms during web-search`);
|
|
679
755
|
}
|
|
680
756
|
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
681
|
-
if (error instanceof LoopError) throw error;
|
|
757
|
+
if (error instanceof LoopError || error instanceof RequestPacingQueueOverloadError) throw error;
|
|
682
758
|
throw new LoopError(502, `Provider unreachable: ${error instanceof Error ? error.message : String(error)}`);
|
|
683
759
|
} finally {
|
|
684
760
|
headerDeadline.clear();
|
|
@@ -710,15 +786,25 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
710
786
|
const events: AdapterEvent[] = [];
|
|
711
787
|
let liveWindowOpen = deps.streamRoutedModelOutput === true;
|
|
712
788
|
let streamedPassthroughCount = 0;
|
|
789
|
+
let anthropicReplay: AnthropicReplaySnapshot | undefined;
|
|
790
|
+
let releaseCapture = () => {};
|
|
713
791
|
try {
|
|
714
|
-
const parse =
|
|
792
|
+
const parse: ProviderAdapter["parseStream"] = (response, budget) => {
|
|
793
|
+
if (prepared.responseAdapter.name === "anthropic") releaseCapture = enableAnthropicReplayCapture(response);
|
|
794
|
+
return prepared.responseAdapter.parseStream(response, budget);
|
|
795
|
+
};
|
|
715
796
|
for await (const event of parseStreamWithProgress(prepared.response, parse, {
|
|
716
797
|
signal,
|
|
717
798
|
inactivityTimeoutMs: routedModelStallTimeoutMs,
|
|
718
799
|
translatorBudget,
|
|
800
|
+
preserveAdapterError: deps.thinkingRecovery !== undefined,
|
|
719
801
|
// A pass that fails was still billed; the error path reports it with the hidden passes.
|
|
720
802
|
onErrorUsage: usage => { hiddenUsage = mergeUsage(hiddenUsage, usage); },
|
|
721
803
|
})) {
|
|
804
|
+
if (event.type === "done") {
|
|
805
|
+
anthropicReplay = takeAnthropicReplay(event);
|
|
806
|
+
if (anthropicReplay) replaySnapshots.add(anthropicReplay);
|
|
807
|
+
}
|
|
722
808
|
if (event.type === "heartbeat") yield event;
|
|
723
809
|
// Kiro's explicit-completion protocol marks ordinary assistant text as commentary while
|
|
724
810
|
// it performs a bounded final-answer retry. That text is safe to surface immediately and
|
|
@@ -740,12 +826,21 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
740
826
|
}
|
|
741
827
|
}
|
|
742
828
|
} catch (error) {
|
|
743
|
-
if (isTranslatorBudgetExceededError(error)) throw error;
|
|
829
|
+
if (isTranslatorBudgetExceededError(error) || error instanceof SendBudgetExhaustedError) throw error;
|
|
744
830
|
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
831
|
+
if (error instanceof WebSearchAdapterEventError) {
|
|
832
|
+
const replacement = await prepared.recover?.(error);
|
|
833
|
+
if (replacement) return yield* consumeIterationEvents(replacement);
|
|
834
|
+
const event = isNonReplayableResponse(prepared.response)
|
|
835
|
+
? { ...error.event, message: "Provider stream error: upstream diagnostic withheld for a non-replayable failure", retryable: false as const }
|
|
836
|
+
: error.event;
|
|
837
|
+
throw new LoopError(event.status ?? 502, event.message, event);
|
|
838
|
+
}
|
|
839
|
+
if (error instanceof LoopError) throw error;
|
|
745
840
|
if (error instanceof RoutedModelInactivityError) throw new LoopError(504, error.message);
|
|
746
841
|
if (error instanceof WebSearchStreamProtocolError) throw new LoopError(502, error.message);
|
|
747
842
|
throw new LoopError(502, `Provider stream error: ${error instanceof Error ? error.message : String(error)}`);
|
|
748
|
-
}
|
|
843
|
+
} finally { releaseCapture(); }
|
|
749
844
|
|
|
750
845
|
const terminalIndexes = events.flatMap((event, index) =>
|
|
751
846
|
event.type === "done" || event.type === "incomplete" || event.type === "error" ? [index] : []);
|
|
@@ -757,9 +852,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
757
852
|
if (terminal.code === "translation_buffer_limit") {
|
|
758
853
|
throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
|
|
759
854
|
}
|
|
760
|
-
|
|
855
|
+
// Keep an adapter's own overflow verdict; every other terminal stays a generic 502.
|
|
856
|
+
throw new LoopError(502, terminal.message, terminal.code === "context_length_exceeded" ? terminal : undefined);
|
|
761
857
|
}
|
|
762
|
-
|
|
858
|
+
deps.thinkingRecovery?.observeIteration(events);
|
|
859
|
+
return { ...scanEventsForWebSearch(events), streamedPassthroughCount, events, anthropicReplay };
|
|
763
860
|
};
|
|
764
861
|
|
|
765
862
|
// Execute one model-requested web_search call. The call may batch several queries (native
|
|
@@ -768,9 +865,10 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
768
865
|
// valid, and surface as ONE search cell carrying every attempted query. A real search (one that
|
|
769
866
|
// hits the sidecar) shows the spinner WHILE the batch runs. Empty/limit/repeat placeholders never
|
|
770
867
|
// emit a cell (matching the prior single-query behavior).
|
|
771
|
-
async function* runSearchCall(call: WebSearchCall, precedingThinking: OcxThinkingContent[] = []): AsyncGenerator<AdapterEvent> {
|
|
868
|
+
async function* runSearchCall(call: WebSearchCall, precedingThinking: OcxThinkingContent[] = [], appendAssistant = true): AsyncGenerator<AdapterEvent> {
|
|
772
869
|
const results: { query: string; outcome: SidecarOutcome }[] = [];
|
|
773
870
|
let beganCell = false;
|
|
871
|
+
const cellItemId = newSidecarSearchCellId();
|
|
774
872
|
if (call.queries.length === 0) {
|
|
775
873
|
// The model called web_search with neither query nor queries — count it against the budget
|
|
776
874
|
// (loop-bounding) exactly as the old empty-query placeholder did, but emit no cell.
|
|
@@ -792,7 +890,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
792
890
|
// Real sidecar search. Open the cell once, before the first real query runs.
|
|
793
891
|
if (!beganCell) {
|
|
794
892
|
beganCell = true;
|
|
795
|
-
yield { type: "web_search_call_begin", id: call.id };
|
|
893
|
+
yield { type: "web_search_call_begin", id: call.id, itemId: cellItemId };
|
|
796
894
|
}
|
|
797
895
|
// F5: the anthropic sidecar authenticates with its own stored OAuth — it never touches the
|
|
798
896
|
// ChatGPT forward headers and must NOT record a Codex/OpenAI pool outcome.
|
|
@@ -845,7 +943,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
845
943
|
const callArgs: Record<string, unknown> = call.queries.length > 1
|
|
846
944
|
? { queries: call.queries }
|
|
847
945
|
: { query: call.queries[0] ?? "" };
|
|
848
|
-
messages.push({
|
|
946
|
+
if (appendAssistant) messages.push({
|
|
849
947
|
role: "assistant",
|
|
850
948
|
content: [
|
|
851
949
|
// Signed thinking must precede tool_use on replay (Anthropic extended thinking), and
|
|
@@ -867,12 +965,15 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
867
965
|
});
|
|
868
966
|
// One aggregated tool result. isError only when EVERY query failed (a partial success is usable).
|
|
869
967
|
const allFailed = results.every(r => !!r.outcome.error);
|
|
968
|
+
const resultText = formatWebSearchResults(results, !!parsed._structuredOutput);
|
|
870
969
|
messages.push({
|
|
871
970
|
role: "toolResult", toolCallId: call.id, toolName: WEB_SEARCH_TOOL_NAME,
|
|
872
|
-
content:
|
|
971
|
+
content: resultText,
|
|
873
972
|
isError: allFailed, timestamp: now,
|
|
874
973
|
});
|
|
875
974
|
if (beganCell) {
|
|
975
|
+
// The client sees only the cell; keep the result so its replay can carry it (#7269).
|
|
976
|
+
rememberSidecarSearch(parsed, cellItemId, { id: call.id, arguments: callArgs }, resultText);
|
|
876
977
|
// The cell is "completed" if any query produced a usable result, else "failed". `queries`
|
|
877
978
|
// carries every attempted query so Codex renders the native plural label.
|
|
878
979
|
const anySuccess = results.some(r => !r.outcome.error);
|
|
@@ -888,7 +989,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
888
989
|
}
|
|
889
990
|
}
|
|
890
991
|
yield {
|
|
891
|
-
type: "web_search_call_end", id: call.id,
|
|
992
|
+
type: "web_search_call_end", id: call.id, itemId: cellItemId,
|
|
892
993
|
queries: call.queries,
|
|
893
994
|
status: anySuccess ? "completed" : "failed",
|
|
894
995
|
...(sources.length > 0 ? { sources } : {}),
|
|
@@ -904,7 +1005,9 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
904
1005
|
firstPrepared = await prepareIterationDrained(false);
|
|
905
1006
|
} catch (e) {
|
|
906
1007
|
if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
|
|
907
|
-
if (e instanceof LoopError) return jsonError(e.status, e.message);
|
|
1008
|
+
if (e instanceof LoopError) return markSidecarContextOverflow(jsonError(e.status, e.message, e.event), loopFailure(e));
|
|
1009
|
+
if (e instanceof SendBudgetExhaustedError) return jsonError(429, e.message,
|
|
1010
|
+
{ type: "error", message: e.message, errorType: "rate_limit_error", code: e.code, retryable: false });
|
|
908
1011
|
throw e;
|
|
909
1012
|
}
|
|
910
1013
|
|
|
@@ -987,7 +1090,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
987
1090
|
hiddenUsage = mergeUsage(hiddenUsage, terminalEvent.usage);
|
|
988
1091
|
if (!split.hasMalformedToolCall && !split.hasRealToolCall && emptyAnswerRetries === 0) {
|
|
989
1092
|
emptyAnswerRetries++;
|
|
990
|
-
console.warn("[web-search-loop] empty forced answer — retrying once
|
|
1093
|
+
console.warn("[web-search-loop] empty forced answer — retrying once with tool calls disabled");
|
|
991
1094
|
yield { type: "heartbeat" };
|
|
992
1095
|
continue;
|
|
993
1096
|
}
|
|
@@ -1012,10 +1115,36 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
1012
1115
|
for (const event of split.passthrough.slice(split.streamedPassthroughCount)) {
|
|
1013
1116
|
if (event.type === "text_delta" && event.anthropicThinkingSignature) yield event;
|
|
1014
1117
|
}
|
|
1015
|
-
const
|
|
1118
|
+
const preserveAnthropicHistory = prepared.responseAdapter.name === "anthropic";
|
|
1119
|
+
if (preserveAnthropicHistory) {
|
|
1120
|
+
if (!split.anthropicReplay) throw new LoopError(502, "Anthropic search history cannot be replayed losslessly");
|
|
1121
|
+
const message = { role: "assistant" as const,
|
|
1122
|
+
content: anthropicSearchAssistantContent(split.events), timestamp: Date.now() };
|
|
1123
|
+
const originalCalls = split.anthropicReplay.content.filter(block => block.type === "tool_use");
|
|
1124
|
+
const priorIds = new Set(messages.flatMap(message => message.role === "assistant"
|
|
1125
|
+
? message.content.filter(part => part.type === "toolCall").map(part => part.id)
|
|
1126
|
+
: message.role === "toolResult" ? [message.toolCallId] : []));
|
|
1127
|
+
if (originalCalls.some(block => priorIds.has(block.id as string))) {
|
|
1128
|
+
throw new LoopError(502, "Anthropic search replay tool identity changed");
|
|
1129
|
+
}
|
|
1130
|
+
const calls = message.content.filter(part => part.type === "toolCall");
|
|
1131
|
+
if (originalCalls.length !== calls.length || originalCalls.some((block, index) => block.id !== calls[index]?.id)) {
|
|
1132
|
+
throw new LoopError(502, "Anthropic search replay tool identity changed");
|
|
1133
|
+
}
|
|
1134
|
+
// The raw input is authoritative, including non-empty content_block_start input.
|
|
1135
|
+
for (const [index, call] of calls.entries()) call.arguments = originalCalls[index]!.input as Record<string, unknown>;
|
|
1136
|
+
for (const call of split.calls) {
|
|
1137
|
+
const original = originalCalls.find(block => block.id === call.id);
|
|
1138
|
+
if (!original) throw new LoopError(502, "Anthropic search replay tool identity changed");
|
|
1139
|
+
call.queries = parseQueries(JSON.stringify(original.input));
|
|
1140
|
+
}
|
|
1141
|
+
bindAnthropicReplay(message, split.anthropicReplay);
|
|
1142
|
+
messages.push(message);
|
|
1143
|
+
}
|
|
1144
|
+
const iterationThinking = preserveAnthropicHistory ? [] : extractIterationThinking(split.passthrough);
|
|
1016
1145
|
for (const [callIndex, call] of split.calls.entries()) {
|
|
1017
1146
|
accountRefusalOutputStarted = true;
|
|
1018
|
-
yield* runSearchCall(call, callIndex === 0 ? iterationThinking : []);
|
|
1147
|
+
yield* runSearchCall(call, callIndex === 0 ? iterationThinking : [], !preserveAnthropicHistory);
|
|
1019
1148
|
}
|
|
1020
1149
|
} catch (e) {
|
|
1021
1150
|
// The bridge reports no usage for either error; the iterations already sent still count.
|
|
@@ -1029,7 +1158,15 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
1029
1158
|
message: "upstream translation buffer exceeded the safe limit",
|
|
1030
1159
|
};
|
|
1031
1160
|
} else {
|
|
1032
|
-
yield { type: "error",
|
|
1161
|
+
if (e instanceof SendBudgetExhaustedError) yield { type: "error", status: 429,
|
|
1162
|
+
errorType: "rate_limit_error", code: e.code, message: e.message, retryable: false };
|
|
1163
|
+
// A later iteration's overflow ends the turn as the main path's terminal
|
|
1164
|
+
// context_length_exceeded, so Codex compacts instead of resending (#7316).
|
|
1165
|
+
else yield (e instanceof LoopError ? sidecarLoopTerminalEvent(loopFailure(e)) : undefined)
|
|
1166
|
+
?? (e instanceof LoopError && e.event ? e.event
|
|
1167
|
+
: e instanceof LoopError && deps.thinkingRecovery
|
|
1168
|
+
? { type: "error", status: e.status, errorType: "upstream_error", message: e.message }
|
|
1169
|
+
: { type: "error", message: e instanceof Error ? e.message : String(e) });
|
|
1033
1170
|
}
|
|
1034
1171
|
return;
|
|
1035
1172
|
}
|
|
@@ -1037,6 +1174,8 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
1037
1174
|
if (hiddenUsage) deps.onUsage?.(hiddenUsage);
|
|
1038
1175
|
yield { type: "error", message: "web-search loop exceeded its iteration cap" };
|
|
1039
1176
|
} finally {
|
|
1177
|
+
for (const snapshot of replaySnapshots) snapshot.release();
|
|
1178
|
+
replaySnapshots.clear();
|
|
1040
1179
|
if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
|
|
1041
1180
|
}
|
|
1042
1181
|
}
|
|
@@ -26,7 +26,15 @@ export class WebSearchStreamProtocolError extends Error {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Opt-in preserves an adapter refusal before it becomes an untyped parser exception. */
|
|
30
|
+
export class WebSearchAdapterEventError extends Error {
|
|
31
|
+
constructor(readonly event: Extract<AdapterEvent, { type: "error" }>, readonly replaySafe: boolean) {
|
|
32
|
+
super(event.message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
export interface ParseStreamWithProgressOptions {
|
|
37
|
+
preserveAdapterError?: boolean;
|
|
30
38
|
inactivityTimeoutMs: number;
|
|
31
39
|
translatorBudget: TranslatorBudget;
|
|
32
40
|
signal?: AbortSignal;
|
|
@@ -249,6 +257,7 @@ export async function* parseStreamWithProgress(
|
|
|
249
257
|
|
|
250
258
|
resetInactivity();
|
|
251
259
|
|
|
260
|
+
let replaySafe = true;
|
|
252
261
|
const parserPump = (async (): Promise<void> => {
|
|
253
262
|
let heldTerminal: Extract<AdapterEvent, { type: "done" | "incomplete" }> | undefined;
|
|
254
263
|
try {
|
|
@@ -299,11 +308,15 @@ export async function* parseStreamWithProgress(
|
|
|
299
308
|
}
|
|
300
309
|
if (event.type === "error") {
|
|
301
310
|
if (event.usage) options.onErrorUsage?.(event.usage);
|
|
311
|
+
// An adapter's own overflow verdict keeps its typed event so the sidecar can end the
|
|
312
|
+
// turn as context_length_exceeded (#7316); other errors keep the plain-message shape.
|
|
313
|
+
const preserve = options.preserveAdapterError || event.code === "context_length_exceeded";
|
|
302
314
|
fail(event.code === "translation_buffer_limit"
|
|
303
315
|
? new TranslatorBudgetExceededError("live_transient", TRANSLATOR_MAX_TURN_BYTES)
|
|
304
|
-
: new Error(event.message));
|
|
316
|
+
: preserve ? new WebSearchAdapterEventError(event, replaySafe) : new Error(event.message));
|
|
305
317
|
return;
|
|
306
318
|
}
|
|
319
|
+
replaySafe &&= event.type === "heartbeat" && event.replayUnsafe !== true;
|
|
307
320
|
if (event.type === "done" || event.type === "incomplete") {
|
|
308
321
|
heldTerminal = event;
|
|
309
322
|
// Response-byte inactivity ends once the adapter has produced a terminal event.
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type TranslatorBudget,
|
|
44
44
|
} from "../lib/translator-budget";
|
|
45
45
|
import { jsonUtf8Bytes } from "../lib/json-byte-size";
|
|
46
|
+
import { newSidecarSearchCellId, rememberSidecarSearch, restoreSidecarSearchResults } from "./sidecar-search-replay";
|
|
46
47
|
|
|
47
48
|
export interface RunTurnWebSearchDeps {
|
|
48
49
|
parsed: OcxParsedRequest;
|
|
@@ -77,6 +78,8 @@ export interface RunTurnWebSearchDeps {
|
|
|
77
78
|
* active: same request plus the synthetic tool so the model can ask to search.
|
|
78
79
|
*/
|
|
79
80
|
export function runTurnWebSearchInitialParsed(parsed: OcxParsedRequest): OcxParsedRequest {
|
|
81
|
+
// Runs before the first dispatch: put recorded results back into replayed search pairs (#7269).
|
|
82
|
+
restoreSidecarSearchResults(parsed);
|
|
80
83
|
return {
|
|
81
84
|
...parsed,
|
|
82
85
|
context: {
|
|
@@ -296,6 +299,7 @@ export async function* runTurnWebSearchLoop(
|
|
|
296
299
|
): AsyncGenerator<AdapterEvent> {
|
|
297
300
|
const results: { query: string; outcome: SidecarOutcome }[] = [];
|
|
298
301
|
let beganCell = false;
|
|
302
|
+
const cellItemId = newSidecarSearchCellId();
|
|
299
303
|
if (call.queries.length === 0) {
|
|
300
304
|
searchesExecuted++;
|
|
301
305
|
results.push({ query: "", outcome: { text: "", sources: [], error: "the model called web_search with an empty query" } });
|
|
@@ -318,7 +322,7 @@ export async function* runTurnWebSearchLoop(
|
|
|
318
322
|
} else {
|
|
319
323
|
if (!beganCell) {
|
|
320
324
|
beganCell = true;
|
|
321
|
-
yield { type: "web_search_call_begin", id: call.id };
|
|
325
|
+
yield { type: "web_search_call_begin", id: call.id, itemId: cellItemId };
|
|
322
326
|
}
|
|
323
327
|
if (abortSignal?.aborted) {
|
|
324
328
|
yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: "failed" };
|
|
@@ -358,12 +362,14 @@ export async function* runTurnWebSearchLoop(
|
|
|
358
362
|
timestamp: now,
|
|
359
363
|
});
|
|
360
364
|
const allFailed = results.every(r => !!r.outcome.error);
|
|
365
|
+
const resultText = formatWebSearchResults(results, !!parsed._structuredOutput);
|
|
361
366
|
appendMessage({
|
|
362
367
|
role: "toolResult", toolCallId: call.id, toolName: WEB_SEARCH_TOOL_NAME,
|
|
363
|
-
content:
|
|
368
|
+
content: resultText,
|
|
364
369
|
isError: allFailed, timestamp: now,
|
|
365
370
|
});
|
|
366
371
|
if (beganCell) {
|
|
372
|
+
rememberSidecarSearch(parsed, cellItemId, { id: call.id, arguments: callArgs }, resultText);
|
|
367
373
|
const anySuccess = results.some(r => !r.outcome.error);
|
|
368
374
|
const sources: { url: string; title?: string }[] = [];
|
|
369
375
|
const seenSrc = new Set<string>();
|
|
@@ -375,7 +381,7 @@ export async function* runTurnWebSearchLoop(
|
|
|
375
381
|
}
|
|
376
382
|
}
|
|
377
383
|
yield {
|
|
378
|
-
type: "web_search_call_end", id: call.id,
|
|
384
|
+
type: "web_search_call_end", id: call.id, itemId: cellItemId,
|
|
379
385
|
queries: call.queries,
|
|
380
386
|
status: anySuccess ? "completed" : "failed",
|
|
381
387
|
...(sources.length > 0 ? { sources } : {}),
|