Skip to content

fix(responses): handle replayed hosted web-search history (#7250, #7269) - #7353

Merged
lidge-jun merged 6 commits into
devfrom
fix/hosted-search-history-and-remove-free
Oct 19, 2026
Merged

lidge-jun merged 6 commits into
devfrom
fix/hosted-search-history-and-remove-free

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

This fixes two related ways that replayed hosted web-search history goes wrong. It absorbs and supersedes community PR #7268.

Native Sol 500 on search history without a search tool (#7250). If an earlier turn produced a web_search_call item (for example on Gemini) and the conversation switches to gpt-6.1-sol with a request that declares no web_search tool, the ChatGPT backend answers with a generic SSE server_error. Codex retries that request until it gives up. Before this change, the passthrough adapter forwarded those cells unchanged. Now the final Responses serializer converts every unreferenced web_search_call into the existing bounded, untrusted assistant reference note whenever the final outbound body declares no hosted search tool. That covers tools, Lite additional_tools, and the case where the destination's unsupported hosted tools were stripped. This applies on every Responses destination and on both HTTP and WS, because WS sends the same body. Requests that do declare search keep structural history. A function tool named web_search does not count as a declaration. No search tool is added, no search is rerun, and no result is invented. Cells referenced by a same-input item_reference stay structural. The exact synthetic request from the issue is now a regression test on the canonical Codex endpoint, the public API, and a third-party gateway, with a declared-tool control for each.

#7268 by @EricFeng9 is cherry-picked with authorship kept. It removes the _compactionRequest exclusion so the existing projectResponsesLiteHostedSearchHistory opt-in also covers native v2 compaction_trigger requests. Because native v2 compaction carries an empty tool list, the new missing-declaration rule now projects that history even without the opt-in. The opt-in still decides what happens when search is declared. A follow-up commit translates that PR's source and test comments into English.

Search results never reach routed models such as anthropic/claude-opus-5-5 (#7269). The sidecar search loop gives the model its results inside the turn. When the model then calls a client tool (shell), the turn ends. Codex replays only the hosted cell, which carries the query and no result text, and src/responses/parser.ts dropped that cell for routed providers. On the next request the model had no record that it had searched, so it searched again and reran the tool. That matches the report's 18 searches and 18 identical shell commands, and the resumed turn saying "I never called the search tool". The fix has two parts:

  • When the current request declares hosted search, the parser rewrites each replayed cell, before its item loop, into the web_search function_call the routed model made, plus its function_call_output. The output is deferred past the following run of call/output items, so a cell inside a parallel client batch never splits it, and reasoning attaches the same way as for any other call. Synthetic call IDs avoid every client call_id and each other, and they map back to the cell ID. The fallback output names the queries, states that the result text is not available, and lists up to 20 untrusted titles and URLs. Without a declaration the cell stays out of routed history, as before.
  • When no search plan resolves (sidecar disabled, no credentials, tool choice excludes search, compaction), executeResponsesSidecars and executeResponsesRunTurn remove the pairs again, because the synthetic tool would be undeclared on the wire.
  • Both search loops (loop.ts, run-turn-loop.ts) now name the cell they emit. Both bridges honour a well-formed ws_ itemId on web_search_call_begin/_end. The loops record the result text in src/web-search/sidecar-search-replay.ts, which reuses the bounded, in-memory, never-logged store from [Provider compatibility] Bridged hosted web_search result is not replayed to the destination on the next turn #4587. Before the first iteration of the next turn, the recorded result is swapped into the rebuilt pair. The memo is keyed by caller principal and conversation only, because the proxy ran the search with its own backend, so the result survives a model switch within one conversation. It fails closed when either key is missing.

Closes #7250
Closes #7269
Supersedes #7268

Co-authored-by: Junming Feng 12311031@mail.sustech.edu.cn

Decisions (HOTL)

  • For [Provider compatibility] Sol returns server_error for search history without a search tool declaration #7250 I chose lossy projection over a local refusal. A refusal leaves the user stuck after a model switch, and adding a declaration would grant a capability the caller did not ask for. Recorded as structure/decisions/ADR-7250-undeclared-hosted-search-history.md. This narrows the earlier ADR-6930 position that an absent declaration should not trigger projection: here the trigger is a request-shape fact, and Lite intent is not inferred.
  • The rule applies to all Responses destinations, because the condition (a hosted call with no hosted tool) has no valid interpretation on any of them. The cost is lossy notes on a destination that might have accepted the cells.
  • For [Bug]: Built-in web_search results never reach anthropic/claude-opus-5-5 under Codex CLI (model repeats steps, invents URLs) #7269, pairs are built at the raw-input level so the parser keeps owning turn batching. They survive only when a search plan resolves, which is when the synthetic tool is actually declared. An independent review (GPT-6.1-Sol) of the first version found three problems: undeclared calls without a sidecar, split client batches, and colliding IDs. All three are fixed, and each has a regression test.

Verification

  • bun run typecheck: passed.
  • bun test tests/adapters/openai/openai-native-hosted-search-history.test.ts tests/responses/responses-native-compact-search-history.test.ts tests/responses/responses-compaction-override.test.ts tests/config/config-hosted-search-projection.test.ts: 152 pass, 0 fail.
  • bun test tests/web-search/web-search-sidecar-replay.test.ts (new): 12 pass. It covers result recovery through a real runWithWebSearch two-turn flow, isolation by conversation and caller, keyless fallback, the fallback text, failed cells, no declaration, parallel-batch placement, reasoning grouping, collision-free IDs, and removal without a plan. One test runs handleResponses against an Anthropic provider with the sidecar off and asserts that no web_search tool_use reaches the wire; it fails when the removal is disabled.
  • bun test tests/responses/openai-responses-passthrough.test.ts tests/web-search/web-search-bridge-replay.test.ts tests/server/server-key-failover-e2e.test.ts: 260 pass. The fixtures that replay a cell and expect structural output now declare web_search, as Codex does on every turn. The passthrough test file stays at its line cap.
  • bun test on every file referencing web_search_call, plus tests/web-search tests/responses tests/bridge: the only failures were compaction and retention groups that pass when each file runs alone (reasoning-retention-v2, responses-compaction-override, reasoning-retention-routing, anthropic-model-weekly-admission). They come from running whole directories in one process.
  • bun run test:changed: 43,615 pass, 6 fail. All six are client-home and link-runtime ownership tests (tests/cli/sibling-home-client-sync.test.ts, tests/clients/client-link-runtime.test.ts) that pass when run alone (40/40); they are timing flakes on a loaded machine.
  • Focused regression set (sidecar replay, web-search, run-turn loop, Anthropic search, bridge replay, bridges, hosted-search history, parser suites): 507 pass, 0 fail.
  • Layout, file-size ratchet, bun run structure:check and bun run privacy:scan: passed.
  • The full bun run test was not run locally because several agent worktrees share this machine; CI covers the full suite. No live upstream requests were made.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

EricFeng9 and others added 3 commits October 19, 2026 15:12
…declared (#7250)

Native Sol answers a replayed web_search_call without a matching web_search declaration with a generic server_error. Project unreferenced cells into the existing bounded reference notes whenever the final body declares no hosted search, on every Responses destination; declared requests keep structural history. Also translates the comments carried over from #7268 into English.
Codex replays a hosted web_search_call cell without its result text, and the parser dropped it for routed providers, so a model whose turn ended on a client tool call forgot it had searched and repeated the search and the tool. Rebuild the cell as a web_search call/result pair when search is declared, and let the sidecar loops record the result under the emitted cell id so the next turn restores it, scoped to caller and conversation.
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Oct 19, 2026
…p them without a sidecar

Review round 1: the parse-time pair could call an undeclared web_search tool when no sidecar plan resolves, split a parallel client batch, and collide with client call ids. Rewrite cells into function_call/output items before the item loop, defer the output past the following call batch, allocate collision-free call ids mapped back to cell ids, and remove the pairs when the search plan is undefined.
Review round 2: a deferred search output crossed into the following assistant turn when consecutive client batches followed the cell, and a restored failed search lost its error flag. Flush the deferred output when a call follows the batch outputs, and set the flag independently of the restored text.
@lidge-jun
lidge-jun marked this pull request as ready for review October 19, 2026 06:43
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner October 19, 2026 06:43
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T06:50:40.801222Z 1f0370d Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f0370d05d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

export function rewriteReplayedWebSearchCells(input: readonly unknown[]): RewrittenWebSearchInput | undefined {
if (!input.some(item => isObj(item) && item.type === "web_search_call")) return undefined;
// A synthetic call id must not collide with any client call id, nor with another cell's.
const taken = new Set(input.flatMap(item => isObj(item) && typeof item.call_id === "string" ? [item.call_id] : []));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve fallback IDs used by local-shell calls

When a replayed cell's id matches a local_shell_call.id or tool_search_call.id on an item without call_id, this set misses the collision even though parseRequest treats those IDs as call.call_id ?? call.id. The rewrite then emits two tool calls with the same effective ID, so the subsequent output can attach to the synthetic web_search call instead of the original tool and corrupt the routed history. Reserve the parser's effective fallback IDs for these call types as well.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants