[WRONG BRANCH] promote: dev to main for v2.29.0 - #2284
Conversation
docs(devlog): record the v2.28.0 release and its CI evidence
… replay cache
- Freeform/custom tools (such as default_api:exec) are emitted to clients as
custom_tool_call with { input: '{"cmd":...}' } while upstream observation
records parsed function arguments { cmd: ... }.
- applyAntigravityReplay now unwraps { input: string } to match against observed
JSON argument signatures when exact matching misses, fixing 400 errors during
early turns and replayed freeform execution (#2125).
…ut length pre-parse - Restore call.signature when call && matchedKey (not gated on ck), ensuring whitespace-padded wrapped arguments whose ck overflows 64 KiB still restore their signature if the parsed inner JSON is within bounds. - Bound argsObj.input.trim() to REPLAY_MAX_CANONICAL_ARGS_BYTES before calling JSON.parse in the custom tool replay unwrap, preventing oversized remote payloads from triggering unbounded allocations. - Add regression tests for oversized whitespace-wrapped input restoring correctly and oversized JSON payloads being rejected before parse (addressing review feedback from @Ingwannu on #2198).
…undaries - Compute trimmedInput and pass that exact validated string to JSON.parse, preventing large whitespace-padded prefixes/suffixes from reaching JSON.parse. - Add parse seam assertions proving that trimmed small payloads are parsed directly and that oversized valid payloads never reach JSON.parse (addressing feedback from @Ingwannu on #2198).
- Guard trimmedInput.length <= REPLAY_MAX_CANONICAL_ARGS_BYTES before calling utf8.encode(trimmedInput), preventing large ASCII/serialized input from allocating proportional Uint8Array buffers on the request path. - Add regression test asserting that oversized inputs are rejected before TextEncoder.encode and JSON.parse (addressing review feedback from @Ingwannu and CodeRabbit on #2198).
docs: clarify ChatGPT account pool policy boundary
fix(responses): bound orphan call reordering work
…ence-and-compaction fix(google): match unwrapped freeform tool arguments in antigravity replay cache
…ention fix(responses): bound terminal guard delta retention
Hygiene blocked the PR for missing_regression_test; lock in the userMessageAction path for composer-2.5 and keep resumeAction on fast. Co-authored-by: Cursor <cursoragent@cursor.com>
composer-2.5 now routes tool continuations through the external path; keep native turn-step replay coverage on composer-2.5-fast. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep isCursorExternalWireModel for history/replay and apply the composer-2.5 userMessageAction exception only on trailing tool results. Co-authored-by: Cursor <cursoragent@cursor.com>
Prime Agent (PrimeIntellect) is not a Pi lookalike — it is the pi coding agent shipped under a different brand. Its package declares a `piConfig` block, and the agent derives both its config directory (`.prime/agent`) and its env prefix from that block alone; with no block the same code falls back to `@earendil-works/pi-coding-agent` and `~/.pi`. `models.json` is therefore the same contract Pi already reads, so this client reuses Pi's builder and summarizer verbatim rather than restating a schema that would drift. A test locks the two generated documents byte-for-byte so a future Pi-only change cannot quietly diverge. The one thing that could still differ is the path we own, and it does not: Prime keeps our entries under the same `providers.<id>` key. The only new behavior is path resolution, which honors `PRIME_AGENT_CODING_AGENT_DIR` (verified live: setting it moves which models.json `prime-agent model list` reads) and refuses relative overrides for the same reason MCode and ZCode do. Registered loopback-only. Prime's provider block does accept `headers`, so a dedicated admission header has somewhere to live, but remote credential wiring is deferred from this initial integration — the same stance OMP took. The English CLI reference for `--client` was two clients stale; it now names the current set. Translated copies are left for a docs sync.
Top-level help advertised "(10 clients)" while the export registry held eleven, so the first screen a user sees understated the supported set. The reason the suite could not catch it is the more useful half: the assertion in tests/cli-help.test.ts froze the same literal, so code and test stayed self-consistent and wrong while the registry grew. The test now derives the number from EXPORT_CLIENT_IDS.length. help.ts keeps a literal on purpose. It currently imports only node:fs, node:path, node:url and the pure-data src/cli/registry.ts; importing the export registry there would pull node:os and the whole client-export module onto the `ocx --help` path. The assertion is the cheaper place to hold the invariant.
…ists Adds the Prime Agent row to the destination table, updates the sentence that counts those rows so the page stays internally consistent, names Prime in the loopback-only list with its own reason (its provider block does accept headers; remote credential wiring is deferred), and documents PRIME_AGENT_CODING_AGENT_DIR and the relative-path refusal alongside the equivalent MiniMax paragraph. Also syncs the `--client` accepted-value list in the seven translated CLI references. An accepted-value list is correctness rather than translation: a stale one tells users a valid flag value will be rejected. Those lists were two clients behind, so they now name mcode and zcode as well as prime.
Two review findings, both about the same failure mode: stating a path as if it were fixed when an environment override can move it. The dashboard's Prime description hard-coded ~/.prime/agent/models.json even though PRIME_AGENT_CODING_AGENT_DIR redirects it — and the panel already renders the resolved path directly beneath that sentence, so the literal was both redundant and capable of contradicting what the user could see. All nine locales now name the default and the override. The CLI reference's canonical-destination table also stopped at dsh. Widening the advertised `--client` set in the previous commit is what made that gap reachable for mcode and zcode, not just prime, so all three rows are added with their own overrides and the relative-path refusal, plus a paragraph noting that the three are loopback-only and that Prime and Pi produce the same document from different destinations.
Syncing only the `--client` heading in the translated CLI references left each of those pages contradicting itself: the heading named mcode, zcode and prime while the flag table two lines below still stopped at dsh. That is worse than leaving both stale, and it was mine to fix. All seven translations now carry the full accepted-value list in both the heading and the flag table, plus mcode, zcode and prime rows in the canonical-destination table with each client's own environment override and the relative-path refusal, phrased in that locale's existing wording for the placeholder column. Verified by pattern rather than by eye this time: every locale, English included, now reports the same eleven-client list in both places and eleven destination rows.
ja, ko, ru and zh-cn each restated the loopback-only set as "Pi, OMP, Kimi and DSH", which was already wrong for mcode and zcode and became wrong for prime with this PR. English and the other three locales carry a generic statement instead — configs hold either a documented environment reference or a non-secret loopback placeholder — so these four now match it rather than growing a list that has to be edited every time a client is added. The Gajae sentence in the same paragraph enumerated the same set as a comparison; it now simply says the generated Gajae integration remains loopback-only, which is what the English text says.
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
…eries (#2254 rebased) (#2258) * fix(responses): keep compaction blobs on the backend that minted them A replayed compaction item carries an `encrypted_content` blob only its minting backend can decode, and Codex replays it on every later turn. Two paths modified or misrouted it, and because the item outlives the failure in the client transcript, both wedged the session until its history was cleared — the routed compaction turn the proxy itself drives replays the same item. Relay: `scrubOcxCompactionItems` treated every non-`ocx1:` blob as OpenAI's and forwarded it verbatim, with no check that the destination was the issuer. A session that compacted on a canonical route and then switched to a routed provider sent that blob to an upstream that could only answer "Could not decode the compaction blob". Native blobs now travel only to destinations that mint them — forward-auth routes, which relay the caller's own OpenAI credentials to the ChatGPT backend or a relay in front of it, and the official OpenAI API under key auth — and degrade elsewhere to the same opaque note the bridged parser uses. Backfill: the response-side exemption list named `compaction` alone, so `compaction_summary` and `context_compaction` received synthesized ids that the client stored and replayed as "modified from the compact response". That divergence was possible because the compact wire family was enumerated in three places; it is now one predicate in `src/responses/compaction.ts`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): stop reshaping reasoning items that carry encrypted_content Codex replays the reasoning item it received in the next request's input, and a backend that issued `encrypted_content` verifies what comes back. The content-to-summary channel rewrite deletes `content` and substitutes a synthesized `summary`, so the client stored and replayed an item the issuer had never sent, and every later turn failed with "Could not decrypt the provided encrypted_content. Ensure the value is the unmodified encrypted_content from a previous response." No route change is needed to reach this: it fires on the second turn of a fresh session. The rewrite's replay round trip was verified against DeepSeek, which is `statelessResponses` and issues no blob — its reasoning replay goes through the proxy-side cache instead. Providers that do issue a blob joined the same route later through `preserveReasoningContentModels`, a flag whose own purpose is Chat-wire prompt-cache replay, and the verified premise did not follow them. Only the stored item is exempt. The `reasoning_text` delta events carry no blob and still route to the summary channel, so the expandable trace Codex renders for the live turn is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): drop a null reasoning content channel before routed passthrough Codex serializes an absent reasoning content channel as `"content": null`, and the sanitizer only acted on a non-empty array, so the null went to the wire verbatim. xAI rejects the item and blames the sibling field: {"code":"invalid-argument", "error":"Could not decode the compaction blob. Ensure it is unmodified from the compact response."} The blob is not the problem. Captured from a live failing request and bisected against it: replaying the body verbatim reproduces the 400, deleting only the `content` key returns 200, and setting it to `[]` also returns 200 — while removing `encrypted_content` instead fails schema validation, so the blob is both required and intact. The proxy was verified not to alter the blob: the value grok streamed to the client and the value replayed upstream matched in length, prefix and suffix, under identical `x-grok-conv-id`, `x-grok-session-id` and account. This bites the second turn of every Grok conversation — the first request that replays a reasoning item — which is why a fresh session fails just as reliably as a resumed one, and why the error looked like stale compaction state. The field is optional and null carries nothing, so the key is dropped rather than rewritten; an array content channel still follows the existing rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): decide native-blob relay by destination, not by forward auth Review found the discriminator unsound, and it was. `authMode === "forward"` describes local credential handling, not which backend answers: the adapter forwards caller credentials only to the canonical ChatGPT Codex surface, so a noncanonical forward provider receives none and may point anywhere. That produced both errors at once. A self-hosted or xAI-backed forward gateway was classified as able to decode a foreign blob, was sent it unchanged, and stayed wedged — the exact failure this branch exists to fix. Meanwhile a key-auth relay genuinely fronting OpenAI was classified as unable to decode and needlessly lost its compacted context. Relay is now positive only for the canonical surface, the exact official OpenAI API, or a destination whose operator opts in with the new `decodesNativeCompactionBlobs` provider flag. Verified that the flag survives config derivation and reaches the predicate, since the unit tests construct provider literals and would not have caught it being dropped there. Also corrects a stale line in the transport notes: compact-wire items are not exempt from the `store: false` item-id strip. That exemption was deliberately reverted to match codex-rs (`core/src/client.rs:918-925`). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(responses): stop asserting a disproven cause for the blob-preservation guard The guard is sound, but its comments claimed it fixed Grok's `Could not decrypt the provided encrypted_content` failure. Live bisection disproved that: Grok emits summary-channel reasoning natively, so `reasoningItemToSummaryShape` returns early and this rewrite never fires on that route. The real cause was `"content": null` on the replayed reasoning item, fixed separately. A false causal claim in a comment is worse than none — the next reader trusts it. The rule is restated on its own terms: an item carrying opaque provider state should not have its stored shape changed unless that backend has an explicit replay contract, which is why DeepSeek was safe and why the Kimi/GLM/NeuralWatt routes now on `preserveReasoningContentModels` are the ones this actually guards. Comments and prose only; no behaviour change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): scope the null-content strip to routed destinations The first version stripped `"content": null` from every reasoning item, which broke OpenAI. Caught in live traffic minutes after deploying it locally: 400 invalid_request_error The encrypted content k7pQ...Px7D could not be verified. Reason: Encrypted content could not be decrypted or parsed. An OpenAI-operated backend binds the blob to the item's exact shape, so removing a field invalidates it. The two requirements are exactly opposed: xAI refuses the null key, OpenAI needs it kept — so the strip has to follow the destination. The predicate is deliberately not `authMode === "forward"`. A noncanonical forward provider never receives the caller's credentials, so forward auth says nothing about which backend answers; only the canonical ChatGPT surface and the official OpenAI API are treated as OpenAI-operated, and a self-hosted relay is routed like any other gateway. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(xai): restore Grok Responses tool compatibility * fix(responses): address namespace review findings * fix(responses): close the remaining private-shape leaks on the routed boundary The namespace boundary lowered complete groups but still let several Codex-private shapes reach a strict gateway, each reproducing the pre-inference rejection the boundary exists to prevent. No `type: "namespace"` value survives now. A group the layer cannot express — empty, nested, or with an unusable child name — is dropped along with the children it cannot represent. Relaying the private shape costs the whole request rather than one tool, so "preserve rather than lose a tool" was losing strictly more. Replayed call items are lowered whether or not this turn declares the group they name. The routed compaction turn strips the entire tool surface before the boundary runs, so every compaction after a namespaced tool call shipped the private `namespace` key this layer's own restoration had stamped on the item. Only tool_choice resolves a bare name through the catalog: a history item records which tool actually ran, so re-pointing it at a same-named namespace child would rewrite that record on a coincidence rather than translate it. Codex-private tool fields now come from one table instead of one bespoke pass each, and it gains `defer_loading` — `activateDeferredTool` clears that only for tools a `tool_search_output` already loaded, so the first turn of a deferred catalog carried it to the wire — and the `web_search_preview` variant. A bare declaration and a `functions` child of the same name are one logical tool: `buildTools` flattens the reserved group without a namespace, the parser tolerates the duplicate, and `promoteClientLoadedTools` produces it. That shape raised a wire-name collision that escaped every catch up to the Bun handler, so an ordinary catalog became an unstructured 500 with no request log — while the rotation-rebuild path answered 400 for the identical throw. It is now deduped, and a genuine collision is a typed error the passthrough maps to 400. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): drop reasoning blobs and output-only status across a route switch Switching models mid-conversation broke the next turn. Reproduced end to end through the proxy: mint a reasoning item on xai/grok-4.6, replay to openai/gpt-5.6-sol. replay grok -> grok : OK replay grok -> SOL : Unknown parameter: 'input[1].status' ... status removed: replay grok -> SOL : The encrypted content ZvQ+...fBJg could not be verified. ... status and encrypted_content removed: replay grok -> SOL : OK Two independent problems. Grok emits an output-only `status` on reasoning items that OpenAI rejects on input, and a reasoning blob is decodable only by the backend that minted it, so after a switch the client replays blobs the new destination cannot read. This extends the mechanism the repo already uses for opaque provider state rather than adding a retry: `reasoning-replay-cache` already keeps a bounded, thread-scoped store and already computes the provider/destination/adapter/model/ credential identity. It now also records which identity served a thread last, and a request whose identity differs from that record drops `encrypted_content` from replayed reasoning items before they go out. No record — fresh process, evicted, expired, no client thread — keeps the blobs rather than discarding valid cached reasoning on a guess; that leaves a switch spanning a proxy restart uncovered, which the comment states rather than implies. `status` is stripped only from items that are not forwarding a blob. An OpenAI-operated backend binds the blob to the item's exact shape, so removing any field from an item we still expect it to decode can invalidate it — the same failure an unconditional `content` strip already produced once on this codebase. Content blanking predates that invariant and is unchanged; an item carrying both a native blob and raw content is a known unresolved conflict, noted in place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): make namespace dedup order-independent and restore custom calls by wire identity Review found two defects in the flattening layer; both are fixed here. Deduplication depended on declaration order. A bare declaration and a `functions` child of the same name are one logical tool, but which one owned the wire name — and therefore which one was emitted — followed whichever container the rewrite reached first. The plan now records the bare wire names from the complete catalog and the bare declaration always wins, so the same catalog flattens identically whichever container declares it. Custom-call restoration used the wrong coordinate. A custom tool inside a non-`functions` namespace is lowered twice on the way out (custom to function, then renamed to `<ns>__<name>`), while on the way back namespace restore runs first and replaces the wire name with the bare one. Custom restore then matched that bare name and could convert an unrelated same-named function call, sending Codex a `custom_tool_call` with the wrong payload shape. Converted custom tools are now tracked by their final upstream wire name, and restoration reconstructs that identity from the `{namespace, name}` an earlier rewrite restored. A namespaced custom and a namespaced function sharing a child name now round-trip to their own item types, on both the JSON and SSE paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): compare the serving identity on rotation-safe dimensions The serving-identity record compared `credentialIdentity`, which for OAuth is `accountId + generation` and therefore changes on every token refresh. Six of the eight `bindRouteReasoningReplayScope` call sites are key-rotation or OAuth-refresh rebinds, so an ordinary refresh registered as "the backend changed" and the next turn on that thread dropped a valid blob. Key-pool providers would have paid that repeatedly, and silently — nothing errors, the model just loses cached reasoning. The module already distinguishes the durable dimensions for exactly this reason (#1926: the rotating generation deliberately does not participate). The serving record now compares `providerDestinationDurableIdentity` and `credentialDurableIdentity`, and refuses to record at all when those are missing rather than falling back to the volatile pair: a missed strip costs one degraded turn, a spurious strip is a permanent quality regression. The proxy-owned replay cache keeps its stricter key, which is deliberate. Also documents two behaviours that would otherwise read as bugs: a combo that rotates targets between turns legitimately drops blobs while the SSE model-name rewrite hides the switch from the client, and the image/web-search loops consume the replay scope without rebinding, which is what stops an internal small-model call from poisoning the record for the main conversation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): recover when an upstream rejects foreign opaque state The thread-scoped serving-identity record strips replayed blobs deterministically, but it is in-process and bounded, and it deliberately keeps blobs when it has no record — stripping on "unknown" would discard valid reasoning after every restart. That leaves a failure users hit routinely. From the live usage log, one conversation: 19:33:31 xai grok-4.6 200 <- last grok turn 19:38 proxy restarted (records wiped) 19:48:11 openai gpt-5.6-sol 400 "The encrypted content Py6J...kwW9 could not be verified. Reason: Encrypted content could not be decrypted or parsed." The proxy never served the turn that minted those blobs, so it cannot know they are foreign. TTL expiry, LRU eviction and any transcript older than the process open the same hole. Register a recovery kind rather than invent a retry path: `image-413` already reacts to an upstream rejection by rebuilding the body once and refetching inside the recovery loop, with a single-attempt guard. This adds `opaque-blob-rejection` on the same shape, triggered only by a decoder's own 4xx identity — OpenAI's nested `invalid_encrypted_content`, or xAI's two concrete decoder messages — and only when the exact outbound body still carried a blob, so an unrelated `invalid-argument` never gains a hidden resend and a blobless body never triggers an identical resend. The deterministic pre-flight stays primary: when a record exists the first request is already correct and this never runs. Cost when it does run is one extra round trip and one turn of degraded reasoning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): compare serving identity for compaction blobs too `scrubOcxCompactionItems` forwarded any non-`ocx1:` blob whenever the destination could decode native blobs. That is sound only if native blobs have a single minter, and they do not: xAI mints them as well, so an xAI-minted compaction blob replayed to an OpenAI-operated destination was forwarded verbatim and rejected. Reproduced against the live proxy on a thread whose serving identity had already changed and was known to have changed — the reasoning path stripped correctly while the compaction item sailed through: POST /v1/responses model=gpt-5.6-sol, thread last served by xai/grok-4.6 input: [{"type":"compaction","encrypted_content":<opaque non-ocx blob>}, ...] -> 400 invalid_encrypted_content "The encrypted content rmey...SQ== could not be verified." Reuse the signal the reasoning path already consumes rather than recomputing identity in the adapter: on a known mismatch a native blob degrades through the existing `compactionItemToText` note instead of being forwarded. With no known mismatch, behaviour is unchanged. This covers threads the process has served. A cold record — after a restart, TTL expiry or eviction — still forwards, which is a separate change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): strip output-only reasoning status unconditionally The cross-backend fix kept `status` on any reasoning item that forwarded its `encrypted_content`, to honour "an item whose blob is forwarded is not otherwise modified". That invariant was defensive rather than observed, and it broke the cold-record recovery path. With no provenance record — after a restart, TTL expiry or eviction — the blob is retained, so `status` is retained too, and OpenAI rejects the request on the field before it ever validates the blob: 400 Unknown parameter: 'input[1].status'. The opaque-blob recovery correctly does not match that error, so the conversation stayed broken. Measured against the live backends: - OpenAI never mints `status` on a reasoning item (keys are content, encrypted_content, id, summary, type), so the retain branch could only ever fire for an item minted elsewhere — the exact item OpenAI then rejects. It never protected an OpenAI-minted item. - Grok accepts its own 1707-char blob with `status` removed: 200. - With `status` removed, that same item replayed to gpt-5.6-sol returns 200 and the usage log records sendCount=2, recoveryKinds=['opaque-blob-rejection'] — removing the field is what lets the request reach the blob check the recovery is armed for. The `content` rule is untouched: blanking predates this and is required by ChatGPT's input contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(responses): converge the two opaque-blob recovery call sites Two units landed separately and left duplication. The recovery unit was written on a branch without the compaction-provenance change, so it degraded compaction items itself by rewriting `parsed._rawBody.input` in place. Once both are merged that walk is redundant: it sets `_stripReasoningEncryptedContent`, which is exactly the signal the adapter's own compaction scrub consumes. Verified rather than assumed, since the two call sites rebuild through different adapters. Both reach `openai-responses` (the recovery predicate restricts to it), whose `buildRequest` consumes `_rawBody` and runs `scrubOcxCompactionItems`; the native passthrough site resolves a passthrough retry adapter, the generic site rebuilds through the retained `activeAdapter`. So the manual walk changes no outbound body on either path, and dropping it removes a mutation whose side effect outlived the request. The native Responses branch returns before the generic `recovery:` loop, so the recovery block was also written out twice. Whoever next adds a recovery kind to the generic loop would not know a second loop exists. Extract the shared predicate, guard, preparation, body cancellation and rebuild into one `attemptOpaqueBlobRecovery` helper both sites call, each keeping its own control flow and its site-specific rebuild — the generic one still invalidates the same-target request. Cross-reference comments on both loops name the other. No outbound behaviour changes. Existing recovery tests are untouched; added coverage for routed compaction recovery through the generic loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(responses): pin that the first send already drops reasoning status The recovery suite asserted the resend well but said nothing about the first outbound send beyond "it carries a blob". That the first send has `status` already stripped is load-bearing: the recovery is armed for the upstream's blob-rejection error, and if `status` survives, OpenAI answers 400 Unknown parameter: 'input[1].status'. before it validates the blob. The recovery correctly does not match that error, so it never fires. That exact regression shipped once — `stripOutputStatus` was gated on the item not forwarding its `encrypted_content`, which is precisely the cold provenance case — and the entire suite stayed green while the live path was unchanged. Assert the first send's reasoning item by shape: blob present, no `status`. Verified the guard bites: reintroducing the old condition turns this test red, where before it left the suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: olddonkey <olddonkeyblog@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#2250 rebased) (#2259) * fix(integrations): honor OFF for Claude Desktop drift and Grok ensure Claude Desktop disable refused owned gateway_drifted profiles (including a missing appliedFingerprint), so leftover Claude-3p configs could not be turned off and looked like they needed an update. Grok ensure still called syncGrokConfig unconditionally, rewriting ~/.grok after updates even when the durable switch stayed off. Gate ensure on desired state, clear Desktop residue when OFF, and stop treating OFF leftovers as stale applies. * test(integrations): cover desired-off gateway drift status Keep leftover owned drift and assert the status route reports residue, not a stale apply. * fix(integrations): re-read desired state before ensure mutations A toggle during ensure's probe/start window could make the stale snapshot strip a freshly enabled Grok fence or delete a freshly applied Desktop profile. Re-read persisted desired state immediately before each file write, and show leftover desired-off Desktop gateways as stale/cleanup-pending instead of absent. * fix(integrations): close ensure desired-state race --------- Co-authored-by: lilinxiong <lilinxiong1997@gmail.com>
…only table (#2262) The #2258 series unconditional strip collided with the #2238 capability contract: official OpenAI API-key traffic lost external_web_access (lidge full suite 1/14025 fail). The table row is now capabilityGated, active only when supportsOpenAiWebSearchToolFields === false; defer_loading stays unconditional. 387/0 across six suites.
…2266) * feat: add xAI Responses opt-in switch * docs(pr): screenshot asset for the responses opt-in switch
Every required publisher-key ACL harden failure reached CI as one fixed string, "public publisher key ACL hardening did not complete". The discriminator existed only on `cause`, which the test reporter does not print, so the three causes that occur on the Windows leg -- ETIMEDOUT (the budget), EACLIDENTITY (the effective-SID lookup) and EICACLS (icacls refusing the path) -- were indistinguishable from a log, and each needs a different fix. Append the bounded errno-shaped code to the message. Only the code crosses that boundary, re-checked for shape here rather than trusted, so a pathname or username component cannot reach a public log through it. A cause without such a code keeps the previous message unchanged. Refs #2152.
Co-authored-by: Ingwannu <ingwannu@users.noreply.github.com>
…ngs (#2264 rebased) (#2273) * fix(responses): address review findings on the native passthrough lane Three defects found in review of the Grok Responses series, plus one stale comment. All confirmed against the code before fixing. **One gate used the wrong predicate.** Custom-tool lowering was gated on `provider.authMode !== "forward"` while every neighbouring gate uses `!isCanonicalOpenAiForwardProvider`. A noncanonical forward provider therefore skipped `rewriteRoutedCustomToolsForUpstream` but still ran namespace lowering, so a namespace child that was a custom tool got promoted while keeping `type: "custom"` and the gateway rejected it. This repeats the mistake the same series documented elsewhere: forward auth says nothing about which backend answers, because a noncanonical forward provider never receives the caller's credentials. Both sides move together — the adapter's lowering gate and core's converted-name collection — since lowering names without restoring them is worse than not lowering at all. **The OpenAI-operated classifier missed a legitimate base-URL form.** It compared the normalized base URL for exact equality with `https://api.openai.com/v1`, so a provider configured as `baseUrl: "https://api.openai.com"` with `responsesPath: "/v1/responses"` reaches the official endpoint yet was classified as routed. That is not cosmetic: routed classification drops `content: null` from OpenAI-minted encrypted reasoning and degrades native compaction blobs — this series' own regression, in reverse. Both official forms are now accepted, still by exact normalized match so a lookalike host cannot qualify. **Request rebuilds left the namespace alias map stale.** Every recovery rebuild replaces `request` without refreshing the alias map the response path uses to restore private tool names, so a rebuild that changes the lowering decision restores against a stale map. Refreshed from the rebuilt request on every path that replaces it — the pre-existing OAuth-401 and image-413 rebuilds included, since the bug is in the rebuild pattern rather than in one caller. **`_stripReasoningEncryptedContent` is no longer only a route-switch flag.** It is also set when an upstream rejects opaque state of unknown provenance. The comment now names both producers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): record the serving route only after it serves `updateReasoningReplayServingIdentity` compared and committed in one call, and `bindRouteReasoningReplayScope` calls it before the request goes out — so the candidate destination was recorded whether or not that request ever completed. turn 1 -> A succeeds record = A turn 2 -> B: A != B, strip A blobs record = B (committed too early) ... this request then fails (rate limit, transport, 5xx) turn 3 -> retry B: B == B, no strip but the transcript still carries A-minted blobs -> rejected The opaque-blob recovery rescues turn 3, so this degraded rather than broke: one wasted round trip and one turn of degraded reasoning on a path meant to be deterministic. The record's meaning was the defect — it should mean "this destination served this thread", and a request that never completed served nothing. Split the call in two. `reasoningReplayServingIdentityChanged` compares without writing; `commitReasoningReplayServingIdentity` records, and runs only at a successful terminal response. Bounded discipline is unchanged: same LRU/TTL and byte accounting, same refusal to record without a durable identity dimension, same fail-soft direction where no record still means keep the blobs. For bridged transports a terminal means `completed` or `incomplete`. For streamed passthrough it means a non-error upstream status before relay starts: waiting for SSE completion would retain request state for the stream's lifetime, and a later body failure does not undo that the destination accepted and served the turn. That boundary is stated in the code rather than implied. The two post-recovery re-records are gone — a successful recovery now reaches the same terminal commit as any other success. Regression test: A succeeds, an A->B turn strips and then fails, and the next B request for the same thread still strips. Verified it fails against the old code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(responses): classify the resolved Responses endpoint, not the base URL isOpenAiOperatedResponsesDestination() matched on the base URL alone, so a provider with baseUrl "https://api.openai.com" and a custom responsesPath such as "/other" was classified as OpenAI-operated even though the adapter posts that request to a non-Responses endpoint. That preserved OpenAI-only null-content and reasoning semantics for a destination that never sees the official Responses API. Resolve the effective endpoint with the adapter's own construction rules — a configured responsesPath is appended verbatim, only the default branch runs the /v1/responses suffix normalization — and require an exact normalized match on https://api.openai.com/v1/responses. The conventional /v1 base and the bare official origin still classify; lookalike hosts still do not. Adds negative regressions for a custom non-Responses path on both official base forms, plus positive coverage for the bare origin default and an explicit /responses path. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017zpLCh4eEms6un3VjapRgL --------- Co-authored-by: olddonkey <olddonkeyblog@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… caching (#2274) Agent-Generated-By: Codex Co-authored-by: leon80900 <80900400+leon80900@users.noreply.github.com>
* fix(integrations): sync MCode model capabilities * fix(mcode): drop the guessed effort ladder from the doc example and pin the unlimited count - The minimax.md example showed thinking.effortOptions on anthropic/claude-opus-5, but the catalog has no authoritative reasoning-effort ladder for it (the low..max ladder is the Cursor adapter's map, not catalog data). Keep limit.context, drop thinking. - modelsWithoutLimits in the MCode route test now asserts exactly 2, matching the two fixture models without context limits (a/m2 and b/no-context). * fix(integrations): refresh owned MCode on sync * fix(integrations): serialize MCode sync writes --------- Co-authored-by: 弘扬 <hylouis@stu.xmu.edu.cn>
…ion (#2054 rebased) (#2277) * fix(cursor): reuse conversation checkpoints for incremental continuation Preserve Cursor's returned ConversationStateStructure after a successful no-tool turn and reuse that snapshot on validated linear continuations instead of rebuilding the full root history. Tool-result turns reuse the last completed checkpoint plus only the uncovered suffix. Compaction, helper/shadow isolation, account or model mismatch, missing refs, decode failures, and invalid_argument recovery keep the existing full-replay path. Bind checkpoint snapshots to conversation, credential identity, and model affinity. Keep an opaque process-local checkpointRef on Responses continuation state, pin referenced blobs for the checkpoint lifetime, and never treat OpenCodex usage as a cache-hit counter. Refs #1527 * fix(cursor): pin store:false chat hops and helper-owned cache Chat Completions / Codex Sol hops often omit previous_response_id and thread headers, so every hop minted a new conversation and missed the checkpoint store. Pin those hops to the first user text and reuse the live snapshot. Isolated helpers keep their own cache and stay off the parent thread. Refs #1527 * fix(cursor): keep recovered checkpoints and close review holes Do not invalidate the checkpoint just committed during forced-fresh recovery. Invalidate the inherited ref, including compaction leftovers. Pin checkpoint blobs atomically, collect nested subagent blob ids, and keep suffix replay off the system prompt. Refs #1527 * fix(cursor): fail closed on stale refs and prefix collisions An explicit missing checkpointRef now expires instead of picking another snapshot. Ref-less Chat hops look up only a unique covered-prefix plus system digest. Identical first prompts no longer share a conversation. Expired snapshots are pruned by an unref timer, not the next request. Refs #1527 * fix(cursor): frame checkpoint digests and drop stale recovery state Length-prefix instruction and prefix hashes so delimiter splits cannot collide. Clear the failed transport before forced-fresh retry so the recovered turn cannot commit the previous attempt. The idle TTL test now pins a real blob and asserts the lease is gone after prune. Refs #1527 * test(cursor): distinguish stale refs from ref-less fallback --------- Co-authored-by: keepitmello <71975659+keepitmello@users.noreply.github.com>
…der config Routed Grok turns on the Responses lane died with `400 Argument not supported: external_web_access` before inference. routedProviderConfig() backfills every other registry-only scalar (supportsServiceTier, preserveResponsesReasoningContent, fastWire) but not supportsOpenAiWebSearchToolFields. enrichProviderFromRegistry() does fill it, and the request path never calls that function -- so a saved xai row reached the Responses adapter with the flag undefined. The #2262 capability gate reads undefined as "unclassified upstream, keep the fields", so Codex's OpenAI-only web_search config went to the wire and xAI rejected the whole request. Live probe against the OAuth Grok endpoint (2026-08-21) isolates the cause: bare {type:"web_search"} returns 200, +external_web_access returns 400, and +search_context_size returns 400 -- individually, before inference. Verified end-to-end on a remote macOS host running this dev head: with the GUI Responses opt-in on and NO hand-written capability in config.json, a multi-step codex exec tool-use turn now completes over adapter "openai-responses" with status 200, where the same turn 400'd before. The existing tests could not catch this: they hand-build a provider with the flag already set, or call enrichProviderFromRegistry() directly, so both start downstream of the break. The new tests assert on routedProviderConfig() output and were driven red against the unfixed router.
…-capability fix(router): backfill the xAI web-search capability into routed provider config
|
✅ Deterministic PR hygiene checks passed. |
⏳ DRAFT
What to do
Its title has been prefixed with |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (275)
📝 WalkthroughWalkthroughThis PR bundles many concerns: extensive devlog/docs updates; a new Prime Agent client (GUI, CLI export, integration registry); unified sidecar authentication and candidate selection feeding new xAI/Gemini/Exa web-search executors and an xAI Responses opt-in switch; Cursor conversation checkpoint reuse; Responses namespace-tool, reasoning-replay, and opaque-blob-recovery compatibility work; Google adapter response validation; and smaller fixes to OAuth logging, ACL error codes, and OpenAI-chat EOF handling. ChangesDocumentation
Estimated code review effort: 2 (Simple) | ~10 minutes GUI
Estimated code review effort: 3 (Moderate) | ~25 minutes Sidecar/Web-search backend unification
Estimated code review effort: 4 (Complex) | ~60 minutes Responses server compatibility
Estimated code review effort: 5 (Critical) | ~120 minutes Cursor adapter checkpoint continuity
Estimated code review effort: 5 (Critical) | ~100 minutes Google adapter hardening
Estimated code review effort: 4 (Complex) | ~40 minutes Client export and integration lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Small provider, security, and streaming fixes
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ResponsesCore as src/server/responses/core.ts
participant CursorAdapter as src/adapters/cursor.ts
participant CheckpointStore as checkpoint-store.ts
participant Upstream as Cursor upstream
Client->>ResponsesCore: POST /v1/responses (tool-result turn)
ResponsesCore->>CursorAdapter: runTurn(parsed request)
CursorAdapter->>CheckpointStore: getCursorCheckpointForPrefix(digests)
alt checkpoint valid
CheckpointStore-->>CursorAdapter: CursorCheckpointSnapshot
CursorAdapter->>Upstream: request with checkpointBytes + suffix
else checkpoint invalid or missing
CheckpointStore-->>CursorAdapter: invalidation reason
CursorAdapter->>Upstream: full-replay request
end
Upstream-->>CursorAdapter: conversationCheckpointUpdate frame
CursorAdapter->>CheckpointStore: commitCursorCheckpoint(bytes)
CursorAdapter-->>ResponsesCore: done event with checkpointRef
ResponsesCore-->>Client: response with provider continuation state
sequenceDiagram
participant CLI as ocx agent sidecar --list
participant ManagementAPI as agent-settings-routes.ts
participant SidecarAuth as src/sidecar/auth.ts
participant Backends as web-search/backends.ts
CLI->>ManagementAPI: GET /api/sidecar-settings
ManagementAPI->>SidecarAuth: resolveSidecarAuth(config)
SidecarAuth-->>ManagementAPI: SidecarAuthState (Codex/Anthropic slots)
ManagementAPI->>Backends: webSearchSidecarCandidates(config, auth, all)
Backends-->>ManagementAPI: eligible candidates per active backend
ManagementAPI-->>CLI: webSearchModels options
CLI->>ManagementAPI: PUT /api/sidecar-settings {backend, model}
ManagementAPI->>Backends: webSearchModelIsRejected(backend, model, candidates)
alt model rejected
ManagementAPI-->>CLI: 400 with allowed models
else model accepted
ManagementAPI-->>CLI: 200 persisted settings
end
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0498a05223
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const effectiveBackend = body.webSearch.backend === "anthropic" | ||
| ? "anthropic" | ||
| : body.webSearch.backend === "openai" || body.webSearch.backend === null | ||
| ? "openai" | ||
| : config.webSearchSidecar?.backend ?? "openai"; |
There was a problem hiding this comment.
Validate every requested web-search backend directly
When the dashboard selects an offered xAI or Gemini model, it sends both the model and its backend, but this ternary recognizes only anthropic and openai; xai, gemini, and exa therefore fall back to the previously stored backend—normally openai—and webSearchModelIsRejected returns 400 for the otherwise valid pair. The same narrowing occurs for Claude overrides in agent-settings-routes.ts; accept every validated backend-union value as the effective backend in both routes.
Useful? React with 👍 / 👎.
| updateFramed(hash, cursorInstructionDigest(parsed)); | ||
| for (const message of parsed.context.messages.slice(0, coveredMessageCount)) { | ||
| updateFramed(hash, message.role); | ||
| updateFramed(hash, contentToText(message.content)); |
There was a problem hiding this comment.
Include tool-call metadata in checkpoint lineage hashes
When covered history contains tool calls, changing a call's id, name, namespace, arguments, or paired result metadata without changing its visible text leaves this digest unchanged: contentToText deliberately discards assistant toolCall parts and reduces tool results to content alone. lineageMismatch can consequently accept a stale checkpoint and omit the changed exchange from the suffix, giving Cursor provider state from a different tool execution; hash the complete normalized wire representation, including tool-call and result metadata.
AGENTS.md reference: src/AGENTS.md:L17-L20
Useful? React with 👍 / 👎.
| // boundary, leaving an unscrubbable key prefix in the surviving text. | ||
| return { text: "", sources: [], error: `exa sidecar HTTP ${res.status}: ${scrub(t).slice(0, 200)}` }; | ||
| } | ||
| const payload = await res.json().catch(() => null); |
There was a problem hiding this comment.
Bound Exa response bodies before parsing
If Exa or an intermediary returns an oversized body, res.json() buffers and parses the entire response without a byte limit; the adjacent error path likewise uses unbounded res.text(). A malformed upstream response can therefore consume unbounded memory and stall or terminate the proxy instead of degrading to a sidecar error. Read both success and error bodies through the existing bounded-body helper and reject payloads over MAX_SIDECAR_RESPONSE_BYTES.
AGENTS.md reference: src/AGENTS.md:L15-L19
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
opencodex/src/web-search/exa-executor.ts
Line 84 in 0498a05
When an Exa result title, URL, or snippet contains </web_search_result> followed by newlines, this interpolation closes the untrusted-data block created by formatWebSearchResult and places attacker-controlled page content outside the boundary; titles are also repeated raw in the subsequent Sources list. This is directly reachable from Exa result metadata/content, so sanitize control characters and neutralize boundary tags before constructing the digest, while validating citations through the existing safe-source helper.
opencodex/src/web-search/xai-executor.ts
Line 117 in 0498a05
When xAI or an intermediary returns an oversized 4xx/5xx body, res.text() buffers the entire response before the later 200-character slice, allowing a sidecar failure to consume unbounded memory instead of degrading normally. Read this path through readBoundedResponseBytes with MAX_SIDECAR_RESPONSE_BYTES, as the Gemini executor already does.
AGENTS.md reference: src/AGENTS.md:L17-L17
opencodex/src/server/responses/core.ts
Line 3499 in 0498a05
For a passthrough Responses stream that returns HTTP 200 and then emits response.failed, this commits the candidate route before the body reveals that it never served the thread. If route A previously served the conversation, route B fails this way, and the client retries A, the store now reports a B→A identity change and strips A's still-valid encrypted reasoning state. Defer the commit until terminal inspection observes response.completed or response.incomplete, rather than treating the HTTP status alone as success.
AGENTS.md reference: src/AGENTS.md:L17-L17
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Summary
Promote
devtomainfor the v2.29.0 release.mainlast moved for v2.28.0, which is already published (npmlatest= 2.28.0). This promotion carries the 83-commit unreleased train ondev: the #2188 sidecar/web-search unification (L1-L9), the xAI wire-policy series, and the 260821 bug-backlog train (#2269, #2271, #2246, #2273, #2274, #2276, #2277), plus the routed web-search capability backfill (#2283) that closes the half-landed path #2262 left open.Both branches carry unique commits, so this is a merge rather than a fast-forward:
mainkeeps its own release commits.Verification
bun run typecheck— exit 0bun run test— 14090 pass / 0 fail across 887 filesbun run privacy:scan— passeddevwith 23/23 required checks green, 0 failures.main..devdelta (two rounds): round 1 raised exactly one blocker — the unlanded capability backfill — and round 2 confirmed closure atorigin/dev7881319 with no new blocker. The audit found no workflow, release-automation, lockfile, or dependency change in the delta; the security-sensitive parts are sidecar auth eligibility and OAuth log redaction, both tightening.devhead: with the Grok Responses lane opted in and no hand-written capability inconfig.json, a multi-stepcodex exectool-use turn completed over adapter"openai-responses"withstatus: 200.origin/devis thepackage.jsonversion line, whichscripts/release.tsowns.Checklist
Summary by CodeRabbit
New Features
Bug Fixes
Documentation