From 1c0830dc922f32007b7a2fff8426dc7237fa0489 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 21 Aug 2026 11:11:17 +0900 Subject: [PATCH 1/6] feat(web-search): exa executor and the non-LLM search lane (#2188 L9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs-site/src/content/docs/guides/sidecars.md | 12 ++ src/server/responses/core.ts | 2 + src/web-search/backends.ts | 7 + src/web-search/exa-executor.ts | 86 +++++++++++ src/web-search/index.ts | 23 ++- src/web-search/loop.ts | 8 + tests/exa-web-search.test.ts | 137 ++++++++++++++++++ 7 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 src/web-search/exa-executor.ts create mode 100644 tests/exa-web-search.test.ts diff --git a/docs-site/src/content/docs/guides/sidecars.md b/docs-site/src/content/docs/guides/sidecars.md index 4cf009abc9..d92316d796 100644 --- a/docs-site/src/content/docs/guides/sidecars.md +++ b/docs-site/src/content/docs/guides/sidecars.md @@ -18,6 +18,18 @@ closed. Explicit `xai` requires a usable stored Grok OAuth account and does not requires both ChatGPT login auth and an enabled `forward` provider. ::: +### Additional web-search backends (explicit-only) + +Three more web-search backends exist beyond the ChatGPT and Claude paths. Each is +**explicit-only** — it never activates from credential presence — and **fails closed**: +a missing credential produces no sidecar plan and the request takes the normal routed path. + +| Backend | Runs | Credential | Notes | +| --- | --- | --- | --- | +| `xai` | Grok hosted `web_search` (+ opt-in `x_search`) on `api.x.ai` Responses | Stored Grok OAuth (`ocx login xai`) | `webSearchSidecar.xSearch` enables X search with `allowedXHandles`/`excludedXHandles` (max 20, mutually exclusive) and ISO `fromDate`/`toDate`. Default model `grok-4.6`. | +| `gemini` | `google_search` grounding on the Antigravity transport | Stored Antigravity OAuth with a discovered project (`ocx login google-antigravity`) | Default model `gemini-3.7-flash`; reasoning maps to the tiered thinking level. | +| `exa` | Exa Search API (non-LLM result digest) | `webSearchSidecar.exaApiKey` | The key is write-only through the management API (never echoed, redacted from logs). No sidecar model applies. | + ## Web-search sidecar When Codex requests hosted `web_search` for a non-passthrough routed model, opencodex: diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 0f94e89b60..7d661fd763 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -3589,6 +3589,8 @@ async function handleResponsesInner( xaiSidecar: wsPlan.xaiSidecar, geminiSidecar: wsPlan.geminiSidecar, xaiSearchOptions: wsPlan.xaiSearchOptions, + // The exa key never rides the plan: read it from config at unpack time (L9). + ...(wsPlan.exaConfigured ? { exaApiKey: config.webSearchSidecar?.exaApiKey } : {}), hostedTool: wsPlan.hostedTool, selectedForwardHeaders: wsPlan.forwardSidecar?.headers ?? selectedForwardHeaders, settings: wsPlan.settings, diff --git a/src/web-search/backends.ts b/src/web-search/backends.ts index 2531d57096..ee07e81b7d 100644 --- a/src/web-search/backends.ts +++ b/src/web-search/backends.ts @@ -79,6 +79,13 @@ export const WEB_SEARCH_BACKENDS: readonly WebSearchBackendDescriptor[] = [ }, eligibleModel: candidate => candidate.provider === "google-antigravity", }, + { + backend: "exa", + // Probe = operator key present. Exa is not an LLM: no candidate models ever + // match, so the GUI's model list stays untouched by this backend. + isActive: (_auth, config) => !!config.webSearchSidecar?.exaApiKey, + eligibleModel: () => false, + }, ]; /** diff --git a/src/web-search/exa-executor.ts b/src/web-search/exa-executor.ts new file mode 100644 index 0000000000..c93d0b612b --- /dev/null +++ b/src/web-search/exa-executor.ts @@ -0,0 +1,86 @@ +/** + * Execute ONE web search via the Exa Search API (#2188 L9) — the non-LLM lane. + * + * Exa returns ranked result JSON, not a prose answer: the outcome's text is a + * per-result digest the routed model synthesizes from. Probe-verified 2026-08-21 + * (devlog 002): POST https://api.exa.ai/search with x-api-key returns + * {requestId, results[{title,url,publishedDate,text}], costDollars}. + * redirect: "manual" because Bun forwards custom headers across redirects. + * Never throws; every error string passes redactSecretString. + */ +import { fetchWithResetRetry } from "../lib/upstream-retry"; +import { signalWithTimeout } from "../lib/abort"; +import { sidecarEnter } from "../lib/sidecar-tracker"; +import { redactSecretString } from "../lib/redact"; +import type { WebSearchSource } from "./parse"; +import type { SidecarOutcome, SidecarSettings } from "./executor"; + +const EXA_SEARCH_URL = "https://api.exa.ai/search"; +const EXA_NUM_RESULTS = 5; +const EXA_SNIPPET_CHARS = 1000; + +function isRec(v: unknown): v is Record { + return !!v && typeof v === "object" && !Array.isArray(v); +} + +export async function runExaWebSearch( + query: string, + apiKey: string, + settings: SidecarSettings, + abortSignal?: AbortSignal, +): Promise { + if (!apiKey) return { text: "", sources: [], error: "exa backend selected without an exaApiKey" }; + // The executor KNOWS the secret — pattern-based redaction cannot be trusted to + // recognize an arbitrary operator key, so scrub the literal value explicitly. + const scrub = (s: string) => redactSecretString(s.split(apiKey).join("[redacted-exa-key]")); + const linkedSignal = signalWithTimeout(settings.timeoutMs, abortSignal); + const sidecarExit = sidecarEnter("web-search"); + const t0 = Date.now(); + try { + const res = await fetchWithResetRetry( + () => fetch(EXA_SEARCH_URL, { + method: "POST", + headers: { "Content-Type": "application/json", "x-api-key": apiKey }, + body: JSON.stringify({ query, numResults: EXA_NUM_RESULTS, contents: { text: { maxCharacters: EXA_SNIPPET_CHARS } } }), + signal: linkedSignal.signal, + redirect: "manual", + }), + { abortSignal: linkedSignal.signal, label: "exa-web-search-sidecar" }, + ); + if (!res.ok) { + const t = await res.text().catch(() => ""); + return { text: "", sources: [], error: `exa sidecar HTTP ${res.status}: ${scrub(t.slice(0, 200))}` }; + } + const payload = await res.json().catch(() => null); + return mapExaSearchResponse(payload); + } catch (e) { + const kind = e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error"; + console.warn(`[web-search] exa sidecar ${kind} (${Date.now() - t0}ms)`); + return { text: "", sources: [], error: scrub(e instanceof Error ? e.message : String(e)) }; + } finally { + sidecarExit(); + linkedSignal.cleanup(); + } +} + +/** Map an Exa /search payload to a digest the routed model can synthesize from. */ +export function mapExaSearchResponse(payload: unknown): SidecarOutcome { + if (!isRec(payload) || !Array.isArray(payload.results)) { + return { text: "", sources: [], error: "exa sidecar returned a non-JSON or shapeless body" }; + } + const sources: WebSearchSource[] = []; + const lines: string[] = []; + const seen = new Set(); + for (const result of payload.results) { + if (!isRec(result) || typeof result.url !== "string" || result.url.length === 0) continue; + if (seen.has(result.url)) continue; + seen.add(result.url); + const title = typeof result.title === "string" && result.title.length > 0 ? result.title : result.url; + sources.push({ url: result.url, ...(title !== result.url ? { title } : {}) }); + const snippet = typeof result.text === "string" ? result.text.trim().slice(0, EXA_SNIPPET_CHARS) : ""; + const dated = typeof result.publishedDate === "string" ? ` (${result.publishedDate.slice(0, 10)})` : ""; + lines.push(`- ${title}${dated}: ${snippet || "(no excerpt)"} [${result.url}]`); + } + if (lines.length === 0) return { text: "", sources: [], error: "exa sidecar returned no results" }; + return { text: `Search results:\n${lines.join("\n")}`, sources }; +} diff --git a/src/web-search/index.ts b/src/web-search/index.ts index f02b2df7c4..8dad3b3d73 100644 --- a/src/web-search/index.ts +++ b/src/web-search/index.ts @@ -15,6 +15,7 @@ export { buildWebSearchTool, extractHostedWebSearch, WEB_SEARCH_TOOL_NAME }; export { runAnthropicWebSearch, parseAnthropicSidecarSSE } from "./anthropic-executor"; export { runXaiWebSearch, parseXaiResponsesSSE, validateXaiSearchOptions, type XaiSearchOptions } from "./xai-executor"; export { runGeminiWebSearch, mapCcaGroundedResponse } from "./gemini-executor"; +export { runExaWebSearch, mapExaSearchResponse } from "./exa-executor"; const DEFAULT_SIDECAR_MODEL = "gpt-5.6-luna"; // Default Claude model for the anthropic-backed sidecar (used when cfg.model is unset). @@ -178,6 +179,8 @@ export interface SidecarPlan { geminiSidecar?: { providerName: string; provider: OcxProviderConfig }; /** Opt-in x_search options for the xai backend (validated at the management layer and again in the executor). */ xaiSearchOptions?: XaiSearchOptions; + /** Presence marker for the exa backend — the API key itself never rides the plan. */ + exaConfigured?: true; hostedTool: Record; settings: SidecarSettings; maxSearches: number; @@ -228,9 +231,6 @@ export function planWebSearch( ? { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider } : undefined; const backend = resolveSidecarBackend(cfg.backend); - // Inert arm (roadmap 060): exa stays fail-closed until its executor layer lands. - // xai went live in L7; gemini in L8 below. - if (backend === "exa") return undefined; const maxSearches = cfg.maxSearchesPerTurn ?? DEFAULT_MAX_SEARCHES; const stallTimeoutSec = webSearchStallTimeoutSec( config.stallTimeoutSec, @@ -299,6 +299,23 @@ export function planWebSearch( }; } + // exa (L9): explicit-only, keyed by the operator-supplied exaApiKey. The KEY never + // rides the plan object — core.ts reads it from config at unpack time; the plan + // carries only a presence marker. Fail-closed without a key. + if (backend === "exa") { + if (!cfg.exaApiKey) return undefined; + return { + backend: "exa", + exaConfigured: true, + hostedTool: parsed._webSearch, + settings: { model: cfg.model ?? DEFAULT_SIDECAR_MODEL, reasoning, timeoutMs, describeImages }, + maxSearches, + routedModelStallTimeoutMs, + stallTimeoutSec, + streamRoutedModelOutput, + }; + } + // OpenAI backend: needs a ChatGPT login (main) and a forward provider to reach server-side web_search. if (!openAiSidecar) return undefined; return { diff --git a/src/web-search/loop.ts b/src/web-search/loop.ts index d02dc63a5a..19cb81563c 100644 --- a/src/web-search/loop.ts +++ b/src/web-search/loop.ts @@ -8,6 +8,7 @@ import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type Si import { runAnthropicWebSearch } from "./anthropic-executor"; import { runXaiWebSearch, type XaiSearchOptions } from "./xai-executor"; import { runGeminiWebSearch } from "./gemini-executor"; +import { runExaWebSearch } from "./exa-executor"; import type { WebSearchBackendId } from "./index"; import { clearableDeadline } from "../lib/abort"; import { redactSecretString } from "../lib/redact"; @@ -267,6 +268,8 @@ export interface WebSearchLoopDeps { xaiSidecar?: { providerName: string; provider: OcxProviderConfig }; /** Required for the gemini backend: the stored Antigravity CCA provider (L8). */ geminiSidecar?: { providerName: string; provider: OcxProviderConfig }; + /** Required for the exa backend: the operator key, read from config at plan unpack (L9). */ + exaApiKey?: string; /** Opt-in x_search options for the xai backend. */ xaiSearchOptions?: XaiSearchOptions; hostedTool: Record; @@ -685,6 +688,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise = {}): OcxConfig { + return { port: 10100, defaultProvider: "routed", providers: { routed }, ...overrides }; +} +function parsedWithWebSearch() { + return parseRequest({ model: "routed/model", input: "search", stream: true, tools: [{ type: "web_search" }] }); +} + +describe("mapExaSearchResponse (002 probe shape)", () => { + test("results -> digest text + deduped sources with titles/dates", () => { + const out = mapExaSearchResponse({ requestId: "r1", results: [ + { title: "Bun ships 1.4", url: "https://bun.sh/blog", publishedDate: "2026-08-01T00:00:00Z", text: "Bun 1.4 released with..." }, + { title: "dup", url: "https://bun.sh/blog" }, + { url: "https://example.com/no-title" }, + ], costDollars: { total: 0.007 } }); + expect(out.text).toContain("Bun ships 1.4 (2026-08-01): Bun 1.4 released with..."); + expect(out.text).toContain("(no excerpt)"); + expect(out.sources).toEqual([ + { url: "https://bun.sh/blog", title: "Bun ships 1.4" }, + { url: "https://example.com/no-title" }, + ]); + }); + + test("shapeless/empty bodies -> error outcome", () => { + expect(mapExaSearchResponse(null).error).toBeDefined(); + expect(mapExaSearchResponse({ results: [] }).error).toContain("no results"); + }); +}); + +describe("runExaWebSearch key hygiene (canary)", () => { + test("the key never reaches the outcome even when upstream echoes it in an error", async () => { + const realFetch = globalThis.fetch; + globalThis.fetch = (async () => new Response("invalid key exa-canary-9876543210 rejected", { status: 401 })) as typeof fetch; + try { + const out = await runExaWebSearch("q", "exa-canary-9876543210", { model: "m", reasoning: "low", timeoutMs: 5000, describeImages: false }); + expect(out.error).toContain("401"); + expect(JSON.stringify(out)).not.toContain("exa-canary"); + } finally { + globalThis.fetch = realFetch; + } + }); + + test("request carries x-api-key, manual redirect, pinned url; empty key fails closed with no fetch", async () => { + const captured: Array<{ url: string; init: RequestInit }> = []; + const realFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + captured.push({ url: String(input), init: init ?? {} }); + return new Response(JSON.stringify({ results: [{ title: "t", url: "https://e.com" }] }), { status: 200 }); + }) as typeof fetch; + try { + const none = await runExaWebSearch("q", "", { model: "m", reasoning: "low", timeoutMs: 5000, describeImages: false }); + expect(none.error).toContain("without an exaApiKey"); + expect(captured).toHaveLength(0); + const ok = await runExaWebSearch("q", "key-1", { model: "m", reasoning: "low", timeoutMs: 5000, describeImages: false }); + expect(ok.sources).toHaveLength(1); + expect(captured).toHaveLength(1); + expect(captured[0]!.url).toBe("https://api.exa.ai/search"); + expect(captured[0]!.init.redirect).toBe("manual"); + expect((captured[0]!.init.headers as Record)["x-api-key"]).toBe("key-1"); + } finally { + globalThis.fetch = realFetch; + } + }); +}); + +describe("planWebSearch exa arm (L9)", () => { + test("explicit exa + key -> plan with presence marker only (key absent from the plan)", () => { + const cfg = config({ webSearchSidecar: { backend: "exa", exaApiKey: "exa-secret-key-123" } }); + const plan = planWebSearch(cfg, parsedWithWebSearch(), false, routed, "model", undefined); + expect(plan?.backend).toBe("exa"); + expect(plan?.exaConfigured).toBe(true); + expect(JSON.stringify(plan)).not.toContain("exa-secret-key"); + }); + + test("explicit exa without a key fails closed", () => { + const cfg = config({ webSearchSidecar: { backend: "exa" } }); + expect(planWebSearch(cfg, parsedWithWebSearch(), false, routed, "model", undefined)).toBeUndefined(); + }); +}); + +describe("loop dispatch: exa arm fails closed without a key (L9)", () => { + test("missing exaApiKey yields the invariant error; no fetch, no pool recording", async () => { + const firstPass: AdapterEvent[] = [ + { type: "tool_call_start", id: "ws1", name: "web_search" }, + { type: "tool_call_delta", arguments: "{\"query\":\"docs\"}" }, + { type: "tool_call_end" }, + { type: "done" }, + ]; + let pass = 0; + let sawToolResult = ""; + const adapter: ProviderAdapter = { + name: "two-pass", + buildRequest: (parsed) => { + if (pass > 0) sawToolResult = JSON.stringify(parsed.context.messages ?? parsed); + return { url: "https://routed.test/v1", method: "POST", headers: {}, body: "{}" }; + }, + fetchResponse: async () => new Response("wire", { status: 200 }), + async *parseStream() { + const events = pass++ === 0 ? firstPass : [{ type: "text_delta", text: "answer" } as AdapterEvent, { type: "done" } as AdapterEvent]; + for (const event of events) yield event; + }, + async parseResponse() { throw new Error("unreachable"); }, + }; + const fetches: string[] = []; + const realFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL) => { fetches.push(String(input)); return new Response("{}", { status: 500 }); }) as typeof fetch; + let poolRecorded = 0; + try { + const response = await runWithWebSearch({ + parsed: parseRequest({ model: "routed/model", input: "hi", stream: true, tools: [{ type: "web_search" }] }), + adapter, + backend: "exa", + hostedTool: { type: "web_search" }, + selectedForwardHeaders: new Headers({ authorization: "Bearer forward-secret" }), + settings: { model: "m", reasoning: "low", timeoutMs: 5000, describeImages: false }, + maxSearches: 1, + recordSidecarOutcome: () => { poolRecorded += 1; }, + incomingMeta: { headers: new Headers(), translatorBudget: createTestTranslatorBudget() }, + } satisfies WebSearchLoopDeps); + await new Response(response.body).text(); + expect(fetches).toEqual([]); + expect(poolRecorded).toBe(0); + expect(sawToolResult).toContain("without an exaApiKey"); + } finally { + globalThis.fetch = realFetch; + } + }); +}); From 83f375b96cc4fb023543180a292dcf0c4cde16f5 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 21 Aug 2026 11:32:39 +0900 Subject: [PATCH 2/6] fix(web-search): scrub the exa key before truncating error bodies 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. --- src/web-search/exa-executor.ts | 4 +++- tests/exa-web-search.test.ts | 30 +++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/web-search/exa-executor.ts b/src/web-search/exa-executor.ts index c93d0b612b..0f6b0f6b79 100644 --- a/src/web-search/exa-executor.ts +++ b/src/web-search/exa-executor.ts @@ -49,7 +49,9 @@ export async function runExaWebSearch( ); if (!res.ok) { const t = await res.text().catch(() => ""); - return { text: "", sources: [], error: `exa sidecar HTTP ${res.status}: ${scrub(t.slice(0, 200))}` }; + // Scrub BEFORE truncating: slicing first can cut the literal key at the + // boundary, leaving an unscrubbable key prefix in the surviving text. + return { text: "", sources: [], error: `exa sidecar HTTP ${res.status}: ${scrub(t).slice(0, 200)}` }; } const payload = await res.json().catch(() => null); return mapExaSearchResponse(payload); diff --git a/tests/exa-web-search.test.ts b/tests/exa-web-search.test.ts index 72b7b8f57f..a47aaa442d 100644 --- a/tests/exa-web-search.test.ts +++ b/tests/exa-web-search.test.ts @@ -40,13 +40,41 @@ describe("runExaWebSearch key hygiene (canary)", () => { test("the key never reaches the outcome even when upstream echoes it in an error", async () => { const realFetch = globalThis.fetch; globalThis.fetch = (async () => new Response("invalid key exa-canary-9876543210 rejected", { status: 401 })) as typeof fetch; - try { + try { const out = await runExaWebSearch("q", "exa-canary-9876543210", { model: "m", reasoning: "low", timeoutMs: 5000, describeImages: false }); expect(out.error).toContain("401"); expect(JSON.stringify(out)).not.toContain("exa-canary"); } finally { globalThis.fetch = realFetch; } + }); + + test("a key straddling the 200-char truncation boundary never leaks a prefix", async () => { + // Position the key so any post-truncation scrub would leave a literal prefix. + const key = "exa-canary-boundary-9876543210"; + const body = "x".repeat(195) + key + " rejected"; + const realFetch = globalThis.fetch; + globalThis.fetch = (async () => new Response(body, { status: 401 })) as typeof fetch; + try { + const out = await runExaWebSearch("q", key, { model: "m", reasoning: "low", timeoutMs: 5000, describeImages: false }); + expect(out.error).toContain("401"); + expect(JSON.stringify(out)).not.toContain("exa-canary"); + } finally { + globalThis.fetch = realFetch; + } + }); + + test("a fetch rejection carrying the key in its message is scrubbed", async () => { + const key = "exa-canary-reject-9876543210"; + const realFetch = globalThis.fetch; + globalThis.fetch = (async () => { throw new Error(`connect refused for x-api-key ${key}`); }) as typeof fetch; + try { + const out = await runExaWebSearch("q", key, { model: "m", reasoning: "low", timeoutMs: 5000, describeImages: false }); + expect(out.error).toBeDefined(); + expect(JSON.stringify(out)).not.toContain("exa-canary"); + } finally { + globalThis.fetch = realFetch; + } }); test("request carries x-api-key, manual redirect, pinned url; empty key fails closed with no fetch", async () => { From 12763a63545e65bf9b5f6b5cd3eec37176f8a7fd Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 21 Aug 2026 11:50:56 +0900 Subject: [PATCH 3/6] docs(devlog): integration merge-train roadmap 100-150 (chat default, 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. --- .../100_chat_default_regression.md | 56 +++++++++++++++++++ .../110_global_merge_order.md | 50 +++++++++++++++++ .../120_sidecar_chain_merge.md | 32 +++++++++++ .../130_xai_responses_optin_switch.md | 26 +++++++++ .../140_release_prep.md | 7 +++ .../150_lidge_final_gate.md | 14 +++++ 6 files changed, 185 insertions(+) create mode 100644 devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md create mode 100644 devlog/_plan/260820_sidecar_selection_unification/110_global_merge_order.md create mode 100644 devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md create mode 100644 devlog/_plan/260820_sidecar_selection_unification/130_xai_responses_optin_switch.md create mode 100644 devlog/_plan/260820_sidecar_selection_unification/140_release_prep.md create mode 100644 devlog/_plan/260820_sidecar_selection_unification/150_lidge_final_gate.md diff --git a/devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md b/devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md new file mode 100644 index 0000000000..40881914a7 --- /dev/null +++ b/devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md @@ -0,0 +1,56 @@ +# 100 — Chat-default regression for Grok 4.5/4.6 (#2227 integration unit) + +Audited: 3-round adversarial plan audit (sol-medium), round-3 PASS. Amendment of this +unit's roadmap for the integration merge-train; consumes user directives from 260821. + +## Decision + +Third-party Responses APIs are supported only as first-class surfaces. Outside OpenAI, +the default wire is `openai-chat`. xAI's own CLI defaults to Chat; its Responses dialect +rejects opaque reasoning continuation/compaction state on later turns (#2240 regression +axis, 4-layer sanitize chase in #2217). Chat translation structurally filters private +extensions instead of chasing them. + +- ADOPT #2227: `modelWireDefaults` for grok-4.6/grok-4.5 flip `openai-responses` -> + `openai-chat` (src/providers/registry.ts:1032/1038 area), structure/04 rewrite, test + conversions to explicit `modelAdapters` opt-in framing. +- DeepSeek is OUT OF SCOPE (user decision 260821): deepseek-v4-flash/pro keep their + Responses defaults (registry.ts:1563-1564). Add a focused non-regression test locking + both V4 entries to `openai-responses` so this train cannot drift them. +- The Responses implementation is NOT deleted; it becomes the opt-in lane (doc 130). + +## Atomic merge unit (audit blocker R2-B1) + +The #2227 flip and the service_tier policy fix land as ONE merge unit — no intermediate +dev head may exist where OAuth opt-in Responses leaks caller service_tier: + +1. Cherry-pick/merge #2227's registry + structure/04 + test changes onto the post-stack + dev head (anchor: doc 110 global order). +2. In the same unit, fix the fastwire.ts:151 bypass: configured `modelAdapters` must not + skip the registry OAuth policy. Per audit round-3 note: make the OAuth registry tier + policy UNCONDITIONAL for the matching xAI route rather than introducing a new config + field — `modelAdapters` values are wire ids only. +3. Regression matrix locked in tests (5 rows): + | route | expectation | + |---|---| + | OAuth default | chat wire | + | OAuth explicit Responses (modelAdapters) | responses wire, caller service_tier dropped | + | API-key default | unchanged vs current dev | + | API-key explicit Responses | unchanged vs current dev (no Fast policy — #2072 deferred) | + | DeepSeek V4 flash/pro | responses default unchanged | + +## Reasoning-streaming proof (#1886 origin) + +#1886 moved grok to Responses because Chat translation showed a blank screen during long +reasoning turns. The regression must prove the Chat path now streams reasoning as an E2E +SSE assertion, not unit-only: an early upstream `reasoning_content` delta must be +observed on Codex's reasoning-summary SSE channel BEFORE the completion is released. +Test shape: mock xAI chat stream emitting reasoning_content deltas first; assert the +bridged Responses SSE emits reasoning summary deltas before `response.completed`. +Follow with one live probe through the running proxy. + +## Out of scope + +- #2072 API-key Fast/priority policy: DEFERRED (open, conflicting, its own review + cycle). The opt-in switch spec (doc 130) does not depend on it. +- DeepSeek wire changes: none. diff --git a/devlog/_plan/260820_sidecar_selection_unification/110_global_merge_order.md b/devlog/_plan/260820_sidecar_selection_unification/110_global_merge_order.md new file mode 100644 index 0000000000..6aedf6d2ab --- /dev/null +++ b/devlog/_plan/260820_sidecar_selection_unification/110_global_merge_order.md @@ -0,0 +1,50 @@ +# 110 — Global cross-train triage matrix and merge order + +One global order (audit R1-B5): the sidecar stack lands FIRST (9 stacked branches; +cascading a rebase through them is the expensive direction), then the lighter triage +PRs rebase onto the post-stack dev head one at a time. Every individual merge requires +maintainer approval + required CI green (MAINTAINERS.md) — the user's 'CI as lagging +indicator' applies only to repair iterations between pushes, never to the merge click +itself (audit R1-B4). + +## Global order + +1. Sidecar stack bottom-up: #2203 -> #2204 -> #2206 -> #2209 -> #2211 -> #2238 -> + #2242 -> #2243 -> #2245 (details + blocker inventory: doc 120). +2. #2227 integration unit (Chat default + unconditional OAuth tier policy, doc 100). +3. #2217 RESHAPED (not raw): rebase onto post-#2227 dev, reframe tests/docs as opt-in + hardening, gate the compat rewrite to the opt-in Responses route. +4. Doc-130 atomic opt-in switch (wp11). +5. Remaining Responses fixes: #2237, #2229, #2228 (do not depend on the Responses + default; still valuable for the opt-in lane and other Responses routes). +6. luvs01 / Ingwannu / docs PRs (matrix below). +7. Release prep (doc 140) -> lidge final gate (doc 150). + +## Triage matrix (dispositions) + +| PR | author | disposition | rationale | +|---|---|---|---| +| #2227 | olddonkey | MERGE (as doc-100 atomic unit) | owns the chat default | +| #2217 | olddonkey | RESHAPE then merge | opt-in-lane hardening; raw form encodes Responses-as-default | +| #2237 | olddonkey | MERGE after #2227 | null reasoning channel drop; wire-agnostic | +| #2229 | olddonkey | MERGE after #2227 | encrypted_content reshape guard; opt-in lane | +| #2228 | olddonkey | MERGE after #2227 | compaction blob provenance; wire-agnostic | +| #2214 | luvs01 | MERGE (address CHANGES_REQUESTED) | continuation binding bug | +| #2236 | luvs01 | MERGE (address CHANGES_REQUESTED) | catalog comment preservation | +| #2226 | luvs01 | MERGE after hygiene unblocked | secret redaction in events | +| #2196 | Ingwannu | MERGE | maintainer chore, privacy-bounded diagnostics | +| #2207 | Ingwannu | MERGE | google tool-result adjacency bug | +| #2202 | Ingwannu | MERGE | claude roster sync bug | +| #2181 | lidge-jun | MERGE (address CHANGES_REQUESTED) | devlog docs | +| #2168 | lidge-jun | MERGE (address CHANGES_REQUESTED) | devlog docs | +| #2235 | umyunsang | REVIEW-ONLY this train | contributor draft gate owns it | +| #2220 | Hylouis233 | REVIEW-ONLY this train | draft, capability sync | +| #2230 | ppvia | OUT (hygiene-blocked draft) | own cycle | +| #2222 | MarcTCruz | OUT (hygiene-blocked draft) | own cycle | +| #2216 | leon80900 | CLOSE-DIRECT (wrong branch, targets main) | ask re-file onto dev | +| #2215 | parkjs101 | OUT (docs draft, changes requested) | own cycle | +| #2213 | louis-tepe | OUT (draft, overlaps doc-130 design) | revisit post-switch | +| #2072 | olddonkey | DEFERRED (audit R2-B2) | Fast policy composes later; must re-verify against both wires | + +Rebase anchors are named at execution time in each phase's B (exact dev head SHA), +with cascade verification (typecheck + focused suites) after every land. diff --git a/devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md b/devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md new file mode 100644 index 0000000000..1d70b8267e --- /dev/null +++ b/devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md @@ -0,0 +1,32 @@ +# 120 — Sidecar chain merge execution (L1-L9 into dev) + +Order: #2203 -> #2204 -> #2206 -> #2209 -> #2211 -> #2238 -> #2242 -> #2243 -> #2245. +Each merge: resolve CHANGES_REQUESTED, obtain maintainer approval, required CI green, +squash-merge into its base, retarget the next child, cascade-verify (typecheck + +focused suites), then proceed. Mid-stream lidge suites are lagging indicators between +pushes; the merge click itself is gated (MAINTAINERS.md). + +## Current blocker inventory (fresh, 260821) + +- #2203 (L1, CHANGES_REQUESTED Ingwannu): blocker is the tracked cleanup doc + 000_wp0_branch_worktree_cleanup.md — contradictory KEEP/REMOVE entries, no preflight, + incomplete protected set, b2ac2500c preservation, codex/merge-loop-closeout listed + both ways. Fix: rewrite the doc as a non-executable historical record (all deletions + already executed in wp0) with a mechanical protected-set preflight template; or mark + every command block as executed-snapshot. No runtime code change. +- #2204, #2206 (L2, L3): APPROVED. Rebase-carry only. +- #2209 (L4, CHANGES_REQUESTED): runtime blocker — webSearchModelOptionsFrom drops + backend provenance; auth-slot model can persist {backend:'openai', + model:'claude-haiku-4-5'}. Fix: return (backend, model) pairs, validate the pair in + both PUT routes, teach sidecarBackendForModel the auth-slot rows. +- #2211 (L5, CHANGES_REQUESTED): carry the L4 provenance field through the CLI + contract — show backend in `web --list` human output and validate pairs on write. +- #2238 (L6, CHANGES_REQUESTED): re-read latest review; fold with the L4 contract. +- #2242 (L7, CHANGES_REQUESTED): re-read latest review; exact-origin pinning already + fixed; remaining items to fold. +- #2243 (L8, CHANGES_REQUESTED): three runtime blockers per review + red macOS CI + shard — full RCA in the phase B, fixes + rerun. +- #2245 (L9): reviewer PASS locally; needs maintainer approval; one failing test shard + reported on CI — reproduce, fix, re-push. + +Execution phase: wp9 (with wp8 covering the triage PRs per doc 110 order). diff --git a/devlog/_plan/260820_sidecar_selection_unification/130_xai_responses_optin_switch.md b/devlog/_plan/260820_sidecar_selection_unification/130_xai_responses_optin_switch.md new file mode 100644 index 0000000000..8d865bc747 --- /dev/null +++ b/devlog/_plan/260820_sidecar_selection_unification/130_xai_responses_optin_switch.md @@ -0,0 +1,26 @@ +# 130 — xAI Responses opt-in switch (atomic persistence + GUI) + +The Responses implementation survives as an explicit opt-in lane, like the x_search +opt-in block. Default: chat (post-#2227). Surface: xai is ONE provider id +(registry.ts:1000); the 'two pages' are the auth-mode-scoped sections (OAuth account + +API key) of the same provider workspace. + +## Contract (audit R1-B2) + +- Config truth: `modelAdapters` entries for grok-4.5 + grok-4.6 -> 'openai-responses'. +- Atomic management API: extend the provider PATCH surface (provider-routes.ts:378 area + + gui provider-workspace DTO types.ts:88) with a single `xaiResponsesOptIn: boolean` + virtual field. Server-side it sets/clears BOTH grok model entries in one config + transaction, preserves unrelated modelAdapters overrides, normalizes partial/mixed + pre-existing state (one model set, one not -> switch reads 'mixed' and the first + write normalizes), and echoes the effective state in the response DTO. +- GUI: one switch rendered in both auth-mode sections; mixed state shows indeterminate. +- Tier policy: the doc-100 unit already made the OAuth registry tier policy + unconditional; the switch adds NO tier behavior. API-key route: opt-in flips wire + only; everything else keeps current dev semantics (#2072 deferred). +- #2217 sanitize layers arm only on this opt-in Responses route (RESHAPE disposition). +- Tests: atomic set/clear, override preservation, mixed-state normalization, effective- + state echo, GUI switch render + PATCH round-trip, opt-in wire selection E2E. +- Docs-site: provider page gains the switch row; structure/04 notes the opt-in lane. + +Execution phase: wp11 (after #2227 unit + reshaped #2217; before release prep). diff --git a/devlog/_plan/260820_sidecar_selection_unification/140_release_prep.md b/devlog/_plan/260820_sidecar_selection_unification/140_release_prep.md new file mode 100644 index 0000000000..590e681966 --- /dev/null +++ b/devlog/_plan/260820_sidecar_selection_unification/140_release_prep.md @@ -0,0 +1,7 @@ +# 140 — dev release prep + +After wp11 lands. Version bump per release train conventions; release notes cover: +sidecar unification (L1-L9), chat-default regression + opt-in switch, responses fixes +(#2237/#2229/#2228, reshaped #2217), maintainer fixes (#2196/#2207/#2202), luvs01 +fixes (#2214/#2236/#2226), devlog docs (#2181/#2168). scripts/release.ts is the +authority; no release action without the doc-150 gate green. diff --git a/devlog/_plan/260820_sidecar_selection_unification/150_lidge_final_gate.md b/devlog/_plan/260820_sidecar_selection_unification/150_lidge_final_gate.md new file mode 100644 index 0000000000..79080850c4 --- /dev/null +++ b/devlog/_plan/260820_sidecar_selection_unification/150_lidge_final_gate.md @@ -0,0 +1,14 @@ +# 150 — lidge final validation gate (blocking) + +The one BLOCKING CI/validation point of the train (user directive: mid-stream CI is a +lagging indicator; the final gate is not). At the final dev head: + +- lidge: OCX_TEST_NO_QUEUE=1 bun run test — full suite green (baseline 13808+/0 at L9). +- Local: bun run typecheck, bun run privacy:scan, lint:gui (if gui changed). +- GitHub Actions: final dev head green on Linux/Windows/macOS (Windows gate is a + standing release requirement). +- Live probes through the running proxy: OAuth chat default turn, opt-in Responses + turn (no caller service_tier upstream), x_search opt-in turn, exa sidecar turn, + reasoning-streaming E2E (doc 100 matrix). +- Release staged on lidge per release-train conventions; promotion remains + maintainer-controlled. From 6e964235c4f121feac48725eeda66e9b44dd4035 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 21 Aug 2026 11:53:39 +0900 Subject: [PATCH 4/6] docs(devlog): fold C-gate blockers into roadmap 100-150 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. --- .../100_chat_default_regression.md | 4 +-- .../120_sidecar_chain_merge.md | 28 ++++++++++++++++--- .../130_xai_responses_optin_switch.md | 11 ++++---- .../150_lidge_final_gate.md | 11 +++++--- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md b/devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md index 40881914a7..fe6af00f0c 100644 --- a/devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md +++ b/devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md @@ -35,8 +35,8 @@ dev head may exist where OAuth opt-in Responses leaks caller service_tier: |---|---| | OAuth default | chat wire | | OAuth explicit Responses (modelAdapters) | responses wire, caller service_tier dropped | - | API-key default | unchanged vs current dev | - | API-key explicit Responses | unchanged vs current dev (no Fast policy — #2072 deferred) | + | API-key default | chat wire; no tier injected; caller service_tier not forwarded unless a capability declares it | + | API-key explicit Responses (modelAdapters) | responses wire; absent tier stays absent; caller service_tier dropped (no forwarding capability on this route; #2072 deferred) | | DeepSeek V4 flash/pro | responses default unchanged | ## Reasoning-streaming proof (#1886 origin) diff --git a/devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md b/devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md index 1d70b8267e..394502ce80 100644 --- a/devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md +++ b/devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md @@ -21,12 +21,32 @@ pushes; the merge click itself is gated (MAINTAINERS.md). both PUT routes, teach sidecarBackendForModel the auth-slot rows. - #2211 (L5, CHANGES_REQUESTED): carry the L4 provenance field through the CLI contract — show backend in `web --list` human output and validate pairs on write. -- #2238 (L6, CHANGES_REQUESTED): re-read latest review; fold with the L4 contract. -- #2242 (L7, CHANGES_REQUESTED): re-read latest review; exact-origin pinning already - fixed; remaining items to fold. +- #2238 (L6, CHANGES_REQUESTED, head a05f23fa9) — three reviewed blockers: + 1. stripOpenAiOnlyWebSearchFields fires for every non-ChatGPT-forward Responses + provider; official OpenAI API-key traffic loses external_web_access / + search_context_size. Gate on xAI-specific provider identity/capability and add + a buildRequest regression proving OpenAI API-key tools retain both fields. + 2. English config reference + CLI help still advertise only the old backend pair; + document the xai/gemini/exa arms as explicit-only/inert; keep translations + consistent. + 3. exaApiKey only in SENSITIVE_KEY_PATTERN: add it to the shared colon/query/JSON + string-redaction grammar with all three canaries in tests/redact.test.ts. +- #2242 (L7, CHANGES_REQUESTED, head 0f2d670c0) — five reviewed blockers: + 1. runXaiWebSearch misses cancelBodyOnAbort after fetchWithResetRetry resolves + (abort-before-reader race). + 2. parseXaiResponsesSSE must cancel the upstream body at the byte bound, not just + release the reader lock. + 3. the management PUT mutates config.webSearchSidecar before xSearch validation + (400 after live state change) — stage and validate the complete candidate first. + 4. malformed xSearch fields are silently omitted — reject invalid handle arrays, + dates, and enabled values instead of broadening the search with a 200. + 5. public docs + the type comment still call xai inert; update the English source + and translations. Add no-partial-mutation and oversized-stream regressions. - #2243 (L8, CHANGES_REQUESTED): three runtime blockers per review + red macOS CI shard — full RCA in the phase B, fixes + rerun. - #2245 (L9): reviewer PASS locally; needs maintainer approval; one failing test shard reported on CI — reproduce, fix, re-push. -Execution phase: wp9 (with wp8 covering the triage PRs per doc 110 order). +Execution sequence (explicit — the wp numbers are not the order): wp9 (this doc, +sidecar chain) -> wp8 (triage PRs per doc 110) -> wp11 (doc 130 switch) -> wp10 +(docs 140/150). diff --git a/devlog/_plan/260820_sidecar_selection_unification/130_xai_responses_optin_switch.md b/devlog/_plan/260820_sidecar_selection_unification/130_xai_responses_optin_switch.md index 8d865bc747..e690ede50d 100644 --- a/devlog/_plan/260820_sidecar_selection_unification/130_xai_responses_optin_switch.md +++ b/devlog/_plan/260820_sidecar_selection_unification/130_xai_responses_optin_switch.md @@ -9,11 +9,12 @@ API key) of the same provider workspace. - Config truth: `modelAdapters` entries for grok-4.5 + grok-4.6 -> 'openai-responses'. - Atomic management API: extend the provider PATCH surface (provider-routes.ts:378 area - + gui provider-workspace DTO types.ts:88) with a single `xaiResponsesOptIn: boolean` - virtual field. Server-side it sets/clears BOTH grok model entries in one config - transaction, preserves unrelated modelAdapters overrides, normalizes partial/mixed - pre-existing state (one model set, one not -> switch reads 'mixed' and the first - write normalizes), and echoes the effective state in the response DTO. + + gui provider-workspace DTO types.ts:88) with a split write/read contract: + - WRITE (PATCH input): `xaiResponsesOptIn: boolean` — sets/clears BOTH grok model + entries in one config transaction, preserving unrelated modelAdapters overrides. + - READ (GET/echo DTO): `xaiResponsesOptInState: true | false | "mixed"` — partial + pre-existing state (one model set, one not) reads "mixed"; the first boolean + write normalizes both entries and the echo returns the effective state. - GUI: one switch rendered in both auth-mode sections; mixed state shows indeterminate. - Tier policy: the doc-100 unit already made the OAuth registry tier policy unconditional; the switch adds NO tier behavior. API-key route: opt-in flips wire diff --git a/devlog/_plan/260820_sidecar_selection_unification/150_lidge_final_gate.md b/devlog/_plan/260820_sidecar_selection_unification/150_lidge_final_gate.md index 79080850c4..f1e2666fdf 100644 --- a/devlog/_plan/260820_sidecar_selection_unification/150_lidge_final_gate.md +++ b/devlog/_plan/260820_sidecar_selection_unification/150_lidge_final_gate.md @@ -1,10 +1,13 @@ -# 150 — lidge final validation gate (blocking) +# 150 — lidge final aggregate/release gate -The one BLOCKING CI/validation point of the train (user directive: mid-stream CI is a -lagging indicator; the final gate is not). At the final dev head: +The final AGGREGATE gate before release. It does not replace per-merge gating: docs +110/120 keep approval + required CI blocking for every individual merge; 'lagging +indicator' covers only repair iterations between pushes. At the final dev head: - lidge: OCX_TEST_NO_QUEUE=1 bun run test — full suite green (baseline 13808+/0 at L9). -- Local: bun run typecheck, bun run privacy:scan, lint:gui (if gui changed). +- Local: bun run typecheck, bun run privacy:scan, bun run lint:gui, lint:i18n, GUI + tests, bun run build:gui, and the docs-site build (doc 130 touches GUI and + localized copy, so the full GUI/i18n/docs chain is in the gate). - GitHub Actions: final dev head green on Linux/Windows/macOS (Windows gate is a standing release requirement). - Live probes through the running proxy: OAuth chat default turn, opt-in Responses From c53ff7cb6c22705a3cfc009ee4ca2c236881e7c5 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 21 Aug 2026 11:54:49 +0900 Subject: [PATCH 5/6] docs(devlog): doc 100 API-key opt-in row preserves current tier forwarding 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. --- .../100_chat_default_regression.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md b/devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md index fe6af00f0c..c73c63831a 100644 --- a/devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md +++ b/devlog/_plan/260820_sidecar_selection_unification/100_chat_default_regression.md @@ -36,7 +36,7 @@ dev head may exist where OAuth opt-in Responses leaks caller service_tier: | OAuth default | chat wire | | OAuth explicit Responses (modelAdapters) | responses wire, caller service_tier dropped | | API-key default | chat wire; no tier injected; caller service_tier not forwarded unless a capability declares it | - | API-key explicit Responses (modelAdapters) | responses wire; absent tier stays absent; caller service_tier dropped (no forwarding capability on this route; #2072 deferred) | + | API-key explicit Responses (modelAdapters) | responses wire; PRESERVE current dev semantics: absent tier stays absent, caller-supplied service_tier forwards verbatim (resolver proof: forwardCallerTier true on this route today; #2072 deferred) | | DeepSeek V4 flash/pro | responses default unchanged | ## Reasoning-streaming proof (#1886 origin) From 796f63c99944645b17c23dea8b88b81f359e86e9 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 21 Aug 2026 12:16:30 +0900 Subject: [PATCH 6/6] =?UTF-8?q?docs(devlog):=20wp9=20execution=20record=20?= =?UTF-8?q?=E2=80=94=20all=20six=20chain=20blockers=20resolved=20and=20pus?= =?UTF-8?q?hed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../120_sidecar_chain_merge.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md b/devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md index 394502ce80..cd15bcea61 100644 --- a/devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md +++ b/devlog/_plan/260820_sidecar_selection_unification/120_sidecar_chain_merge.md @@ -50,3 +50,31 @@ pushes; the merge click itself is gated (MAINTAINERS.md). Execution sequence (explicit — the wp numbers are not the order): wp9 (this doc, sidecar chain) -> wp8 (triage PRs per doc 110) -> wp11 (doc 130 switch) -> wp10 (docs 140/150). + +## Execution record (wp9 B-phase, 260821) + +Every recorded blocker resolved and pushed; Ingwannu re-review re-requested on all +six layers. Worker lanes ran in parallel worktrees under .tmp/ (four sol-medium +subagents + one direct fix): + +- #2203 d505dacc7 — cleanup doc recast as an executed historical record; corrected + 36-ref remote list (merge-loop-closeout excluded); 5-step preflight template. +- #2209 98eaba601 — options carry (backend, model) pairs; both PUT routes validate + effective pairs; auth-slot Anthropic persists anthropic/claude-haiku-4-5. + Suites 50/0 + 30/0, GUI 9/0, gate 13/0. +- #2211 84357bd2b (parent merge) + 2a610909f — backend-tagged `web --list`, + provenance-aware pair writes, clear rejection errors. CLI suites 401/0. +- #2238 19376f737 — strip gated on supportsOpenAiWebSearchToolFields:false (xAI + registry declares it; OpenAI API-key traffic keeps both fields — regression red + pre-fix); docs/CLI-help union across 8 locales; exaApiKey in the shared + colon/query/JSON redaction grammar, 3 canaries (JSON canary red pre-fix). +- #2242 b2c2054b5 — cancelBodyOnAbort after resolve; byte-bound upstream body + cancel; staged atomic PUT validation (no-partial-mutation); malformed xSearch + rejected with 400; docs/type-comment de-inerted. 47/0 + 12/0. +- #2243 249cc91a3 — atomic token/project snapshot; post-header abort guard; + bounded 64KiB UTF-8 JSON reads. 4 regressions red pre-fix; 50/0, privacy green. + +Remaining before merge clicks: Ingwannu approvals + green required CI per layer +(#2245's earlier shard failure not reproduced at the current head — checks green +except queued/pending reruns). Lidge full suite relaunched at stack top +(/tmp/ocx-gate-stack.log) as the lagging indicator.