Skip to content

feat(web-search): exa executor and the non-LLM search lane (#2188 L9) - #2245

Merged
lidge-jun merged 6 commits into
devfrom
codex/sidecar-exa-executor
Aug 21, 2026
Merged

feat(web-search): exa executor and the non-LLM search lane (#2188 L9)#2245
lidge-jun merged 6 commits into
devfrom
codex/sidecar-exa-executor

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Summary

L9 of the #2188 sidecar chain: a live Exa executor giving the web-search sidecar its first non-LLM lane.

  • src/web-search/exa-executor.ts: POST to the pinned https://api.exa.ai/search origin (manual redirect), maps results to the shared digest/sources shape.
  • Literal-key scrub: the executor knows the operator secret, so error paths scrub the literal key value (scrub BEFORE truncation - a boundary-straddling key cannot leak a prefix), on top of redactSecretString.
  • exaApiKey never rides the serialized SidecarPlan; core.ts reads it from config at unpack time. Plan carries only an exaConfigured presence marker.
  • Fail-closed: backend: exa without a key plans to nothing / errors the tool call; no silent LLM fallback, no fetch, no pool recording.
  • Registry + loop arms stay inert for openai/anthropic/xai/gemini lanes.
  • docs-site sidecars guide gains the exa row.

Verification

  • bun test tests/exa-web-search.test.ts - 9/9 (incl. key canaries: upstream echo, truncation-boundary, fetch-rejection).
  • bun run typecheck, bun run privacy:scan green locally.
  • Remote full suite on the release host at the prior head: 13808 pass / 0 fail (875 files); relaunched at head 95ccd33 as a lagging indicator.
  • Live E2E through the running proxy: real Exa query returned 5435 chars + 5 sources.
  • Adversarial reviewer rounds: round 1 found the scrub-after-truncate blocker (fixed in 95ccd33); round-2 verdict PASS.

Checklist

  • Targets the parent layer (codex/sidecar-gemini-executor), stacked-PR workflow per AGENTS.md
  • Focused regression tests added
  • No secrets in logs, plans, or test snapshots
  • Docs updated

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner August 21, 2026 02:35
@lidge-jun lidge-jun added the enhancement New feature or request label Aug 21, 2026
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@lidge-jun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 18 minutes

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 543cb249-a255-433f-8be8-3aca6d4d511c

📥 Commits

Reviewing files that changed from the base of the PR and between 738eed7 and 796f63c.

📒 Files selected for processing (13)
  • devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md
  • devlog/_plan/260820_sidecar_selection_unification/110_global_merge_order.md
  • devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md
  • devlog/_plan/260820_sidecar_selection_unification/130_xai_responses_optin_switch.md
  • devlog/_plan/260820_sidecar_selection_unification/140_release_prep.md
  • devlog/_plan/260820_sidecar_selection_unification/150_lidge_final_gate.md
  • docs-site/src/content/docs/guides/sidecars.md
  • src/server/responses/core.ts
  • src/web-search/backends.ts
  • src/web-search/exa-executor.ts
  • src/web-search/index.ts
  • src/web-search/loop.ts
  • tests/exa-web-search.test.ts

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 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed.

Hygiene

Deterministic PR hygiene checks passed.

@github-actions
github-actions Bot marked this pull request as draft August 21, 2026 02:36
@github-actions
github-actions Bot marked this pull request as ready for review August 21, 2026 02:36

@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: 95ccd33270

ℹ️ 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".

Comment on lines +50 to +51
if (!res.ok) {
const t = await res.text().catch(() => "");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard the Exa response body before reading it

If the client cancellation or sidecar timeout fires after fetch() resolves but before either res.text() or res.json() attaches a reader, Bun can reject the response body's internal read as an unhandled rejection rather than routing it through this try/catch; the existing web-search executors use cancelBodyOnAbort immediately after receiving headers specifically to close this race. Attach that guard before branching on res.ok and detach it after both the error-body and JSON reads so cancellation degrades through the normal sidecar outcome instead of escaping the request path.

AGENTS.md reference: src/AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Comment on lines +40 to +41
const res = await fetchWithResetRetry(
() => fetch(EXA_SEARCH_URL, {

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 Apply the requested transport recovery to Exa retries

When Bun reports a stale pooled-connection reset, fetchWithResetRetry invokes this callback with "connection-reset", but the callback ignores that argument and recreates the same ordinary fetch. As documented by applyUpstreamRecoveryInit in src/lib/upstream-retry.ts, Bun may reuse another half-closed pooled socket unless the retry sets keepalive: false (and Connection: close), so an otherwise recoverable Exa search can exhaust all attempts and return a tool error. Accept the recovery argument and apply it to the request init.

Useful? React with 👍 / 👎.

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Requesting changes on exact head 95ccd33270b7e720293755acb6ae7fe767e228cf. The Exa lane is a useful addition and the secret handling is directionally strong: the key stays out of the serialized plan, the literal key is scrubbed before truncation, redirects are disabled, missing credentials fail closed, and the focused suite passes 9/9 with typecheck and privacy scan clean.

Two transport blockers remain, and both automated findings are valid against the current code:

  1. Attach cancelBodyOnAbort(res.body, linkedSignal.signal) immediately after fetchWithResetRetry returns, before either res.text() or res.json() starts reading. Detach it in finally on both the error-body and success-body paths. Otherwise cancellation between header resolution and reader attachment can escape as Bun's unhandled body rejection instead of a normal sidecar error.
  2. Accept the recovery argument supplied by fetchWithResetRetry and pass the request init through applyUpstreamRecoveryInit. The current callback ignores "connection-reset", so retries can reuse the same half-closed pooled socket instead of setting keepalive: false and Connection: close.

Please add focused regressions for the abort-before-reader race and the recovery init on the retry attempt. The current exact-head CI also has test 1/4 red and macOS still pending. This stacked layer must remain open until these fixes land, CI is green, and its parent #2243 blockers are resolved.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 58 / 80

#2188 L9임. 베이스가 codex/sidecar-gemini-executor (#2243 L8). 지금 dev HEAD 826a1b7a4 planWebSearch에 exa 암 없음. exaApiKey 필드도 src/types/config.ts에 아직 없음 — L6(#2238)가 넣을 예정. GET /api/sidecar-settings (src/server/management/config-routes.ts:511-522)는 키를 안 울림. 이 PR은 그 키로 첫 non-LLM 레인 runExaWebSearch를 살림. 부모 없이 리타깃하면 깨짐.

키 위생이 본문임. exaApiKeySidecarPlan에 안 탐. 플랜은 exaConfigured: true 마커만. core.ts가 unpack 때 config에서 읽음. 실행기가 시크릿을 아니까 redactSecretString 위에 리터럴 split. 스크럽을 truncate 전에 함. 경계에 걸친 키가 prefix로 안 남음. redirect: "manual" — Bun이 커스텀 헤더를 리다이렉트에 실음. origin 핀 https://api.exa.ai/search. 키 없으면 plan 없음 / 루프 error. 포워드 실행기·풀 레코더 안 탐. eligibleModel: () => false라 GUI 모델 목록을 안 더럽힘. 테스트가 upstream echo / truncation-boundary / fetch-rejection 카나리.

mapExaSearchResponse가 랭킹 JSON을 digest로 바꿈. LLM 답이 아님. 라우티드 모델이 합성함. results 없으면 error. url 디듑. snippet 1000자. 라이브 E2E가 5435자 + 5 소스라고 함. 방향 맞음. #2188이 원하던 non-LLM 레인임.

같이 들어 있는 devlog/_plan/.../100_chat_default_regression.md ~ 150_lidge_final_gate.md는 L9 본문 아님. 130_xai_responses_optin_switch.md#2217/#2227 modelWireDefaults.wire 싸움을 사이드카 체인에 붙임. 그 싸움은 #2240 트래킹임. 여기 넣지 말 것. docs-site 사이드카 가이드가 xai 행에 opt-in x_search를 적음. 그건 #2242가 #2190이랑 섞인 그거. L9 독스가 그 충돌을 고정하면 안 됨.

src/types/config.ts 안 건드림. 키 필드는 L6 것. 스플릿이 플랜/루프를 옮기면 리베이스하지 말고 닫고 다시 짜라. 지금은 그 정도 아님. 닫을 중복 아님. 지금 HEAD 위에 바로 머지하지 말 것. L1→L8 먼저. 2.28 블로커 아님. 프리뷰 배포 플랜 아님.

해결방안: #2243 머지 후 리타깃. 키는 플랜에 넣지 말 것. 스크럽-before-truncate 유지. 머지오더/Responses 옵트인 독스(100-150, 특히 130)는 이 PR에서 빼거나 #2240 쪽으로. xai 가이드에서 x_search 행은 #2190 병합 전까지 빼라.

이 댓글은 grok-bot이 작성했습니다

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Re-reviewed exact head 9982adf0f6c9bda4b5431d94c506674cb355c2e1. The commits since my 95ccd3327 review add only devlog planning files; src/web-search/exa-executor.ts is unchanged, so both transport blockers remain:

  1. Attach cancelBodyOnAbort immediately after headers and cover both success/error reads with a finally detach. Add the abort-after-headers regression.\n2. Accept the recovery argument from fetchWithResetRetry and apply applyUpstreamRecoveryInit to the retry request. Add a regression that proves the connection-reset retry disables pooled reuse.

The owner review also identifies scope blockers in this head:
3. Remove the six devlog/_plan/.../100 through 150 files from this L9 PR. They are merge/release planning plus the separate Grok Responses default dispute, not Exa executor implementation.\n4. Do not make the x_search documentation part of this Exa layer. Keep the Exa and live Gemini documentation needed by this stack, but leave the separate #2190 opt-in contract out until that direction lands independently.
In addition, this layer is based on #2243, which now has unresolved auth-deadline, GUI backend-provenance, and documentation blockers. Keep #2245 stacked and rerun exact-head CI only after the parent and these current-head issues are fixed.

@lidge-jun
lidge-jun force-pushed the codex/sidecar-gemini-executor branch from 249cc91 to ef551e8 Compare August 21, 2026 03:39
runExaWebSearch POSTs api.exa.ai/search with the operator key and maps
ranked results to a digest the routed model synthesizes from. The key
never rides the SidecarPlan — core.ts reads it from config at unpack
time — and the executor scrubs the literal key from every error string
(pattern-based redaction cannot know an arbitrary operator key;
canary-tested). Plan, loop, and registry arms fail closed without the
key. docs-site gains the explicit-only backend table.
Reviewer blocker (L9 round 2): error(t.slice(0,200)) truncated before the literal-key scrub, so a key straddling the 200-char boundary left an unscrubbable prefix in the returned tool error. Scrub first, then slice. Adds truncation-boundary and fetch-rejection canaries; 9/9 focused tests, tsc and privacy:scan green.
…global order, opt-in switch)

Amends the 260820 unit with the audited (3-round sol-medium, round-3 PASS) roadmap: 100 chat-default regression as an atomic #2227+tier-policy unit with a 5-row regression matrix and the E2E reasoning-streaming proof; 110 global cross-train merge order and 21-PR triage matrix (#2072 deferred, #2217 RESHAPE); 120 sidecar L1-L9 merge execution with the fresh blocker inventory; 130 atomic xai Responses opt-in switch (single provider id, auth-mode-scoped sections, virtual PATCH field); 140 release prep; 150 blocking lidge final gate. DeepSeek explicitly out of scope per user decision.
Split the opt-in DTO into a write boolean vs read tri-state; record the concrete #2238 (3) and #2242 (5) review blockers in doc 120; recast doc 150 as the final aggregate gate with the full GUI/i18n/docs chain; replace temporal API-key rows with exact wire+tier assertions; state the explicit wp9->wp8->wp11->wp10 execution sequence.
…rding

C-gate round 2: current dev forwards caller service_tier verbatim on the API-key + explicit openai-responses route (fastPolicyForModel proof). The tier drop is an OAuth-route policy only; the API-key row now states preserve-current semantics, consistent with doc 130.
@lidge-jun
lidge-jun force-pushed the codex/sidecar-exa-executor branch from 9982adf to 796f63c Compare August 21, 2026 03:40
@lidge-jun
lidge-jun changed the base branch from codex/sidecar-gemini-executor to dev August 21, 2026 03:40
@lidge-jun
lidge-jun merged commit 84d5523 into dev Aug 21, 2026
3 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants