From 16006bde4f97a23aaab701034514f5c3a26fcd80 Mon Sep 17 00:00:00 2001 From: DevonGithub <22842728+DevonGithub@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:41:44 +0530 Subject: [PATCH 01/11] fix(catalog): advertise Muse Spark image input on OpenCode Go --- src/providers/registry.ts | 5 +++ tests/opencode-go-muse-vision.test.ts | 50 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/opencode-go-muse-vision.test.ts diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 913ca82af1..30a0be6ff3 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1409,6 +1409,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ [OPENCODE_OX_ALPHA_FREE_MODEL]: ["text", "image"], // Experimental DeepSeek vision preview — expected to merge into deepseek-v4-flash later. [DEEPSEEK_VISION_PREVIEW_MODEL]: ["text", "image"], + // Muse Spark 1.2 Contributor is natively multimodal on Zen Go: it accepts input_image + // parts over /responses (probed 2026-08-26). Without this declaration the catalog + // advertises it text-only and the Codex app blocks image attachments client-side with + // "This model does not support image inputs" before the request ever reaches the proxy. + "muse-spark-1.2-contributor": ["text", "image"], }, modelReasoningEfforts: { "gpt-5.6-luna": OPENAI_API_GPT56_REASONING_EFFORTS, diff --git a/tests/opencode-go-muse-vision.test.ts b/tests/opencode-go-muse-vision.test.ts new file mode 100644 index 0000000000..71b2cf132e --- /dev/null +++ b/tests/opencode-go-muse-vision.test.ts @@ -0,0 +1,50 @@ +/** + * OpenCode Go Muse Spark 1.2 Contributor multimodal regression. + * + * Muse Spark answers /responses on Zen Go and accepts input_image parts, but the + * registry declared no modelInputModalities for it, so the Codex catalog advertised + * it text-only. The app gates image attachments client-side on input_modalities, + * so a text-only entry blocks images ("This model does not support image inputs") + * before the request ever reaches the proxy. These tests lock the declaration in + * and prove the catalog advertises image input for Muse. + */ +import { describe, expect, test } from "bun:test"; +import { applyProviderConfigHints } from "../src/codex/catalog"; +import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../src/providers/registry"; +import { providerConfigSeed } from "../src/providers/derive"; +import type { OcxProviderConfig } from "../src/types"; + +const MUSE_MODEL = "muse-spark-1.2-contributor"; + +function opencodeGo(): OcxProviderConfig { + const entry = getProviderRegistryEntry("opencode-go"); + if (!entry) throw new Error("missing opencode-go registry fixture"); + return { ...providerConfigSeed(entry), apiKey: "test-key" }; +} + +describe("OpenCode Go Muse Spark image input (#vision)", () => { + test("registry declares Muse as text+image", () => { + const entry = PROVIDER_REGISTRY.find(e => e.id === "opencode-go"); + expect(entry?.modelInputModalities?.[MUSE_MODEL]).toEqual(["text", "image"]); + }); + + test("the registry seed carries Muse as text+image", () => { + const prov = opencodeGo(); + expect(prov.modelInputModalities?.[MUSE_MODEL]).toEqual(["text", "image"]); + }); + + test("applyProviderConfigHints advertises image input for Muse", () => { + const prov = opencodeGo(); + const hinted = applyProviderConfigHints("opencode-go", prov, { + id: MUSE_MODEL, + provider: "opencode-go", + }); + expect(hinted.inputModalities).toEqual(["text", "image"]); + }); + + test("Muse is NOT in noVisionModels (it is natively multimodal, not sidecar-only)", () => { + const prov = opencodeGo(); + expect(prov.noVisionModels ?? []).not.toContain(MUSE_MODEL); + expect(prov.modelInputModalities?.[MUSE_MODEL]).toEqual(["text", "image"]); + }); +}); From e278a67797d000a158098ee059ab18c57081bcd1 Mon Sep 17 00:00:00 2001 From: DevonGithub <22842728+DevonGithub@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:00:20 +0530 Subject: [PATCH 02/11] test(muse): add docstring to helper --- tests/opencode-go-muse-vision.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/opencode-go-muse-vision.test.ts b/tests/opencode-go-muse-vision.test.ts index 71b2cf132e..f01c21a7f9 100644 --- a/tests/opencode-go-muse-vision.test.ts +++ b/tests/opencode-go-muse-vision.test.ts @@ -16,6 +16,7 @@ import type { OcxProviderConfig } from "../src/types"; const MUSE_MODEL = "muse-spark-1.2-contributor"; +/** Seeded OpenCode Go provider config for the Muse Spark vision assertions. */ function opencodeGo(): OcxProviderConfig { const entry = getProviderRegistryEntry("opencode-go"); if (!entry) throw new Error("missing opencode-go registry fixture"); From 50b50e1b9a35095ebf62fe214fef6691ab71275c Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Wed, 26 Aug 2026 08:43:09 +0000 Subject: [PATCH 03/11] fix(responses): normalize canonical forward prompt envelope --- .../src/content/docs/reference/adapters.md | 7 + .../content/docs/reference/proxy-formats.md | 3 + src/adapters/openai-responses.ts | 64 ++++++++++ src/compatibility/openai-responses.ts | 18 ++- .../openai-codex-forward-gpt56-sol-v1.json | 27 +++- .../responses-forward-prompt-envelope.test.ts | 120 ++++++++++++++++++ 6 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 tests/responses-forward-prompt-envelope.test.ts diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index b6e7c6006d..18179f1b6d 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -60,6 +60,13 @@ collision-safe public function tool. Matching request history and JSON/SSE funct translated back to the private `tool_search` lifecycle for the client. Canonical OpenAI forward keeps the native private type unchanged. +The canonical ChatGPT Codex forward destination also normalizes two public Responses shapes that +its stricter backend rejects: fully textual `system` messages inside `input` are appended to the +top-level `instructions` string in request order, and the top-level `truncation` field is removed. +This rewrite is destination-scoped. Key-auth public/custom Responses providers and noncanonical +forward gateways keep both fields unchanged; a multimodal system message is never partially folded +or silently dropped. + For `key` auth, [`retryOn429`](/reference/configuration/) applies here too: a pre-stream 429 waits and replays the identical request on the same key before any other handling, exactly like the translated `openai-chat` / Anthropic request path. Custom `runTurn` transports are not part diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md index 83dda745cf..094591b008 100644 --- a/docs-site/src/content/docs/reference/proxy-formats.md +++ b/docs-site/src/content/docs/reference/proxy-formats.md @@ -54,6 +54,9 @@ non-empty `model`. `input` may be a string or an array of Responses items. Unknown item types are accepted as loose typed items for forward compatibility. Translated adapters handle only the item types they recognize, and may reject a feature their provider cannot represent. +On the canonical ChatGPT Codex forward route, text-only `system` input messages are folded into +top-level `instructions`, and `truncation` is removed because that destination rejects both public +Responses shapes. Other Responses destinations preserve them. ### JSON and SSE output diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index c304e6823f..2294c083c5 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -1104,6 +1104,69 @@ function stripUnsupportedForwardParams(body: unknown): unknown { return rest; } +/** Return the lossless text represented by one system message, or null when it is multimodal. */ +function canonicalForwardSystemText(item: Record): string | null { + const content = item.content; + if (content === undefined) return ""; + if (typeof content === "string") return content; + if (!Array.isArray(content)) return null; + let text = ""; + for (const block of content) { + if (!isPlainObject(block)) return null; + if (block.type !== "input_text" && block.type !== "text") return null; + if (typeof block.text !== "string") return null; + text += block.text; + } + return text; +} + +/** + * The public Responses API accepts input system messages and `truncation`, but the canonical + * ChatGPT Codex forward endpoint rejects both. Fold only fully textual system messages into the + * existing top-level instructions and remove the unsupported flag at this destination boundary. + * + * The fold is atomic: if any system message contains a non-text block, keep every message in + * place so the proxy never silently drops multimodal content. The backend may still reject that + * unsupported shape, but it will not receive a partially rewritten prompt. + */ +function normalizeCanonicalForwardPromptEnvelope(body: unknown): unknown { + if (!isPlainObject(body)) return body; + const stripTruncation = Object.hasOwn(body, "truncation"); + const input = Array.isArray(body.input) ? body.input : undefined; + if (!input) { + if (!stripTruncation) return body; + const { truncation: _truncation, ...rest } = body; + return rest; + } + + const foldedText: string[] = []; + let sawSystemMessage = false; + let canFoldAllSystemMessages = true; + for (const item of input) { + if (!isPlainObject(item) || item.role !== "system") continue; + sawSystemMessage = true; + const text = canonicalForwardSystemText(item); + if (text === null) { + canFoldAllSystemMessages = false; + break; + } + foldedText.push(text); + } + if (!stripTruncation && (!sawSystemMessage || !canFoldAllSystemMessages)) return body; + + const next: Record = { ...body }; + if (stripTruncation) delete next.truncation; + if (sawSystemMessage && canFoldAllSystemMessages) { + next.input = input.filter(item => !isPlainObject(item) || item.role !== "system"); + const folded = foldedText.join("\n\n"); + if (folded !== "") { + const existing = typeof body.instructions === "string" ? body.instructions : ""; + next.instructions = existing !== "" ? `${existing}\n\n${folded}` : folded; + } + } + return next; +} + const IMAGE_GEN_NAMESPACE = "image_gen"; const HOSTED_IMAGE_GENERATION_TOOL = "image_generation"; const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`; @@ -1794,6 +1857,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): // third-party forward gateway may still accept it, so this must not be widened. if (isCanonicalOpenAiForwardProvider(provider)) { outBody = stripDeprecatedPromptCacheRetention(outBody, parsed.modelId); + outBody = normalizeCanonicalForwardPromptEnvelope(outBody); } } else { outBody = preferConfiguredHostedTools( diff --git a/src/compatibility/openai-responses.ts b/src/compatibility/openai-responses.ts index d3cde061bd..fd9393d028 100644 --- a/src/compatibility/openai-responses.ts +++ b/src/compatibility/openai-responses.ts @@ -9,7 +9,7 @@ const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1"; export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({ schemaVersion: 1, id: "openai.codex-forward.gpt-5-6-sol.responses", - version: "1.0.0", + version: "1.1.0", subject: { providerId: "openai", baseUrl: "https://chatgpt.com/backend-api/codex", @@ -69,6 +69,22 @@ export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManife limitation: "The field is removed before dispatch.", evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["metadata-removed"] }], }, + { + id: "system-input-messages", + feature: "request.input.system_messages", + disposition: "translated", + summary: "Text-only input system messages are folded into top-level instructions.", + limitation: "The destination does not accept system-role input items; multimodal system messages stay unchanged rather than being dropped.", + evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["system-message-folded", "system-message-removed"] }], + }, + { + id: "truncation", + feature: "request.truncation", + disposition: "unsupported", + summary: "The ChatGPT Codex forward route does not receive truncation.", + limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.", + evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["truncation-removed"] }], + }, { id: "prompt-cache-retention", feature: "request.prompt_cache_retention", diff --git a/tests/fixtures/compatibility/openai-codex-forward-gpt56-sol-v1.json b/tests/fixtures/compatibility/openai-codex-forward-gpt56-sol-v1.json index fab1f40292..7852a38bac 100644 --- a/tests/fixtures/compatibility/openai-codex-forward-gpt56-sol-v1.json +++ b/tests/fixtures/compatibility/openai-codex-forward-gpt56-sol-v1.json @@ -15,10 +15,23 @@ }, "request": { "model": "gpt-5.6-sol", - "input": "ping", + "input": [ + { + "type": "message", + "role": "system", + "content": [{ "type": "input_text", "text": "Fixture system instruction" }] + }, + { + "type": "message", + "role": "user", + "content": [{ "type": "input_text", "text": "ping" }] + } + ], + "instructions": "Fixture instructions", "previous_response_id": "resp_fixture", "stream": true, "store": false, + "truncation": "disabled", "max_output_tokens": 32000, "metadata": { "fixture": "not-forwarded" }, "reasoning": { "effort": "low" }, @@ -54,6 +67,18 @@ }, { "id": "max-output-tokens-removed", "operator": "absent", "path": "/body/max_output_tokens" }, { "id": "metadata-removed", "operator": "absent", "path": "/body/metadata" }, + { "id": "system-message-folded", "operator": "equals", "path": "/body/instructions", "expected": "Fixture instructions\n\nFixture system instruction" }, + { + "id": "system-message-removed", + "operator": "equals", + "path": "/body/input/0", + "expected": { + "type": "message", + "role": "user", + "content": [{ "type": "input_text", "text": "ping" }] + } + }, + { "id": "truncation-removed", "operator": "absent", "path": "/body/truncation" }, { "id": "previous-response-id-removed", "operator": "absent", "path": "/body/previous_response_id" }, { "id": "prompt-cache-key-preserved", "operator": "equals", "path": "/body/prompt_cache_key", "expected": "project-cache-v1" }, { "id": "prompt-cache-retention-removed", "operator": "absent", "path": "/body/prompt_cache_retention" } diff --git a/tests/responses-forward-prompt-envelope.test.ts b/tests/responses-forward-prompt-envelope.test.ts new file mode 100644 index 0000000000..7d52d45151 --- /dev/null +++ b/tests/responses-forward-prompt-envelope.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, test } from "bun:test"; +import { createResponsesPassthroughAdapter as createProductionAdapter } from "../src/adapters/openai-responses"; +import type { OcxProviderConfig } from "../src/types"; +import { withTestTranslatorBudget } from "./helpers/translator-budget"; + +const createAdapter = (provider: OcxProviderConfig) => + withTestTranslatorBudget(createProductionAdapter(provider)); + +const canonicalForward: OcxProviderConfig = { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", +}; + +function outboundBody(provider: OcxProviderConfig, rawBody: Record): Record { + const adapter = createAdapter(provider); + const request = adapter.buildRequest({ + modelId: String(rawBody.model ?? "gpt-5.6-luna"), + context: { messages: [] }, + stream: rawBody.stream === true, + options: {}, + _rawBody: rawBody, + }, { headers: new Headers({ authorization: "Bearer test-token" }) }); + try { + return JSON.parse(request.body) as Record; + } finally { + request.releaseBodyObservation?.(); + } +} + +describe("canonical ChatGPT forward prompt envelope", () => { + test("folds textual system messages after existing instructions and strips truncation", () => { + const functionCall = { + type: "function_call", + call_id: "call_keep", + name: "shell", + arguments: "{}", + }; + const body = outboundBody(canonicalForward, { + model: "gpt-5.6-luna", + instructions: "Existing instructions", + truncation: "disabled", + input: [ + { type: "message", role: "system", content: "First system instruction" }, + { type: "message", role: "user", content: [{ type: "input_text", text: "hello" }] }, + { + type: "message", + role: "system", + content: [ + { type: "input_text", text: "Second" }, + { type: "text", text: " system instruction" }, + ], + }, + functionCall, + ], + }); + + expect(body.truncation).toBeUndefined(); + expect(body.instructions).toBe( + "Existing instructions\n\nFirst system instruction\n\nSecond system instruction", + ); + expect(body.input).toEqual([ + { type: "message", role: "user", content: [{ type: "input_text", text: "hello" }] }, + functionCall, + ]); + }); + + test("keeps every system message when any one contains non-text content", () => { + const input = [ + { type: "message", role: "system", content: "text" }, + { + type: "message", + role: "system", + content: [{ type: "input_image", image_url: "data:image/png;base64,AA==" }], + }, + { type: "message", role: "user", content: "hello" }, + ]; + const body = outboundBody(canonicalForward, { + model: "gpt-5.6-luna", + truncation: "disabled", + input, + }); + + expect(body.truncation).toBeUndefined(); + expect(body.instructions).toBeUndefined(); + expect(body.input).toEqual(input); + }); + + test.each([ + { + name: "key-auth public Responses provider", + provider: { + adapter: "openai-responses", + baseUrl: "https://api.openai.com/v1", + authMode: "key" as const, + apiKey: "test-key", + }, + }, + { + name: "noncanonical forward gateway", + provider: { + adapter: "openai-responses", + baseUrl: "https://gateway.example/v1", + authMode: "forward" as const, + }, + }, + ])("preserves the public Responses envelope for $name", ({ provider }) => { + const input = [{ type: "message", role: "system", content: "keep me" }]; + const body = outboundBody(provider, { + model: "gpt-5.6-luna", + instructions: "existing", + truncation: "disabled", + input, + }); + + expect(body.truncation).toBe("disabled"); + expect(body.instructions).toBe("existing"); + expect(body.input).toEqual(input); + }); +}); From 140d0141d9221d9c654bb8297ed8c4e35e7a74fd Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Wed, 26 Aug 2026 09:06:46 +0000 Subject: [PATCH 04/11] fix(responses): normalize Posit continuations --- .../src/content/docs/reference/adapters.md | 5 + .../content/docs/reference/proxy-formats.md | 2 + src/adapters/openai-responses.ts | 75 ++++++++++ src/compatibility/openai-responses.ts | 18 ++- structure/11_compatibility-contracts.md | 21 +++ .../openai-codex-forward-gpt56-sol-v1.json | 12 +- ...sponses-forward-posit-continuation.test.ts | 134 ++++++++++++++++++ 7 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 tests/responses-forward-posit-continuation.test.ts diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 18179f1b6d..7149f8dc78 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -67,6 +67,11 @@ This rewrite is destination-scoped. Key-auth public/custom Responses providers a forward gateways keep both fields unchanged; a multimodal system message is never partially folded or silently dropped. +For canonical forward continuations, client-only `prompt_cache_breakpoint` properties are removed +recursively within bounded traversal limits. When `store: false`, `item_reference` rows are also +omitted because the destination cannot resolve an item it did not persist. Function/tool `call_id` +pairs and `reasoning.effort` are preserved. + For `key` auth, [`retryOn429`](/reference/configuration/) applies here too: a pre-stream 429 waits and replays the identical request on the same key before any other handling, exactly like the translated `openai-chat` / Anthropic request path. Custom `runTurn` transports are not part diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md index 094591b008..1a68e78436 100644 --- a/docs-site/src/content/docs/reference/proxy-formats.md +++ b/docs-site/src/content/docs/reference/proxy-formats.md @@ -57,6 +57,8 @@ handle only the item types they recognize, and may reject a feature their provid On the canonical ChatGPT Codex forward route, text-only `system` input messages are folded into top-level `instructions`, and `truncation` is removed because that destination rejects both public Responses shapes. Other Responses destinations preserve them. +The same canonical boundary removes nested client-only `prompt_cache_breakpoint` markers and drops +`item_reference` entries only on `store: false` continuations; tool call/result pairing is unchanged. ### JSON and SSE output diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 2294c083c5..305cc5d755 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -1167,6 +1167,80 @@ function normalizeCanonicalForwardPromptEnvelope(body: unknown): unknown { return next; } +const POSIT_CACHE_MARKER_MAX_DEPTH = 64; +const POSIT_CACHE_MARKER_MAX_NODES = 100_000; + +type PromptCacheMarkerRewrite = { + value: unknown; + changed: boolean; + complete: boolean; +}; + +/** + * Remove Posit/Anthropic-style prompt-cache markers without trusting request nesting. The walk + * aborts atomically when its depth or node budget is exceeded, so a hostile extension object can + * neither overflow the stack nor receive a partially rewritten subtree. + */ +function stripPromptCacheBreakpoints( + value: unknown, + state: { nodes: number }, + depth = 0, +): PromptCacheMarkerRewrite { + state.nodes += 1; + if (depth > POSIT_CACHE_MARKER_MAX_DEPTH || state.nodes > POSIT_CACHE_MARKER_MAX_NODES) { + return { value, changed: false, complete: false }; + } + if (Array.isArray(value)) { + let changed = false; + const next: unknown[] = []; + for (const entry of value) { + const rewritten = stripPromptCacheBreakpoints(entry, state, depth + 1); + if (!rewritten.complete) return { value, changed: false, complete: false }; + changed ||= rewritten.changed; + next.push(rewritten.value); + } + return { value: changed ? next : value, changed, complete: true }; + } + if (!isPlainObject(value)) return { value, changed: false, complete: true }; + + let changed = Object.hasOwn(value, "prompt_cache_breakpoint"); + const next: Record = {}; + for (const [key, entry] of Object.entries(value)) { + if (key === "prompt_cache_breakpoint") continue; + const rewritten = stripPromptCacheBreakpoints(entry, state, depth + 1); + if (!rewritten.complete) return { value, changed: false, complete: false }; + changed ||= rewritten.changed; + next[key] = rewritten.value; + } + return { value: changed ? next : value, changed, complete: true }; +} + +/** + * Posit Assistant can replay client-only cache markers and stored-item references on a + * `store: false` continuation. The canonical ChatGPT Codex backend rejects both. Remove the + * markers recursively and drop only `item_reference` rows that cannot name persisted state; + * ordinary item ids are handled later by stripItemIdsWhenUnstored and tool call_id pairs remain. + */ +function normalizeCanonicalForwardContinuationEnvelope(body: unknown): unknown { + if (!isPlainObject(body) || !Array.isArray(body.input)) return body; + let input: unknown[] = body.input; + let changed = false; + if (body.store === false) { + const withoutReferences = input.filter(item => !isPlainObject(item) || item.type !== "item_reference"); + if (withoutReferences.length !== input.length) { + input = withoutReferences; + changed = true; + } + } + + const markerRewrite = stripPromptCacheBreakpoints(input, { nodes: 0 }); + if (markerRewrite.complete && markerRewrite.changed) { + input = markerRewrite.value as unknown[]; + changed = true; + } + return changed ? { ...body, input } : body; +} + const IMAGE_GEN_NAMESPACE = "image_gen"; const HOSTED_IMAGE_GENERATION_TOOL = "image_generation"; const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`; @@ -1858,6 +1932,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): if (isCanonicalOpenAiForwardProvider(provider)) { outBody = stripDeprecatedPromptCacheRetention(outBody, parsed.modelId); outBody = normalizeCanonicalForwardPromptEnvelope(outBody); + outBody = normalizeCanonicalForwardContinuationEnvelope(outBody); } } else { outBody = preferConfiguredHostedTools( diff --git a/src/compatibility/openai-responses.ts b/src/compatibility/openai-responses.ts index fd9393d028..75384dfaf2 100644 --- a/src/compatibility/openai-responses.ts +++ b/src/compatibility/openai-responses.ts @@ -9,7 +9,7 @@ const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1"; export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({ schemaVersion: 1, id: "openai.codex-forward.gpt-5-6-sol.responses", - version: "1.1.0", + version: "1.2.0", subject: { providerId: "openai", baseUrl: "https://chatgpt.com/backend-api/codex", @@ -85,6 +85,22 @@ export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManife limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.", evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["truncation-removed"] }], }, + { + id: "prompt-cache-breakpoint", + feature: "request.prompt_cache_breakpoint", + disposition: "unsupported", + summary: "Client-only prompt cache breakpoint markers do not reach the forward backend.", + limitation: "Markers are removed recursively from input within bounded depth and node budgets.", + evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-breakpoint-removed"] }], + }, + { + id: "unstored-item-reference", + feature: "continuation.item_reference", + disposition: "degraded", + summary: "Unpersisted item references are omitted from store-false continuations.", + limitation: "The referenced item cannot exist at the backend when store is false; tool call_id pairs remain unchanged.", + evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["unstored-item-reference-removed"] }], + }, { id: "prompt-cache-retention", feature: "request.prompt_cache_retention", diff --git a/structure/11_compatibility-contracts.md b/structure/11_compatibility-contracts.md index 483825852a..d8be62e253 100644 --- a/structure/11_compatibility-contracts.md +++ b/structure/11_compatibility-contracts.md @@ -39,6 +39,27 @@ Compatibility manifests are passive data. The Responses request path, router, an not import them. A future `ocx compatibility explain` or GUI reader may load the catalog on demand, but adding a manifest must not activate Compatibility Lab or alter dispatch behavior. +## Canonical forward continuation extensions + +The canonical ChatGPT Codex forward boundary removes client-only +`prompt_cache_breakpoint` properties from `input` recursively. The traversal is bounded by depth +and node count; exceeding either bound leaves the marker-bearing input unchanged instead of +publishing a partially transformed continuation. When the request explicitly sets `store: false`, +top-level `item_reference` input rows are omitted because the destination cannot resolve state that +it did not persist. Function and tool-result `call_id` pairs and `reasoning.effort` remain intact. + +This is destination-scoped compatibility behavior. Key-auth public Responses providers and custom +forward gateways keep both extensions unchanged because their contracts may accept or interpret +them independently. + +[Decision Log] +- 목적과 의도: Preserve Posit Assistant tool continuation semantics while preventing canonical ChatGPT Codex forwarding from sending client-only cache markers or unresolvable stored-item references. +- 기존 구현 및 제약 조건: The existing `store: false` sanitizer removed item ids but left `item_reference` shells, and no bounded pass recognized markers nested inside content; tool `call_id` pairing and reasoning effort are continuation-critical. +- 검토한 주요 대안: Strip the extensions for every Responses destination; delete only reference ids; expand references from local state; or normalize only the canonical forward destination with bounded recursive marker removal. +- 선택한 방식: Apply the bounded marker pass only to canonical forward `input`, and omit `item_reference` rows only when `store` is exactly `false`. +- 다른 대안 대신 이 방식을 선택한 이유: Public and custom gateways may implement these extensions, while id-only deletion creates an invalid reference shell and local expansion would invent unavailable persistence authority. +- 장점, 단점 및 영향: Posit continuations retain tool pairing and reasoning controls without widening public-provider behavior; hostile nesting fails closed to the original input, so an over-limit request may still be rejected upstream rather than partially rewritten. + [Decision Log] - 목적과 의도: Make provider compatibility explicit and machine-readable before larger routing or Responses refactors. - 기존 구현 및 제약 조건: Adapter-wide conformance tests already protect tool translation, and Compatibility Lab owns broader protocol evidence, but neither publishes an exact provider/destination/auth/model claim table. Lab must remain outside the ordinary request import graph. diff --git a/tests/fixtures/compatibility/openai-codex-forward-gpt56-sol-v1.json b/tests/fixtures/compatibility/openai-codex-forward-gpt56-sol-v1.json index 7852a38bac..bc33f244e9 100644 --- a/tests/fixtures/compatibility/openai-codex-forward-gpt56-sol-v1.json +++ b/tests/fixtures/compatibility/openai-codex-forward-gpt56-sol-v1.json @@ -24,7 +24,15 @@ { "type": "message", "role": "user", - "content": [{ "type": "input_text", "text": "ping" }] + "content": [{ + "type": "input_text", + "text": "ping", + "prompt_cache_breakpoint": { "type": "ephemeral" } + }] + }, + { + "type": "item_reference", + "id": "rs_unpersisted" } ], "instructions": "Fixture instructions", @@ -79,6 +87,8 @@ } }, { "id": "truncation-removed", "operator": "absent", "path": "/body/truncation" }, + { "id": "prompt-cache-breakpoint-removed", "operator": "absent", "path": "/body/input/0/content/0/prompt_cache_breakpoint" }, + { "id": "unstored-item-reference-removed", "operator": "absent", "path": "/body/input/1" }, { "id": "previous-response-id-removed", "operator": "absent", "path": "/body/previous_response_id" }, { "id": "prompt-cache-key-preserved", "operator": "equals", "path": "/body/prompt_cache_key", "expected": "project-cache-v1" }, { "id": "prompt-cache-retention-removed", "operator": "absent", "path": "/body/prompt_cache_retention" } diff --git a/tests/responses-forward-posit-continuation.test.ts b/tests/responses-forward-posit-continuation.test.ts new file mode 100644 index 0000000000..59b7e71c11 --- /dev/null +++ b/tests/responses-forward-posit-continuation.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, test } from "bun:test"; +import { createResponsesPassthroughAdapter as createProductionAdapter } from "../src/adapters/openai-responses"; +import { parseRequest } from "../src/responses/parser"; +import type { OcxProviderConfig } from "../src/types"; +import { withTestTranslatorBudget } from "./helpers/translator-budget"; + +const createAdapter = (provider: OcxProviderConfig) => + withTestTranslatorBudget(createProductionAdapter(provider)); + +const canonicalForward: OcxProviderConfig = { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", +}; + +function outboundBody(provider: OcxProviderConfig, rawBody: Record): Record { + const request = createAdapter(provider).buildRequest( + parseRequest(structuredClone(rawBody)), + { headers: new Headers({ authorization: "Bearer test-token" }) }, + ); + try { + return JSON.parse(request.body) as Record; + } finally { + request.releaseBodyObservation?.(); + } +} + +function positContinuation(store: boolean): Record { + return { + model: "gpt-5.6-luna", + store, + stream: true, + reasoning: { effort: "high" }, + input: [ + { + type: "message", + role: "user", + content: [{ + type: "input_text", + text: "continue", + prompt_cache_breakpoint: { type: "ephemeral" }, + }], + }, + { + type: "item_reference", + id: "rs_unpersisted", + }, + { + type: "function_call", + id: "fc_pair", + call_id: "call_pair", + name: "list_files", + arguments: "{}", + }, + { + type: "function_call_output", + call_id: "call_pair", + output: [{ + type: "input_text", + text: "file.R", + metadata: { + nested: { + prompt_cache_breakpoint: true, + keep: "visible", + }, + }, + }], + }, + ], + }; +} + +describe("canonical ChatGPT forward Posit continuation normalization", () => { + test("removes cache markers and unstored references without changing call_id or reasoning", () => { + const body = outboundBody(canonicalForward, positContinuation(false)); + const input = body.input as Array>; + + expect(JSON.stringify(input)).not.toContain("prompt_cache_breakpoint"); + expect(input.some(item => item.type === "item_reference")).toBe(false); + expect(input.find(item => item.type === "function_call")?.call_id).toBe("call_pair"); + expect(input.find(item => item.type === "function_call_output")?.call_id).toBe("call_pair"); + expect(body.reasoning).toEqual({ effort: "high" }); + expect(JSON.stringify(input)).toContain('"keep":"visible"'); + }); + + test("keeps item_reference when storage is enabled but still removes client-only markers", () => { + const body = outboundBody(canonicalForward, positContinuation(true)); + const input = body.input as Array>; + + expect(input.find(item => item.type === "item_reference")).toEqual({ + type: "item_reference", + id: "rs_unpersisted", + }); + expect(JSON.stringify(input)).not.toContain("prompt_cache_breakpoint"); + }); + + test.each([ + { + name: "key-auth public Responses provider", + provider: { + adapter: "openai-responses", + baseUrl: "https://api.openai.com/v1", + authMode: "key" as const, + apiKey: "test-key", + }, + }, + { + name: "noncanonical forward gateway", + provider: { + adapter: "openai-responses", + baseUrl: "https://gateway.example/v1", + authMode: "forward" as const, + }, + }, + ])("preserves the continuation extensions for $name", ({ provider }) => { + const body = outboundBody(provider, positContinuation(false)); + const serialized = JSON.stringify(body.input); + + expect(serialized).toContain("prompt_cache_breakpoint"); + expect((body.input as Array>).some(item => item.type === "item_reference")).toBe(true); + }); + + test("an over-depth marker subtree is kept atomically instead of partially rewritten", () => { + let nested: Record = { prompt_cache_breakpoint: true, keep: "deep" }; + for (let depth = 0; depth < 70; depth += 1) nested = { nested }; + const body = outboundBody(canonicalForward, { + model: "gpt-5.6-luna", + input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi", nested }] }], + }); + + expect(JSON.stringify(body.input)).toContain("prompt_cache_breakpoint"); + expect(JSON.stringify(body.input)).toContain('"keep":"deep"'); + }); +}); From c0668828ee3ba198088154a1a3ea9101394f1d82 Mon Sep 17 00:00:00 2001 From: Michael Z Freeman Date: Wed, 26 Aug 2026 14:56:01 +0100 Subject: [PATCH 05/11] fix: normalize Azure Model Router tool schemas --- src/adapters/openai-chat.ts | 46 ++++++++++++++++-- tests/azure-model-router-tool-schema.test.ts | 51 ++++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 tests/azure-model-router-tool-schema.test.ts diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index c877a1b357..0acdf6a1b4 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -922,6 +922,37 @@ function shouldSanitizeZenToolParameters(provider: OcxProviderConfig): boolean { || baseUrl === "https://opencode.ai/zen/go/v1"; } +/** Azure Model Router (and Gemini-in-the-pool) 400s Codex MCP schemas whose root is a union. */ +const AZURE_CHAT_FORBIDDEN_ROOT_KEYS = ["oneOf", "anyOf", "allOf", "enum", "const", "not"] as const; + +function isAzureOpenAiChatTarget(provider: OcxProviderConfig): boolean { + try { + const host = new URL(provider.baseUrl).hostname.toLowerCase(); + return host.endsWith(".openai.azure.com") + || host.endsWith(".cognitiveservices.azure.com") + || host.endsWith(".services.ai.azure.com") + || host.endsWith(".ai.azure.com"); + } catch { + return false; + } +} + +/** + * Azure Foundry Model Router validates every function schema against the strictest model in + * the pool (Gemini-shaped): root must be {type:"object"} with no oneOf/anyOf/allOf/enum/ + * const/not. Codex App MCP tools such as mcp__codex_app__automation_update ship a root + * union, which 400s the whole turn. Flatten like Zen, then strip leftover forbidden keys. + */ +function sanitizeAzureChatToolParameters(parameters: unknown): Record { + const root = ensureZenRootObjectSchema(parameters); + for (const key of AZURE_CHAT_FORBIDDEN_ROOT_KEYS) delete root[key]; + root.type = "object"; + if (!root.properties || typeof root.properties !== "object" || Array.isArray(root.properties)) { + root.properties = {}; + } + return root; +} + function isXaiSchemaTarget(provider: OcxProviderConfig): boolean { try { // Public api.x.ai accepts native root object unions. Only the Grok CLI proxy @@ -1249,17 +1280,22 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig function toolsToChatFormatForProvider(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined { const base = toolsToChatFormat(parsed, provider); - if (!base || !shouldSanitizeZenToolParameters(provider)) return base; + const azureChat = isAzureOpenAiChatTarget(provider); + const zenChat = shouldSanitizeZenToolParameters(provider); + if (!base || (!zenChat && !azureChat)) return base; return base.map(tool => { if (!tool || typeof tool !== "object") return tool; const functionDef = (tool as { function?: Record }).function; if (!functionDef || typeof functionDef !== "object") return tool; + const parameters = azureChat + ? sanitizeAzureChatToolParameters(functionDef.parameters ?? {}) + : ensureZenRootObjectSchema(functionDef.parameters ?? {}); + const nextFunction = { ...functionDef, parameters }; + // strict: true plus a flattened schema is rejected by Gemini-in-the-pool routers. + if (azureChat) delete nextFunction.strict; return { ...tool, - function: { - ...functionDef, - parameters: ensureZenRootObjectSchema(functionDef.parameters ?? {}), - }, + function: nextFunction, }; }); } diff --git a/tests/azure-model-router-tool-schema.test.ts b/tests/azure-model-router-tool-schema.test.ts new file mode 100644 index 0000000000..352c000682 --- /dev/null +++ b/tests/azure-model-router-tool-schema.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test } from "bun:test"; +import { createOpenAIChatAdapter } from "../src/adapters/openai-chat"; +import type { OcxParsedRequest, OcxProviderConfig } from "../src/types"; + +const toolSchema = { + oneOf: [ + { type: "object", properties: { id: { type: "string" } }, required: ["id"] }, + { type: "object", properties: { name: { type: "string" } }, required: ["name"] }, + ], + enum: ["invalid-at-root"], + const: "invalid-at-root", + not: { type: "null" }, +}; + +function requestFor(provider: OcxProviderConfig): Record { + const parsed: OcxParsedRequest = { + modelId: "model-router", + context: { + messages: [{ role: "user", content: "Use the tool", timestamp: 0 }], + tools: [{ name: "automation_update", namespace: "mcp__codex_app", description: "Update an automation", parameters: toolSchema, strict: true }], + }, + stream: false, + options: {}, + }; + return JSON.parse(createOpenAIChatAdapter(provider).buildRequest(parsed).body) as Record; +} + +describe("Azure Model Router tool schemas", () => { + test("flattens forbidden root composition and removes strict", () => { + const body = requestFor({ + adapter: "openai-chat", + baseUrl: "https://example.openai.azure.com/openai/v1", + apiKey: "test-key", + authMode: "key", + }); + const fn = ((body.tools as Array<{ function: Record }>)[0]).function; + const parameters = fn.parameters as Record; + + expect(parameters.type).toBe("object"); + expect(parameters.properties).toMatchObject({ id: { type: "string" }, name: { type: "string" } }); + for (const key of ["oneOf", "anyOf", "allOf", "enum", "const", "not"]) expect(parameters[key]).toBeUndefined(); + expect(fn.strict).toBeUndefined(); + }); + + test("leaves non-Azure OpenAI-compatible tool schemas unchanged", () => { + const body = requestFor({ adapter: "openai-chat", baseUrl: "https://api.example.test/v1", apiKey: "test-key", authMode: "key" }); + const fn = ((body.tools as Array<{ function: Record }>)[0]).function; + expect(fn.parameters).toEqual({ ...toolSchema, type: "object" }); + expect(fn.strict).toBe(true); + }); +}); From f64ef6dab1a9199f880a9306409b9d469b1ccebe Mon Sep 17 00:00:00 2001 From: Michael Z Freeman Date: Wed, 26 Aug 2026 15:01:31 +0100 Subject: [PATCH 06/11] fix: preserve tool function schema typing --- src/adapters/openai-chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index 0acdf6a1b4..b1d35b42ed 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -1290,7 +1290,7 @@ function toolsToChatFormatForProvider(parsed: OcxParsedRequest, provider: OcxPro const parameters = azureChat ? sanitizeAzureChatToolParameters(functionDef.parameters ?? {}) : ensureZenRootObjectSchema(functionDef.parameters ?? {}); - const nextFunction = { ...functionDef, parameters }; + const nextFunction: Record = { ...functionDef, parameters }; // strict: true plus a flattened schema is rejected by Gemini-in-the-pool routers. if (azureChat) delete nextFunction.strict; return { From c3ef6302394f592fc015f3b5b00a41beee84ceb9 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 27 Aug 2026 12:32:31 +0900 Subject: [PATCH 07/11] docs(devlog): intake matrix and lane roadmap for the 260827 bug-PR merge round Compile-gates all 12 PRs in isolated worktrees, which the draft-PR CI does not do, and assigns each to a disposition lane. Two PRs are not what their status says: - #2694 is review-ready with green checks and does not compile (5 tsc errors, including a call to a function defined nowhere). - #2693 is a test-only diff whose test fails on its own branch; the implementation was never written. #2639 is a real fix carrying a real regression: its created_at backfill breaks the byte-exact passthrough assertion in tests/server-combo-failover-e2e.test.ts:1323. --- .../260827_bug_pr_merge_round/000_intake.md | 51 +++++++++++++ .../001_pr2694_compile_break.md | 57 +++++++++++++++ .../002_pr2639_regression.md | 55 ++++++++++++++ .../003_disposition_matrix.md | 29 ++++++++ .../004_pr2693_missing_implementation.md | 47 ++++++++++++ .../010_lane_l1_commit_then_merge.md | 67 +++++++++++++++++ .../020_lane_l3_cherry_pick.md | 73 +++++++++++++++++++ .../030_lane_l2_close_squash.md | 36 +++++++++ .../040_lane_l4_reimplement.md | 67 +++++++++++++++++ 9 files changed, 482 insertions(+) create mode 100644 devlog/_plan/260827_bug_pr_merge_round/000_intake.md create mode 100644 devlog/_plan/260827_bug_pr_merge_round/001_pr2694_compile_break.md create mode 100644 devlog/_plan/260827_bug_pr_merge_round/002_pr2639_regression.md create mode 100644 devlog/_plan/260827_bug_pr_merge_round/003_disposition_matrix.md create mode 100644 devlog/_plan/260827_bug_pr_merge_round/004_pr2693_missing_implementation.md create mode 100644 devlog/_plan/260827_bug_pr_merge_round/010_lane_l1_commit_then_merge.md create mode 100644 devlog/_plan/260827_bug_pr_merge_round/020_lane_l3_cherry_pick.md create mode 100644 devlog/_plan/260827_bug_pr_merge_round/030_lane_l2_close_squash.md create mode 100644 devlog/_plan/260827_bug_pr_merge_round/040_lane_l4_reimplement.md diff --git a/devlog/_plan/260827_bug_pr_merge_round/000_intake.md b/devlog/_plan/260827_bug_pr_merge_round/000_intake.md new file mode 100644 index 0000000000..aeb012926d --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/000_intake.md @@ -0,0 +1,51 @@ +# 260827 bug-PR merge round — intake + +Base: dev @ 9b838d062 (fast-forwarded from origin/dev on 2026-08-27). +Scope: the 12 open bug-labelled PRs named by the user. +Lanes: L1 commit-then-merge, L2 close + squash-merge, L3 cherry-pick, L4 reimplement. + +## Method + +Every PR head was fetched to a local branch (`pr-check`), checked out into an +isolated worktree under `/tmp/ocx-tc-`, and compiled with `bun x tsc --noEmit` +against the repository's own `node_modules`. This is the gate the repository's +`resolve-pr`-only CI does NOT run for draft PRs: a draft here gets +`enforce-target`, `hygiene`, `label`, `resolve-pr` and CodeRabbit, none of which +compile the tree. Two PRs (#2672, #2674) carry the full matrix because they are +authored by a maintainer. + +## Compile gate result (2026-08-27, local, bun 1.4.0) + +| PR | tsc --noEmit | note | +|---|---|---| +| #2694 | **FAIL (5 errors)** | see 001 | +| #2693 | OK | test-only diff | +| #2690 | OK | | +| #2684 | OK | | +| #2674 | OK | full CI green | +| #2672 | OK | full CI green | +| #2671 | OK | | +| #2663 | OK | | +| #2647 | OK | branch CONFLICTING against dev | +| #2639 | OK | but its own change fails an existing suite, see 002 | +| #2638 | OK | | +| #2497 | OK | branch CONFLICTING against dev | + +Evidence: `/tmp/ocx-pr-typecheck.txt`, produced by `/tmp/ocx-tc-all.sh`. + +## Mergeability and CI as reported by GitHub + +| PR | draft | mergeable | review | checks of note | +|---|---|---|---|---| +| #2694 | ready | MERGEABLE | REVIEW_REQUIRED | all 5 pass | +| #2693 | draft | MERGEABLE | REVIEW_REQUIRED | CodeRabbit pending | +| #2690 | draft | MERGEABLE | REVIEW_REQUIRED | enforce-target FAIL | +| #2684 | draft | MERGEABLE | REVIEW_REQUIRED | enforce-target FAIL, label FAIL | +| #2674 | draft | MERGEABLE | — | full matrix PASS; base is #2672's head | +| #2672 | draft | MERGEABLE | REVIEW_REQUIRED | full matrix PASS | +| #2671 | ready | MERGEABLE | CHANGES_REQUESTED | all pass | +| #2663 | ready | MERGEABLE | REVIEW_REQUIRED | all pass | +| #2647 | ready | **CONFLICTING** | CHANGES_REQUESTED | all pass | +| #2639 | ready | MERGEABLE | CHANGES_REQUESTED | ci FAIL, macos FAIL, test 4/4 FAIL | +| #2638 | draft | MERGEABLE | CHANGES_REQUESTED | enforce-target FAIL, hygiene FAIL | +| #2497 | draft | **CONFLICTING** | REVIEW_REQUIRED | enforce-target FAIL, hygiene FAIL | diff --git a/devlog/_plan/260827_bug_pr_merge_round/001_pr2694_compile_break.md b/devlog/_plan/260827_bug_pr_merge_round/001_pr2694_compile_break.md new file mode 100644 index 0000000000..df09f1548c --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/001_pr2694_compile_break.md @@ -0,0 +1,57 @@ +# #2694 — SenseNova code-mode exec alias: does not compile + +Head: `d6b2433453` (branch `fix/sensenova-code-mode-exec-alias`, author yxr1995-maker). +GitHub shows all five checks green and the PR is marked review-ready with 4/4 boxes +ticked, including "All CI tests are green on my local testing." That claim is false. + +## Evidence + +``` +$ cd /tmp/ocx-pr2694 && bun x tsc --noEmit +src/bridge.ts(645,13): error TS2554: Expected 2-3 arguments, but got 1. +src/bridge.ts(647,11): error TS2304: Cannot find name 'failMalformedCodeModeExecCommand'. +src/server/responses/collaboration.ts(173,9): error TS2304: Cannot find name 'ToolBridgeMaps'. +src/server/responses/core.ts(2946,11): error TS2353: Object literal may only specify known + properties, and 'requestId' does not exist in type '{ code?: ...; retryAfter?: ... }'. +src/server/responses/core.ts(2946,37): error TS2339: Property 'requestId' does not exist on + type 'OcxRequestOptions'. +``` + +Four distinct defects: + +1. `failMalformedCodeModeExecCommand()` is CALLED at `src/bridge.ts:647` and defined + nowhere in the branch — `git grep -n 'failMalformed' pr2694-check -- src/` returns + exactly the one call site. It does not exist on dev either. +2. `freeformInput` takes `(args, toolName, namespace)` on dev (`src/bridge.ts:247`); + the PR calls it with one argument. +3. `ToolBridgeMaps` is used as a parameter type in the new exported function but is + never imported or declared. `buildToolBridgeMaps` returns an inline object type on + dev, so there is no such named type to import. +4. `formatErrorResponse(..., { requestId })` — that options bag has no `requestId`, + and `OcxRequestOptions` has no such property. + +## Why CI did not catch it + +The PR ran only `CodeRabbit`, `enforce-target`, `hygiene`, `label`, `resolve-pr`. +None of those compile or test the tree. The full matrix (`ci`, `macos`, `test 1..4/4`) +only appears on maintainer-authored PRs in this round (#2672, #2674). + +## Second problem: the provider id is wrong + +`src/server/responses/core.ts` gates the alias on `route.providerName === "sensenova"`. +There is no `sensenova` provider in `src/providers/registry.ts`. The id appears only in +`src/providers/free-directory.ts:141` as a free-directory entry +(`https://token.sensenova.cn/v1`). Whether `route.providerName` ever equals +`sensenova` for such a provider is unverified by the PR — it ships no test that +exercises the gate. Its single test calls `bridgeToResponsesSSE` directly with a +hand-built `toolNsMap`, so it never touches `enableSensenovaCodeModeExecCommandAlias` +at all, and would pass even if the gate never fires in production. + +## Lane + +**L4 — reimplement.** The diagnosis (a provider emitting bare `exec_command` instead of +the code-mode `exec` wrapper) is plausible and #2663 is independently solving the +general version of it. This branch cannot be committed-and-merged because it does not +build, and the missing function means there is no "small fix": the malformed-input +failure path was never written. Sequence it AFTER #2663 lands and re-evaluate whether +anything is still needed. diff --git a/devlog/_plan/260827_bug_pr_merge_round/002_pr2639_regression.md b/devlog/_plan/260827_bug_pr_merge_round/002_pr2639_regression.md new file mode 100644 index 0000000000..dfb1631063 --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/002_pr2639_regression.md @@ -0,0 +1,55 @@ +# #2639 — status/created_at backfill: real defect, real regression + +Head: `aa385f9746` (branch `fix/responses-backfill-status-and-created-at`, author bet4it). +Diff: `src/server/responses/responses-field-backfill.ts` +114/-15, plus 343 lines of new +tests. It compiles clean, and its own suite passes (37/37). + +## The defect is real + +Strict Responses decoders require `status` on `OutputMessage` and `created_at: u64` on +the response object. An upstream relay that omits either makes such a client fail with +`missing field`. The PR backfills both, infers the message status from the event type, +and maps response-level `failed`/`cancelled` to `incomplete` rather than `completed` — +that last choice is right, since claiming `completed` would let a client treat a +truncated message as whole. + +## The regression is also real, and it is caused by this PR + +``` +$ cd /tmp/ocx-tc-2639 && bun test ./tests/server-combo-failover-e2e.test.ts +(fail) server combo failover 030 activation matrix > cross-adapter chat 503 to Responses 200 + returns the exact backup response + 73 pass, 1 fail +``` + +On clean dev the same file is 74 pass / 0 fail, so this is not a pre-existing flake. + +The diff of the failing assertion: + +``` + { ++ "created_at": 1787801315, + "id": "resp-m2", +``` + +`tests/server-combo-failover-e2e.test.ts:1323` asserts the proxy returns the backup +provider's JSON **exactly** (`expect(await response.json()).toEqual(exact)`). The +`created_at` backfill injects a field the upstream never sent, so a passthrough body is +no longer byte-identical. + +This is a genuine contract conflict, not a stale test. The combo-failover contract says +a passthrough backup response is returned unchanged; the backfill says a response +missing `created_at` gets one. Both cannot hold for the same body. + +## Resolution direction + +The `status` half is uncontroversial and stays. For `created_at`, the backfill must not +apply to a passthrough body that is being relayed verbatim — scope it to the +translated/bridged path, or make the combo passthrough exempt. Deciding which of the two +contracts yields is a maintainer call and belongs in the lane doc, not here. + +## Lane + +**L3 — cherry-pick.** Take the `status` backfill and its tests; hold `created_at` until +the passthrough-exactness conflict is resolved, then land it as its own change with the +combo test updated deliberately. diff --git a/devlog/_plan/260827_bug_pr_merge_round/003_disposition_matrix.md b/devlog/_plan/260827_bug_pr_merge_round/003_disposition_matrix.md new file mode 100644 index 0000000000..34f537931b --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/003_disposition_matrix.md @@ -0,0 +1,29 @@ +# Disposition matrix — all 12 PRs + +| PR | title | lane | reason | +|---|---|---|---| +| #2672 | normalize canonical forward prompt envelope | **L1** | maintainer-authored, full CI matrix green, destination-scoped, manifest version bumped, docs updated. Merge first — it is the base of #2674. | +| #2674 | normalize Posit tool continuations | **L1** | stacked child of #2672, full CI matrix green. Merge second, retarget to dev after the parent lands. | +| #2671 | Muse Spark image input on OpenCode Go | **L1** | five-line registry declaration on an existing mechanism, four focused tests, CodeRabbit clean, review recommends merge. Add the reviewer's requested test (live row advertises `["text"]`, config still wins) before merging. | +| #2693 | Gemini 3 thought-signature fallback | **L4** | 15 lines, test-only — and the test FAILS on its own branch because the implementation was never written (see 004). Needs an upstream fact before it can be implemented or closed. | +| #2684 | Azure Model Router function schemas | **L1** | 92 lines, self-contained, host-scoped to Azure endpoints, reuses the existing Zen flattening helper, ships a negative test proving non-Azure targets are untouched. Fix the label/enforce-target hygiene, then merge. | +| #2639 | backfill status and created_at | **L3** | `status` half is correct; `created_at` breaks `tests/server-combo-failover-e2e.test.ts:1323` (proven, see 002). Cherry-pick `status`, hold `created_at`. | +| #2647 | Command Code reasoning presets | **L3** | content is three table rows and is fine, but the branch conflicts with dev on `src/providers/command-code-efforts.ts` and its test rewrites a catalog snapshot count 51 -> 60 that must be re-verified live. Re-apply the rows on a dev-based branch. | +| #2690 | normalize xAI Responses root tool schemas | **L3** | 926/289 across 8 files including a 430-line new module and a 269-line deletion from `openai-chat.ts`. Overlaps #2684 conceptually. Take the xai-tool-schema normalization; leave the openai-chat refactor out of a bug round. | +| #2663 | bridge code-mode helpers through exec | **L2** | 528/68 across 12 files with substantive tests, CI green, compiles. Too broad to merge as a single review-required commit on its own head; land squashed with a written summary and close. Sequence before #2694. | +| #2694 | SenseNova bare exec_command wrapper | **L4** | does not compile: 5 tsc errors, one call to a function that does not exist (see 001). Gate keys on a provider id absent from the registry. Reimplement minimally after #2663, or close as NOOP if #2663 subsumes it. | +| #2638 | close drain routing follow-ups | **L4** | 1341/119 across 7 files, hygiene FAIL, enforce-target FAIL, CHANGES_REQUESTED. Touches `src/server/responses/core.ts` and subagent fallback — shared runtime. Rewrite the actual routing fix minimally. | +| #2497 | native main token refresh and replay | **L4** | 2622/76 across 20 files, CONFLICTING on 5 files including `src/server/responses/core.ts`, hygiene FAIL. Touches OAuth token refresh = credential boundary, so it needs explicit security review per MAINTAINERS.md before any rewrite lands. | + +## Order + +L1 first (#2672 -> #2674 -> #2671 -> #2684): each is small, green, and independent of +the others except the declared stack. + +L3 second (#2639 -> #2647 -> #2690): each needs a dev-based branch and a decision +about which part travels. + +L2 third (#2663): large but healthy; landing it changes the answer for #2694. + +L4 last (#2693 -> #2694 -> #2638 -> #2497): each is a rewrite. #2693 is blocked on an +upstream provider fact and #2497 additionally needs a human security decision. diff --git a/devlog/_plan/260827_bug_pr_merge_round/004_pr2693_missing_implementation.md b/devlog/_plan/260827_bug_pr_merge_round/004_pr2693_missing_implementation.md new file mode 100644 index 0000000000..2895ea6cb9 --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/004_pr2693_missing_implementation.md @@ -0,0 +1,47 @@ +# #2693 — Gemini 3 thought-signature fallback: test without implementation + +Head branch `fix/gemini-thought-signature-bypass`, author yxr1995-maker. The diff is +15 added lines in ONE file: `tests/google-antigravity-replay.test.ts`. No source file +is touched. + +The test asserts that when the replay cache misses, the first `functionCall` part gets +`thoughtSignature = "skip_thought_signature_validator"`: + +``` +$ cd /tmp/ocx-tc-2693 && bun test ./tests/google-antigravity-replay.test.ts +(fail) durable antigravity replay snapshot > fallback to skip_thought_signature_validator + on the first functionCall when replay cache misses + 61 pass, 1 fail + + 1015 | expect(parts[0].thoughtSignature).toBe("skip_thought_signature_validator"); + Expected: "skip_thought_signature_validator" + Received: undefined +``` + +The string `skip_thought_signature_validator` appears nowhere under `src/`: +`grep -c` returns 0 for `google-antigravity-replay.ts`, `google.ts`, and +`google-antigravity-wire.ts`. `applyAntigravityReplay` only assigns a signature it +actually has cached (`src/adapters/google-antigravity-replay.ts`), so on a cache miss +the part is left alone by design. + +So the PR is a red test for an unimplemented behavior. It is still a DRAFT with +CodeRabbit pending, which is consistent — the author may not have pushed the source +half yet. + +## Is the underlying claim right? + +Plausible but unverified here. CLIProxyAPI uses that sentinel to bypass Gemini's +signature validator when it has nothing genuine to replay. The existing dev design +takes the opposite position — `src/adapters/google-antigravity-wire.ts` deliberately +refuses to forward a non-genuine signature, because "sending a foreign id as `n` +breaks" continuity. A sentinel that upstream treats as "skip validation" is a +different thing from a forged signature, but that distinction needs an upstream fact +to settle, and nothing in this repository establishes it. + +## Lane + +**L4 — reimplement**, downgraded from the initial L1 read. Either implement the +fallback in `applyAntigravityReplay` and keep the test, or close the PR as +not-reproducible. Deciding needs one external fact: whether Gemini 3 on Antigravity +actually honors `skip_thought_signature_validator` on a functionCall part. Until that +is answered the PR is BLOCKED on an upstream provider fact, not on our implementation. diff --git a/devlog/_plan/260827_bug_pr_merge_round/010_lane_l1_commit_then_merge.md b/devlog/_plan/260827_bug_pr_merge_round/010_lane_l1_commit_then_merge.md new file mode 100644 index 0000000000..a0748cf988 --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/010_lane_l1_commit_then_merge.md @@ -0,0 +1,67 @@ +# L1 — commit-then-merge + +Members: #2672, #2674, #2671, #2684. Each lands on its own head branch and merges to dev. + +## Order and dependency + +``` +#2672 (base dev) -> #2674 (base = #2672 head) -> #2671 -> #2684 +``` + +#2674 is a declared stacked child. `enforce-target` skips the wrong-base gate for such +children, so it is legitimate; after #2672 merges, retarget #2674 to `dev` before +merging it. + +## #2672 — canonical forward prompt envelope + +Full CI matrix green (ci, macos, all 4 test shards, keyring x3, npm-global x3, gates, +storage policy, api usage, react-doctor). No change needed. The implementation folds +text-only `system` input messages into top-level `instructions` and strips +`truncation`, both scoped to `isCanonicalOpenAiForwardProvider`. The fold is atomic: a +single multimodal system block aborts the whole rewrite, so nothing is silently +dropped. Compatibility manifest bumped 1.0.0 -> 1.1.0 with new fixture assertions, and +both `adapters.md` and `proxy-formats.md` document the destination scoping. + +Action: merge as-is. + +## #2674 — Posit tool continuations + +Full CI matrix green. Adds `stripPromptCacheBreakpoints` with an explicit depth cap +(64) and node budget (100k) that aborts atomically rather than returning a partially +rewritten subtree, plus `item_reference` removal gated on `store: false`. Docs updated +in the same two reference pages. + +Action: after #2672 merges, retarget base to `dev`, confirm CI re-runs green, merge. + +## #2671 — Muse Spark image input + +Five lines in `src/providers/registry.ts` adding +`"muse-spark-1.2-contributor": ["text", "image"]` to the opencode-go +`modelInputModalities` map, plus four focused tests. CodeRabbit found nothing. The +maintainer review recommends merge with one addition: a test proving the configured +value still wins when the live discovery row advertises only `["text"]`. + +Action: add that test on the head branch, then merge. + +Open questions the reviewer raised and this round does NOT resolve (they are not +blockers): whether Zen Go also serves the non-contributor `muse-spark-1.2` id, and +whether Muse should carry a context window like its neighbours. + +## #2684 — Azure Model Router function schemas + +92/5 across two files. `isAzureOpenAiChatTarget` matches four Azure hostname suffixes; +`sanitizeAzureChatToolParameters` reuses the existing `ensureZenRootObjectSchema` +flattening, then deletes the six forbidden root keys and drops `strict`. The second +test asserts a non-Azure base URL is left completely untouched, which is the +containment proof this kind of change needs. + +Blockers to clear before merge: `enforce-target` FAIL and `label` FAIL. Both are +hygiene, not code — the PR is a draft with an incomplete description checklist. Verify +what each check actually demands, fix the PR body, and re-run. + +## Gate before any L1 merge + +`bun run typecheck` plus the focused tests for each touched subsystem. #2672 and #2674 +touch `src/adapters/openai-responses.ts`, which is shared runtime, so the full suite +must be green at the merge commit — but both already carry a green full matrix at their +own heads, so re-running the full suite is only required if a rebase changes the tree. diff --git a/devlog/_plan/260827_bug_pr_merge_round/020_lane_l3_cherry_pick.md b/devlog/_plan/260827_bug_pr_merge_round/020_lane_l3_cherry_pick.md new file mode 100644 index 0000000000..994cde911e --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/020_lane_l3_cherry_pick.md @@ -0,0 +1,73 @@ +# L3 — cherry-pick + +Members: #2639, #2647, #2690. Each carries a correct part and a part that must not +travel with it. + +## #2639 — take `status`, hold `created_at` + +Full analysis in 002. The `status` backfill is correct and self-contained: +`backfillItemStatus` only touches `type === "message"` items that have no `status`, +and `messageStatusFromResponseStatus` maps `failed`/`cancelled` to `incomplete` +rather than overclaiming `completed`. + +The `created_at` backfill injects a field into passthrough bodies and breaks +`tests/server-combo-failover-e2e.test.ts:1323`, which asserts the backup provider's +JSON is returned byte-exact. Proven: that file is 74/74 on dev and 73/74 on this branch. + +Take: `backfillItemStatus`, `inferredStatusForEventType`, +`messageStatusFromResponseStatus`, the `inferredStatus` threading through +`backfillOutputItem` / `backfillResponseOutput` / `rewriteEvent`, and the subset of +the 343 new test lines that covers status. + +Leave: the `created_at` insertion and its tests. + +Then decide the `created_at` contract separately: either scope the backfill to the +translated path so a verbatim relay stays verbatim, or change the combo-failover +contract deliberately and update its test. That decision is a maintainer call; it is +NOT made by quietly letting the assertion break. + +## #2647 — re-apply three table rows on a dev-based branch + +The branch is CONFLICTING: `src/providers/command-code-efforts.ts` has moved on dev +(`git merge-tree` reports a content conflict on exactly that file). The content the PR +wants is three entries in `COMMAND_CODE_MODEL_EFFORTS`: +`deepseek/deepseek-v4-flash-vision-exp` (high, max), `gpt-5.6-luna` +(low..max), `google/gemini-3.7-flash` (low, medium, high). + +Two things must be re-verified rather than trusted, because both are time-sensitive +catalog facts: + +1. The test rewrites the CommandCode catalog snapshot from 51 rows to 60. That number + is a live-roster fact and the fixture was regenerated by the author; confirm it + against the current fixture rather than accepting the delta. +2. The effort ladders themselves are provider capability claims. Per the repo's own + convention in this file, a ladder is recorded with the evidence that produced it. + +Plan: branch from dev, apply the three rows, port the assertions, re-verify the +snapshot count, land, and comment on #2647 with the landed sha. + +## #2690 — take the xAI schema normalization, leave the refactor + +926 added / 289 deleted across 8 files. The deletion is 269 lines removed from +`src/adapters/openai-chat.ts` and the addition is a new 430-line +`src/adapters/xai-tool-schema.ts`, i.e. a module extraction bundled with a bug fix. +It also adds 179 test lines to `openai-responses-passthrough.test.ts` and touches +`src/server/responses/core.ts` — shared runtime. + +Conceptual overlap with #2684: both normalize tool schemas whose root is a union, for +different destinations. #2684 is host-scoped to Azure and 92 lines; #2690 rewrites the +xAI Responses lane. They do not textually conflict (different helper, different call +site) but landing #2684 first keeps the smaller, provable change independent of the +larger one. + +Take: the xAI root-schema normalization and its tests. +Leave: the `openai-chat.ts` extraction, unless review concludes the fix is impossible +without it — in which case it becomes its own non-bug PR. + +`enforce-target` FAIL must be diagnosed before anything lands. + +## Gate + +Each cherry-pick lands on a dev-based branch with a focused test that fails before and +passes after. #2639 and #2690 touch shared runtime, so the full suite must be green at +the merge commit — run it on `ssh lidge-ai` via `ocx-run` rather than blocking this box. diff --git a/devlog/_plan/260827_bug_pr_merge_round/030_lane_l2_close_squash.md b/devlog/_plan/260827_bug_pr_merge_round/030_lane_l2_close_squash.md new file mode 100644 index 0000000000..29a8ecba92 --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/030_lane_l2_close_squash.md @@ -0,0 +1,36 @@ +# L2 — close + squash-merge + +Member: #2663. + +## #2663 — bridge code-mode helpers through exec + +528 added / 68 deleted across 12 files, author Eleven-is-cool, marked review-ready with +all five checks green. It compiles clean (`TYPECHECK_OK`, see 000). + +Files: `src/bridge.ts`, a new `src/responses/code-mode-helper-compat.ts`, +`src/responses/custom-tool-compat.ts`, `src/server/responses-custom-tool-repair.ts`, +`src/server/responses/core.ts`, `src/types/tools.ts`, plus six test files including two +new ones (`legacy-shell-compat.test.ts` +93, `responses-custom-tool-repair.test.ts` ++126) and a substantial extension of `responses-undeclared-tool-guard.test.ts`. + +Why L2 and not L1: the change is broad enough that its value is in the squashed +summary, and it touches `src/server/responses/core.ts` — the shared request path. +Landing it as one reviewed commit with a written rationale gives the history a single +revert point, which a 12-file merge commit from a contributor branch does not. + +Why it lands at all: the tests are real (they exercise the repair and guard paths, not +just the happy path), CI is green, and the defect class — a provider emitting a bare +helper call instead of the code-mode `exec` wrapper — is the same class #2694 tried and +failed to address. + +## Sequencing consequence + +Landing this changes the answer for #2694. Re-evaluate #2694 only after this is on dev: +if the general bridge already handles the bare `exec_command` case, #2694 becomes NOOP +and closes with a pointer to this sha. + +## Gate + +Shared runtime is touched, so the full suite must be green before the squash lands. +Run it on `ssh lidge-ai` via `ocx-run`. Then close #2663 with a comment naming the +squashed dev sha and crediting the author. diff --git a/devlog/_plan/260827_bug_pr_merge_round/040_lane_l4_reimplement.md b/devlog/_plan/260827_bug_pr_merge_round/040_lane_l4_reimplement.md new file mode 100644 index 0000000000..b946d976f8 --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/040_lane_l4_reimplement.md @@ -0,0 +1,67 @@ +# L4 — reimplement + +Members: #2693, #2694, #2638, #2497. Ordered by how much external input each needs. + +## #2694 — SenseNova bare exec_command + +Does not compile (001): five tsc errors, including a call to +`failMalformedCodeModeExecCommand()` that is defined nowhere in the branch or on dev. +Its gate keys on `route.providerName === "sensenova"`, an id absent from +`src/providers/registry.ts` and present only in `src/providers/free-directory.ts:141`. +Its one test bypasses the gate entirely by hand-building the `toolNsMap`, so it would +pass even if the alias never activated in production. + +Do NOT rewrite this before #2663 lands. Then: + +- If #2663's general helper bridge already covers a bare `exec_command` call, close + #2694 as NOOP with the sha. +- Otherwise implement the narrow gap on a dev-based branch, with a test that goes + through the real request path so the provider gate is actually exercised. + +## #2693 — Gemini 3 thought-signature fallback + +Test-only diff whose test fails on its own branch (004). BLOCKED on one upstream fact: +does Gemini 3 on Antigravity honor `skip_thought_signature_validator` as a functionCall +`thoughtSignature`? Dev currently takes the opposite stance and refuses to forward +non-genuine signatures. Until that is answered, neither implementing nor closing is +justified — record the question on the PR and move on. + +## #2638 — codex drain routing follow-ups + +1341 added / 119 deleted across 7 files, of which 936 lines are tests +(`codex-routing.test.ts` +405, `subagent-fallback-handle-responses.test.ts` +423, +`codex-auth-context.test.ts` +108). Source: `src/codex/routing.ts` +283/-51, +`src/server/responses/core.ts` +52/-26, `src/codex/subagent-model-fallback.ts` +62/-24. + +Blocking state: `hygiene` FAIL, `enforce-target` FAIL, CHANGES_REQUESTED, and the +`intake: hygiene-blocked` label. Diagnose the hygiene failure first — it decides +whether any of this branch is reusable. + +The subagent-fallback path is the one `AGENTS.md` calls out specifically: the +synchronous activation chain in `src/server/index.ts` has nowhere to await, so an +`await` introduced there silently reroutes subagents to a different model than the +operator configured. Any rewrite must preserve that synchronicity and prove it. + +Plan: extract the actual routing defect from the 283-line `routing.ts` change, +reimplement it minimally on a dev-based branch, keep whichever of the author's tests +pin real behavior, credit the author, close the original. + +## #2497 — native main token refresh and replay + +2622 added / 76 deleted across 20 files. CONFLICTING against dev on five files: +`src/codex/auth-context.ts`, `src/codex/model-entitlements.ts`, +`src/routing/analytics.ts`, `src/server/responses/core.ts`, `src/usage/log.ts`. +`hygiene` FAIL and `enforce-target` FAIL. + +It touches `src/oauth/chatgpt.ts` (+88/-12) and `src/codex/main-account.ts` +(+551/-12) — OAuth token refresh and account credential storage. Per `MAINTAINERS.md` +that is the security boundary: authentication and credential/token handling require +explicit security review, and token logging or serialization is a release blocker. + +Therefore this PR is `NEEDS_HUMAN` for the security decision regardless of how good the +code is. The merge round can prepare the ground — diagnose the hygiene failure, resolve +the five-file conflict semantically, isolate the actual refresh/replay defect — but it +does not land without that review. + +Any security analysis written while doing so goes to scratch space (`.tmp/` or +`mktemp -d`), never into `devlog/`, per `AGENTS.md`. From d1c398871f0be546a15da5a609031355611f4b41 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 27 Aug 2026 12:44:23 +0900 Subject: [PATCH 08/11] docs(devlog): fold A-gate audit corrections into the merge-round plan Independent audit found three overconfident claims: the compile gate ran on stale PR heads (4-294 commits behind dev), #2684 and #2690 do conflict on src/adapters/openai-chat.ts, and "#2663 CI green" cites the same five non-compiling checks that let #2694 ship five tsc errors. --- .../260827_bug_pr_merge_round/000_intake.md | 4 +- .../005_audit_corrections.md | 103 ++++++++++++++++++ .../020_lane_l3_cherry_pick.md | 11 +- .../030_lane_l2_close_squash.md | 11 +- 4 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 devlog/_plan/260827_bug_pr_merge_round/005_audit_corrections.md diff --git a/devlog/_plan/260827_bug_pr_merge_round/000_intake.md b/devlog/_plan/260827_bug_pr_merge_round/000_intake.md index aeb012926d..b7908ab937 100644 --- a/devlog/_plan/260827_bug_pr_merge_round/000_intake.md +++ b/devlog/_plan/260827_bug_pr_merge_round/000_intake.md @@ -8,7 +8,9 @@ Lanes: L1 commit-then-merge, L2 close + squash-merge, L3 cherry-pick, L4 reimple Every PR head was fetched to a local branch (`pr-check`), checked out into an isolated worktree under `/tmp/ocx-tc-`, and compiled with `bun x tsc --noEmit` -against the repository's own `node_modules`. This is the gate the repository's +against the repository's own `node_modules`. Note this compiles the PR HEAD, which is +behind dev by 4 to 294 commits depending on the PR; the merged-tree gate a merge round +actually needs is in 005. This is the gate the repository's `resolve-pr`-only CI does NOT run for draft PRs: a draft here gets `enforce-target`, `hygiene`, `label`, `resolve-pr` and CodeRabbit, none of which compile the tree. Two PRs (#2672, #2674) carry the full matrix because they are diff --git a/devlog/_plan/260827_bug_pr_merge_round/005_audit_corrections.md b/devlog/_plan/260827_bug_pr_merge_round/005_audit_corrections.md new file mode 100644 index 0000000000..74e97be137 --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/005_audit_corrections.md @@ -0,0 +1,103 @@ +# Audit corrections to the intake (A-gate round 1) + +An independent reviewer audited the unit at c3ef63023 and produced two findings that +the plan got wrong. Both are confirmed here by direct command output, and the affected +documents are corrected rather than defended. + +## Correction 1 — the compile gate ran against stale heads + +Every PR head is behind current dev, some by a lot: + +``` +$ for n in ...; do git rev-list --count pr${n}-check..origin/dev; done +2694 26 2693 26 2690 4 2684 84 +2674 87 2672 87 2671 87 2663 96 +2647 84 2639 4 2638 87 2497 294 +``` + +So `TYPECHECK_OK` on a PR head proved that head compiles, not that the MERGED tree +compiles. That is the number a merge round actually needs. + +Re-ran the gate on the merge result (`git merge origin/dev + pr-check`, then +`bun x tsc --noEmit`), recorded in `/tmp/ocx-merged-typecheck.txt`: + +``` +2672 MERGED_TYPECHECK_OK 2671 MERGED_TYPECHECK_OK +2684 MERGED_TYPECHECK_OK 2663 MERGED_TYPECHECK_OK +2690 MERGED_TYPECHECK_OK 2639 MERGED_TYPECHECK_OK +2638 MERGED_TYPECHECK_OK +``` + +The conclusions in 000 survive, but they now rest on the right evidence. #2694's +failure is unaffected: a call to an undefined function does not become defined by +merging dev. + +## Correction 2 — #2684 and #2690 DO conflict + +020 stated they "do not textually conflict (different helper, different call site)." +That is false: + +``` +$ git merge-tree --write-tree pr2684-check pr2690-check +exit=1 +100644 ... 1 src/adapters/openai-chat.ts +100644 ... 2 src/adapters/openai-chat.ts +100644 ... 3 src/adapters/openai-chat.ts +``` + +The reason is structural, not incidental. #2690 DELETES the region #2684 edits: + +``` +$ git diff origin/dev...pr2690-check -- src/adapters/openai-chat.ts +@@ -922,16 +927,6 @@ -function isXaiSchemaTarget(...) +@@ -1212,265 +1207,6 @@ -function normalizeXaiToolParameters(...) (+14 more) +``` + +#2690 moves ~269 lines of xAI schema logic out of `openai-chat.ts` into the new +`src/adapters/xai-tool-schema.ts`, while #2684 adds its Azure helper right beside +`shouldSanitizeZenToolParameters` in the same file and rewrites +`toolsToChatFormatForProvider`, which #2690 also touches. + +Consequence for sequencing: whichever lands second must be re-applied by hand, not +merged. Land #2684 FIRST — it is 92 lines with a containment test and a merged-tree +OK — then rebase #2690's extraction on top, where the conflict is resolved once by the +party doing the extraction. The L3 plan for #2690 (take the normalization, leave the +refactor) becomes more attractive, since the refactor is exactly the conflicting half. + +## Correction 3 — "#2663 CI green" means less than it sounds + +The reviewer turned the plan's own #2694 argument back on it. 030 justifies squashing +#2663 partly on "CI is green". But #2663 ran EXACTLY the same five checks #2694 did: + +``` +$ gh pr checks 2663 +CodeRabbit=pass enforce-target=pass hygiene=pass label=pass resolve-pr=pass +``` + +None of those compile or test the tree — which is precisely why #2694 shipped five tsc +errors behind five green checks. So "#2663 is CI green" is not evidence of health; it +is the same non-evidence, and the plan should not have leaned on it. + +What IS evidence for #2663: it typechecks at its head (`TYPECHECK_OK`) and in the +merged tree (`MERGED_TYPECHECK_OK`). That covers compilation, not behavior. Its 12-file +diff touching `src/server/responses/core.ts` still needs the full suite green on the +merged tree before the squash lands, per 030's own gate — that gate is now the ONLY +thing standing behind this PR, so it is not optional. + +Note for whoever runs it: `bun test` here takes a machine-wide lock +(`bare Bun worker N is waiting for test run pid M to release the machine lock`), so +concurrent suite runs serialize. Run the full suite once, on `ssh lidge-ai` via +`ocx-run`, rather than racing several locally. + +## What this says about the round + +Three claims were wrong in the plan, all in the same direction: stated more confidently +than the evidence supported. The lane assignments themselves survive, but three rules +now bind the rest of the round: + +1. The merged-tree compile gate is the standard, not the PR-head gate. +2. Cross-PR conflicts are checked pairwise with `git merge-tree` before any merge, + never inferred from "different helper, different call site." +3. "Checks are green" is never evidence of health on this repository unless the check + list actually includes `ci` / `test N/4` / `macos`. For draft and contributor PRs + it usually does not. diff --git a/devlog/_plan/260827_bug_pr_merge_round/020_lane_l3_cherry_pick.md b/devlog/_plan/260827_bug_pr_merge_round/020_lane_l3_cherry_pick.md index 994cde911e..05c274d8e9 100644 --- a/devlog/_plan/260827_bug_pr_merge_round/020_lane_l3_cherry_pick.md +++ b/devlog/_plan/260827_bug_pr_merge_round/020_lane_l3_cherry_pick.md @@ -54,11 +54,12 @@ snapshot count, land, and comment on #2647 with the landed sha. It also adds 179 test lines to `openai-responses-passthrough.test.ts` and touches `src/server/responses/core.ts` — shared runtime. -Conceptual overlap with #2684: both normalize tool schemas whose root is a union, for -different destinations. #2684 is host-scoped to Azure and 92 lines; #2690 rewrites the -xAI Responses lane. They do not textually conflict (different helper, different call -site) but landing #2684 first keeps the smaller, provable change independent of the -larger one. +Overlap with #2684: both normalize tool schemas whose root is a union, for different +destinations. They DO textually conflict — see 005. `git merge-tree` between the two +heads reports a content conflict on `src/adapters/openai-chat.ts`, because #2690 +deletes the ~269-line xAI region and rewrites `toolsToChatFormatForProvider`, which is +exactly what #2684 edits. Land #2684 first, then rebase #2690's extraction on top and +resolve once. Take: the xAI root-schema normalization and its tests. Leave: the `openai-chat.ts` extraction, unless review concludes the fix is impossible diff --git a/devlog/_plan/260827_bug_pr_merge_round/030_lane_l2_close_squash.md b/devlog/_plan/260827_bug_pr_merge_round/030_lane_l2_close_squash.md index 29a8ecba92..1848ddb0e6 100644 --- a/devlog/_plan/260827_bug_pr_merge_round/030_lane_l2_close_squash.md +++ b/devlog/_plan/260827_bug_pr_merge_round/030_lane_l2_close_squash.md @@ -19,9 +19,14 @@ Landing it as one reviewed commit with a written rationale gives the history a s revert point, which a 12-file merge commit from a contributor branch does not. Why it lands at all: the tests are real (they exercise the repair and guard paths, not -just the happy path), CI is green, and the defect class — a provider emitting a bare -helper call instead of the code-mode `exec` wrapper — is the same class #2694 tried and -failed to address. +just the happy path), it compiles at its head AND in the merged tree, and the defect +class — a provider emitting a bare helper call instead of the code-mode `exec` wrapper +— is the same class #2694 tried and failed to address. + +Do NOT cite "CI is green" for this PR. It ran the same five non-compiling checks as +#2694 (CodeRabbit, enforce-target, hygiene, label, resolve-pr); none of them build or +test anything. See 005, correction 3. The full suite on the merged tree is the only +behavioral evidence this PR will have, so it is mandatory before the squash. ## Sequencing consequence From 90ef53fb0ddb7f91ff19aa69041fae9c04b3505d Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 27 Aug 2026 12:45:03 +0900 Subject: [PATCH 09/11] docs(devlog): operative execution order for the merge round Orders #2684 before #2690 because they conflict on src/adapters/openai-chat.ts, and records the four standing evidence rules the A gate produced. --- .../006_execution_order.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 devlog/_plan/260827_bug_pr_merge_round/006_execution_order.md diff --git a/devlog/_plan/260827_bug_pr_merge_round/006_execution_order.md b/devlog/_plan/260827_bug_pr_merge_round/006_execution_order.md new file mode 100644 index 0000000000..03fa4eff0d --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/006_execution_order.md @@ -0,0 +1,38 @@ +# Execution order and per-PR gates + +This is the operative sheet for work-phases wp2..wp5. It supersedes the order lines in +003 wherever they disagree, because it accounts for the pairwise conflict found at the +A gate (005, correction 2). + +## Order + +| # | PR | lane | gate before it lands | +|---|---|---|---| +| 1 | #2672 | L1 | already full-matrix green; merge | +| 2 | #2674 | L1 | retarget to dev after #2672, confirm CI re-run, merge | +| 3 | #2671 | L1 | add the reviewer's live-row-vs-config test, then merge | +| 4 | #2684 | L1 | fix PR-body checklist (label + enforce-target), merged-tree suite for the adapter, merge — **before #2690** | +| 5 | #2639 | L3 | cherry-pick `status` only; combo-failover file must be 74/74 | +| 6 | #2647 | L3 | re-apply 3 rows on dev-based branch; re-verify the 51->60 snapshot live | +| 7 | #2690 | L3 | rebase on post-#2684 dev, resolve the openai-chat.ts conflict once | +| 8 | #2663 | L2 | FULL suite on merged tree via ocx-run on lidge-ai — mandatory, it has no other behavioral evidence | +| 9 | #2694 | L4 | re-evaluate after #2663; NOOP-and-close if subsumed | +| 10 | #2638 | L4 | diagnose hygiene failure first; preserve synchronous subagent activation | +| 11 | #2693 | L4 | BLOCKED pending one upstream fact; post the question on the PR | +| 12 | #2497 | L4 | NEEDS_HUMAN security review before anything lands | + +## Standing rules for this round (from the A gate) + +1. Compile evidence means the MERGED tree, never the PR head alone. +2. Every pair of PRs touching a shared file gets `git merge-tree` before either merges. +3. Green checks are not health unless the list includes `ci` / `test N/4` / `macos`. +4. `bun test` takes a machine-wide lock; run one suite at a time, long ones on lidge-ai. +5. Pushes use `--no-verify` (user-approved for this round). No main/preview, no release. + +## Known cross-PR file contention + +`src/adapters/openai-chat.ts` — #2684 and #2690 (conflict proven, ordered above). +`src/server/responses/core.ts` — #2663, #2638, #2497, and #2694's abandoned attempt. +Any two of those landing must be re-checked pairwise, not assumed independent. +`src/server/responses/responses-field-backfill.ts` — #2639 only. +`src/providers/registry.ts` — #2671 only; `command-code-efforts.ts` — #2647 only. From 4a4df12f242776e361b8dce7fc3d70fc846f1121 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 27 Aug 2026 13:09:19 +0900 Subject: [PATCH 10/11] test(catalog): pin Muse image input against a text-only discovered row The four tests #2671 shipped all read the registry or the seed, so they would pass even if applyProviderConfigHints let a discovered row win. Zen Go publishes no modality metadata, so the discovered row is exactly where this can break. Proven load-bearing: deleting the registry line makes all six fail; restoring it makes all six pass. Requested in the maintainer review on #2671. --- tests/opencode-go-muse-vision.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/opencode-go-muse-vision.test.ts b/tests/opencode-go-muse-vision.test.ts index f01c21a7f9..d915b9292a 100644 --- a/tests/opencode-go-muse-vision.test.ts +++ b/tests/opencode-go-muse-vision.test.ts @@ -48,4 +48,29 @@ describe("OpenCode Go Muse Spark image input (#vision)", () => { expect(prov.noVisionModels ?? []).not.toContain(MUSE_MODEL); expect(prov.modelInputModalities?.[MUSE_MODEL]).toEqual(["text", "image"]); }); + + // The registry declaration only matters if it survives a live discovery row that + // advertises Muse as text-only. Zen Go publishes no modality metadata, so a + // discovered row can arrive with ["text"] or with nothing at all; in both cases the + // configured value is authoritative (provider-fetch.ts applyProviderConfigHints reads + // configuredInputModalities first). Without this the PR would pass while the catalog + // still blocked image attachments in production. + test("the configured declaration overrides a text-only discovered row", () => { + const prov = opencodeGo(); + const hinted = applyProviderConfigHints("opencode-go", prov, { + id: MUSE_MODEL, + provider: "opencode-go", + inputModalities: ["text"], + }); + expect(hinted.inputModalities).toEqual(["text", "image"]); + }); + + test("the configured declaration fills in a discovered row with no modalities", () => { + const prov = opencodeGo(); + const hinted = applyProviderConfigHints("opencode-go", prov, { + id: MUSE_MODEL, + provider: "opencode-go", + }); + expect(hinted.inputModalities).toEqual(["text", "image"]); + }); }); From 36ff16e6ed5c79cd2c18892f682f0483afbadb6b Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 27 Aug 2026 13:11:28 +0900 Subject: [PATCH 11/11] docs(devlog): fold audit round 2 into the merge-round plan The auditor returned VERDICT: FAIL with 11 findings. Findings 1-4 reproduce the headline claims exactly; 5-11 are corrections. Newly folded in: the CI-mechanism claim in 001 was invented (2639 has all 27 checks and a non-maintainer author), 2690's fix imports the module its refactor creates so L3 was incoherent (reclassified L4), 2671's probe-evidence question survives the test that was added, and core.ts is a five-way constraint whose evidence expires on every merge. --- .../001_pr2694_compile_break.md | 9 ++- .../003_disposition_matrix.md | 2 +- .../007_audit_round2.md | 61 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 devlog/_plan/260827_bug_pr_merge_round/007_audit_round2.md diff --git a/devlog/_plan/260827_bug_pr_merge_round/001_pr2694_compile_break.md b/devlog/_plan/260827_bug_pr_merge_round/001_pr2694_compile_break.md index df09f1548c..2a22571884 100644 --- a/devlog/_plan/260827_bug_pr_merge_round/001_pr2694_compile_break.md +++ b/devlog/_plan/260827_bug_pr_merge_round/001_pr2694_compile_break.md @@ -33,8 +33,13 @@ Four distinct defects: ## Why CI did not catch it The PR ran only `CodeRabbit`, `enforce-target`, `hygiene`, `label`, `resolve-pr`. -None of those compile or test the tree. The full matrix (`ci`, `macos`, `test 1..4/4`) -only appears on maintainer-authored PRs in this round (#2672, #2674). +None of those compile or test the tree. + +An earlier version of this note claimed the full matrix "only appears on +maintainer-authored PRs." That was wrong — see 007, finding 6. #2639 carries all 27 +checks and its author is not a maintainer, and `ci.yml` is `pull_request: {}` with no +draft gating. Why some PRs here get five checks and others twenty-seven is not +established; do not guess at it again. ## Second problem: the provider id is wrong diff --git a/devlog/_plan/260827_bug_pr_merge_round/003_disposition_matrix.md b/devlog/_plan/260827_bug_pr_merge_round/003_disposition_matrix.md index 34f537931b..9cd42c5b6c 100644 --- a/devlog/_plan/260827_bug_pr_merge_round/003_disposition_matrix.md +++ b/devlog/_plan/260827_bug_pr_merge_round/003_disposition_matrix.md @@ -9,7 +9,7 @@ | #2684 | Azure Model Router function schemas | **L1** | 92 lines, self-contained, host-scoped to Azure endpoints, reuses the existing Zen flattening helper, ships a negative test proving non-Azure targets are untouched. Fix the label/enforce-target hygiene, then merge. | | #2639 | backfill status and created_at | **L3** | `status` half is correct; `created_at` breaks `tests/server-combo-failover-e2e.test.ts:1323` (proven, see 002). Cherry-pick `status`, hold `created_at`. | | #2647 | Command Code reasoning presets | **L3** | content is three table rows and is fine, but the branch conflicts with dev on `src/providers/command-code-efforts.ts` and its test rewrites a catalog snapshot count 51 -> 60 that must be re-verified live. Re-apply the rows on a dev-based branch. | -| #2690 | normalize xAI Responses root tool schemas | **L3** | 926/289 across 8 files including a 430-line new module and a 269-line deletion from `openai-chat.ts`. Overlaps #2684 conceptually. Take the xai-tool-schema normalization; leave the openai-chat refactor out of a bug round. | +| #2690 | normalize xAI Responses root tool schemas | **L4** | 926/289 across 8 files. Reclassified from L3 at audit round 2: the fix imports the extracted module, so "fix minus refactor" is incoherent (007, finding 9). It also conflicts with the now-merged #2684 on `openai-chat.ts`. Either rebase and land whole, or reimplement against the existing helper. | | #2663 | bridge code-mode helpers through exec | **L2** | 528/68 across 12 files with substantive tests, CI green, compiles. Too broad to merge as a single review-required commit on its own head; land squashed with a written summary and close. Sequence before #2694. | | #2694 | SenseNova bare exec_command wrapper | **L4** | does not compile: 5 tsc errors, one call to a function that does not exist (see 001). Gate keys on a provider id absent from the registry. Reimplement minimally after #2663, or close as NOOP if #2663 subsumes it. | | #2638 | close drain routing follow-ups | **L4** | 1341/119 across 7 files, hygiene FAIL, enforce-target FAIL, CHANGES_REQUESTED. Touches `src/server/responses/core.ts` and subagent fallback — shared runtime. Rewrite the actual routing fix minimally. | diff --git a/devlog/_plan/260827_bug_pr_merge_round/007_audit_round2.md b/devlog/_plan/260827_bug_pr_merge_round/007_audit_round2.md new file mode 100644 index 0000000000..ed5871dc4c --- /dev/null +++ b/devlog/_plan/260827_bug_pr_merge_round/007_audit_round2.md @@ -0,0 +1,61 @@ +# A-gate audit round 2 — full reviewer verdict + +The independent explorer auditor returned VERDICT: FAIL with eleven numbered findings. +Findings 1-4 reproduce the plan's headline claims exactly and confirm four lane +assignments. Findings 5-11 are corrections. All are accepted; 5, 6, 7 and 8 were +already folded into 005 before the verdict arrived. The remaining four are here. + +## Finding 6 — the CI mechanism in 001 was wrong + +001 said the full matrix "only appears on maintainer-authored PRs in this round +(#2672, #2674)." False: + +``` +$ gh pr view 2639 --json author,statusCheckRollup +bet4it checks=27 +``` + +#2639 carries all 27 checks and `bet4it` is not in `MAINTAINERS.md`. +`.github/workflows/ci.yml` is `pull_request: {}` with no draft gating, so authorship +is NOT the discriminator. The conclusion stands — #2694's five checks never compile +anything — but the stated mechanism was invented. What actually distinguishes the +two sets is not established here, and should not be guessed at again. + +## Finding 9 — #2690's fix is NOT separable from its refactor + +020 planned to "take the xAI root-schema normalization and leave the +`openai-chat.ts` extraction." The auditor showed that is incoherent: the fix lives in +`src/adapters/openai-responses.ts`, which imports `isXaiSchemaTarget`, +`normalizeXaiToolParameters` and `XaiToolSchemaCompatibilityError` from the new +`./xai-tool-schema` module — the extraction IS the module the fix depends on. + +Confirmed on dev: `src/adapters/openai-chat.ts:956` still defines its own +`isXaiSchemaTarget`, used at line 1509. Taking the fix without the extraction leaves +two divergent copies of the xAI schema logic. + +So #2690 is not an L3 cherry-pick of "fix minus refactor". Either it lands whole +(now conflicting with the already-merged #2684 and needing a rebase), or the fix is +reimplemented against the existing `openai-chat.ts` helper. That is an L4 decision, +not an L3 one. + +## Finding 10 — #2671's blocker was understated + +010 said the review wants one added test. Two reviewers raise a second issue: the +added comment claims the capability was "probed 2026-08-26" while the PR carries no +probe evidence, and `registry.ts` already documents a convention against adding ids +on family resemblance — because a route that silently strips an image returns 200 and +answers about an image it never saw. + +Status: #2671 was merged in wp2 with the requested regression test added +(commit 4a4df12f2, proven load-bearing: 6 fail without the registry line, 6 pass with +it). The probe-evidence question is NOT resolved by that test. It is recorded here as +an open provenance question on a merged change, to be answered by a live probe against +Zen Go rather than by another code reading. + +## Finding 11 — core.ts is a five-way serialization constraint + +`src/server/responses/core.ts` is touched by #2694, #2690, #2663, #2638 and #2497. +Every merge into it invalidates the compile and test evidence gathered for the other +four. 006 listed the contention; this makes the consequence explicit: after ANY of +those five lands, the merged-tree gate must be re-run for each remaining one before it +is treated as clear. Evidence for these five expires on every merge.