[WRONG BRANCH] chore(release): promote 2.55.0-preview.20260914 to preview - #4617
Conversation
Owner-authorized maintainer integration into dev. Final documentation head 7b4d6ab has successful required checks; expensive product jobs were conditionally skipped for this documentation-only change. Review corrections distinguish issue 4429 from 4519 and archive the completed unit. Integrated implementation evidence remains dev run 34760250023 at cb2e15b.
…4545) Refuses an in-place restart when the CLI version differs from the running proxy, so a newer CLI no longer hands restart to an older server that then respawns its own binary and reports success. Adds the unknown-health-version regression alongside the placeholder case. Carries #4529 by Voyagerroc-Lab. Verification: local product suite, typecheck, build and install NOT RUN. Hosted Cross-platform CI run 34775280313 succeeded at a523f0f. Merged through maintainer self-integration per MAINTAINERS.md. Co-authored-by: Voyagerroc-Lab <328063293+Voyagerroc-Lab@users.noreply.github.com> Co-authored-by: Voyagerroc-Code <325343927+Voyagerroc-Code@users.noreply.github.com>
…end (#4548) sidecarSettingsForBridge read the model out of the global config.webSearchSidecar block without checking which backend that block was configured for, and src/server/responses/core.ts hands the block over whole. A global {backend: "openai", model: "gpt-5.6-luna"} therefore reached runAnthropicWebSearch whenever a provider set webSearchBridge.backend to "anthropic", and Anthropic rejects the model, so the bridge search failed. Same shape for xai and gemini. The global model now applies only when resolveSidecarBackend(sidecar.backend) equals the bridge backend; otherwise the bridge runs that backend's own default. An unset global backend still resolves to "openai", so an unset-backend model reaches an openai bridge and no other. Only the model is gated: reasoning is a generic effort level, and xSearch is xai-only with no per-backend default and no webSearchBridge equivalent, so gating it would make an openai sidecar plus an xai bridge plus x_search inexpressible. No credential crosses a backend, before or after this change. resolvePassthroughWebSearchBridgeAuth switches on the bridge backend and consults only that backend's credential locator. This is a model and settings defect. resolveSidecarBackend and WebSearchBackendId move from src/web-search/index.ts to src/web-search/sidecar-providers.ts, which exists precisely so the bridge can resolve a backend without value-importing the barrel; index.ts re-exports both, so every existing consumer is unchanged.
…4547) parseCatalogBuffer had arms for ClientModelConfig fields 1, 4, 18 and 22 and no default, so field 5 (supports_images) was dropped by omission. Carry it on ModelCatalogEntry as an optional boolean: a present true asserts text+image support, a present false asserts text-only, and an omitted field stays unknown. It deliberately does not copy the disabled pattern, which defaults to false — collapsing unknown into text-only was the #1796 regression, and antigravity-models.ts already implements the same tri-state for its discovered catalog. The header schema comment gains the #5 row and the #18 row it never listed, and its verification claim now says which fields came from the bundled extension.js, which from a live catalog dump, and which from the public WindsurfAPI documentation. The owning structure doc records the catalog pre-flight contract. Propagation of the flag to the client catalog is a separate change.
…nts (#4543) * fix(cursor): refund spare envelope bytes to clipped invocation arguments The 2 KiB per-call cap on the arguments named inside a replayed tool-result envelope is charged while the envelope is still being built, so it cost a call 2 KiB whether or not anything else wanted those bytes. In a small replay nearly the whole 192-root / 512 KiB envelope went unused and the cap still bit: a 4,693-byte successful write_file lost its tail inside a 6,011-byte replay, and because the result text does not repeat the argument, the model could no longer see what it had just written. Add a second pass after the root set is assembled and before it is stored. It spends only leftover aggregate bytes, newest tool result first, skips a root whose own output was already elided, and never drops, shrinks or reorders a retained root. The cap itself is unchanged and still decides admission on its 2 KiB prefix, so a 600 KiB argument stays clipped rather than evicting the output it describes. The gate is echoToolResultInRoot, not externalModel: native composer-2.5 echoes results into roots without being an external wire model, so the narrower gate would have left the one native model with clipped invocation lines capped for no reason. The widening uses the callback form of String.prototype.replace, because serialized arguments routinely contain $&, $' and $1, which the string form would expand into the surrounding match. Closes #4516 * fix(cursor): accept readonly rawMessages in the restoration pass request.rawMessages is readonly OcxMessage[]; the new second pass declared a mutable OcxMessage[] parameter, which strict typecheck rejects (TS4104). The pass only reads the array, so widen the parameter instead of copying. * fix(cursor): keep a collapsed root's run note through a rebuild An adversarial counter-read of the restoration pass found the real defect one layer down. pushDeduped builds the collapsed root's wire payload from the marked text but stored the UNMARKED text in the candidate's `text` field, so every consumer that rebuilds a root from `text` silently deleted the "produced N times in a row" note: truncateToolResultBlob already did, and the new invocation restoration did too. That note is the repetition breaker's per-entry half, so losing it re-primes the self-reinforcing loop the breaker exists to end. Store the marked text, which makes `text` a true mirror of the stored payload for the first time, and fixes the truncation path by the same change. Also anchor the restoration's search on the preceding newline. toolResultToText always emits the invocation after the [tool_result], call_id: and name: lines, so the real line is never first; name: renders the result's tool name, which nothing sanitizes, so an unanchored search could be satisfied by a crafted tool name and rewrite that header instead of the invocation. The regression test fails with the pushDeduped change reverted and passes with it. * test(cursor): record why the 600 KiB cap tests are not refund tests The refund leaves those two fixtures alone because restoring a 600 KiB argument costs more than the whole envelope, so cost > spare is always true there. That is a size-dependent skip, not a rule that the line stays clipped: an argument over the cap but well under the envelope is restored by design. Anyone shrinking those fixtures to speed them up would silently convert them from tests of the cap into tests of the refund, which is the one reading that would make them vacuous.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
) Three leftovers from scoping the history preflight, found by re-reading the owned docs against the shipped behaviour. `codex-home.md` still said a detected migration always restores all three preimages before refusing. That is now direction- and reason-dependent: on apply the paginated refusal retires the relabel unit and the write stands, because it is permanent and compensating it produced a home with no OpenCodex models at all. Every other reason there still compensates, and restore and removal compensate on all of them, because retiring a provider definition its thread rows still name would orphan them. The unattended-sync test stubbed the injector returning a paginated refusal, which it can no longer produce, so the test guarded an unreachable shape while still passing. It now stubs an operational reason, which is still a hard refusal. One preflight test claimed to assert that provider definitions are preserved; it asserts which target sets reach a paginated row. Renamed to match. Co-authored-by: Cursor <cursoragent@cursor.com>
…s failure (#4553) Covers the live path that #4512 was asked for and merged without: an invalid live answer must return 502 to the client while booking the real upstream 200, and an alias-registration failure must return 503 while booking the same 200. Follow-up to #4512 (merged as 9b2fc10). Verification: local product suite, typecheck, build and install NOT RUN. Hosted Cross-platform CI run 34779112640 succeeded at bf29126. Merged through maintainer self-integration per MAINTAINERS.md. Co-authored-by: maoxin1234 <275637173+maoxin1234@users.noreply.github.com>
Five regressions for the invocation refund landed in #4543. - A just-over-cap argument (~2,117 bytes against the 2,048 cap) must come back byte-exact. The existing fixture is 4,600 bytes, where thousands of spare bytes surround the decision and an off-by-one in the cost arithmetic or in the newline-anchored search cannot show. - No result may be evicted to pay for a wider invocation line, and under the fixture's uniform per-round costs the restored set must be the newest contiguous suffix. That second claim is a direction check: flipping the walk to oldest-first makes it a prefix and turns this red. The comment says so, and says plainly that contiguity is not guaranteed under mixed sizes, because the pass skips an unaffordable line with continue rather than break. - A checkpoint-covered call must keep its argument tail in the replayed suffix. Drop knownCallsOffset from the pass's callBefore bound and only this case notices, since that term is identically zero on the full-replay path. - A multi-byte argument must survive intact, with U+FFFD asserted absent so a failure names itself rather than only showing unequal strings. - The outputElided skip is load bearing, and finding that out took two tries. A sweep of single-result fixtures said the guard was dead code — elision appeared to always cut the invocation line too — and an adversarial counter-read found the configuration that sweep could not reach. Truncation alone cannot pay for a restoration: it undershoots its own budget by about 28 bytes. Initiator recovery can. With a ~519.7 KiB system prompt the equal-share pass cuts two trailing results to ~2.3 KiB, losing "output:" but keeping the clipped invocation line, and recovery then drops the older elided sibling to fit the user turn; those freed bytes become spare. The test searches that ~24-byte window rather than pinning a literal size, because pinning one made it pass on a two-character call id and fail on a twelve-character one, and it fails loudly if the window disappears. Each of the last three was verified by mutation. The two 600 KiB cap tests are byte-identical. The only src change is the comment recording what the guard actually depends on, and structure/providers/cursor.md records it too — the earlier draft of both called the guard defensive, which was wrong.
…og (#4556) Round 1 made the Devin catalog parser preserve ClientModelConfig field #5 as a tri-state on ModelCatalogEntry; the flag stopped at the cache. Carry it through: fetchDevinUsableModels now votes per base across the rows that collapse into it and returns inputModalities, and the Devin branch of provider-fetch spreads that value before catalogHintsFromProviderConfig, so exact modelCapabilities declarations, the legacy modelInputModalities record and the vision-sidecar rewrite keep precedence and the live value survives only when none of them applies. Collapse policy, pinned with the round-1 #1796 precedent: rows that never asserted field #5 abstain, so one unsuffixed unknown row cannot poison a base whose effort variants were measured image-capable; unanimous measured rows advertise ["text"] or ["text","image"]; measured disagreement advertises nothing, because a single measured false is not outvoted by its siblings. The accepted mismatch is documented in code: resolveWireModelUid prefers the plain UID when the catalog lists it, so a variant-measured image base can route a no-effort request to an unasserted plain row. Tests: a new devin-live-models suite seeds the cache through the real parser via a setCachedCatalogForTests seam and covers the collapse matrix (disabled and MODEL_* rows proven non-voters by behavior) plus five fetchProviderModels advertised-catalog cases locking the hint precedence, including the existing sidecar path for exact text-only declarations. Layout registries name the new file; structure/catalog.md and structure/adapters/registry.md record the contract.
) The adversarial counter-read of 5d95dbf returned two should-fix findings and one nit, all folded here: the advertised-catalog tests now stub globalThis.fetch to throw, so a seeded-cache miss fails the test instead of dialling Cognition; the catalog.md precedence sentence is scoped to inputModalities (live contextWindow and reasoningEfforts have their own configured sources and the broader claim was not literally true) and moved out of the TTL paragraph; and the registry.md collapse wording covers the whole EFFORT_TOKENS suffix set rather than only effort variants.
Ten PRs landed on dev across two merge rounds run by four worktree lane threads, each merged only after the check run's head_sha was verified against the PR head, with post-merge dev runs 34778300807 and 34782580496 as the joint proof for each round. #4522, #4530 and #4516 are closed with merge references after an independent audit of every claim against the tree; eleven issues are deliberately left open with their residuals named. #4555 is green and deliberately unmerged: MAINTAINERS.md requires explicit security review for a change that sends the serving provider's API key to an operator-named endpoint, and the dev self-integration exception does not cover that review. An adversarial review found a real silent regression there, which the lane fixed. Records what the unit learned, including that a fresh lane worktree has no node_modules so hosted CI is the only evidence that can exist, that a push already queues CI so the explicit dispatch is a fallback, and that a cancelled dev run is a concurrency artifact of the release train rather than a failure.
Names the two items still waiting on people: #4555 green and pending the security review MAINTAINERS.md requires for a credential-destination change, and #4528 whose only CI failure is a stale-base release version line rather than anything in its diff. Records that the thread heartbeat was repointed to watch exactly those two and made read-only by construction, after an audit caught an earlier draft instructing it to close #4519 automatically on merge, which is not the verified-code-evidence standard every other close in this unit met.
…4519) (#4555) * fix(web-search): assess the bridge search endpoint as a destination (#4519) providers.<name>.webSearchBridge.endpoint names the URL that receives that provider's own API key as a Bearer token when the ollama bridge backend runs a search. Two checks stood in front of it and neither was a destination assessment: providerWebSearchBridgeConfigError did new URL plus an http/https protocol test, and resolveOllamaWebSearchEndpoint returned the configured value whenever originOf parsed it. Provider baseUrl has had the real assessment for a long time; the endpoint had none, so endpoint: "http://169.254.169.254/latest/meta-data" was accepted and the key was sent there. Both boundaries now run the existing providerDestinationConfigError. Metadata destinations are refused unconditionally. Loopback, localhost and private space are refused unless the provider sets allowPrivateNetwork or its name is a registry entry that is local by definition, which is what keeps a self-hosted Ollama working. The plan-time check is the load-bearing one, not a second opinion. A hand-edited config file, ocx config set and ocx config import all reach configSchema only and never call providerWebSearchBridgeConfigError, and resolveOllamaWebSearchEndpoint is the only reader of this field in the tree, so a value that survives file load still cannot be spent. It refuses silently by design; config-time is where the operator is told why. Both checks are synchronous and literal-only and resolve no DNS, so a hostname that resolves into metadata or private space still passes. No new classifier was written and no DNS was added at this boundary. * fix(web-search): tell the operator when a bridge endpoint is refused A provider keyed under a custom name, say "my-ollama", pointing at http://127.0.0.1:11434/api/web_search armed before the destination check and disarms after it, because only the registry ids (ollama, vllm, lm-studio, litellm) are local by default. Two properties combined to make that invisible: config load never runs providerWebSearchBridgeConfigError, so the block loads cleanly, and the plan-time refusal returns undefined by design so the key stays unspent. The operator's web search stopped working with no signal at all. resolveOllamaWebSearchEndpoint now warns once per provider and endpoint when the refusal is a destination decision, naming the two remedies: set allowPrivateNetwork, or key the provider under its registry id. The planner runs per request, so the warning is deduplicated and the dedupe set is bounded. The destination URL is never logged and the provider key goes through redactSecretString, since a provider key is caller-controlled. Adds the coverage the review named as missing: a metadata endpoint survives validateConfigCandidate intact and is then refused at plan time, which pins the load-path behavior the whole argument rests on rather than simulating it.
… preserve an explicit reasoning disable (#4534) * [agent] fix: normalize inbound Chat images before route selection and preserve an explicit reasoning disable The native Chat fast path recognized only `image_url` parts, while the translated path also understood Pi/MCP `{type:"image", data, mimeType}` and Anthropic-shaped `{type:"image", source}` parts. Two failures followed from that single gap: a text-only routed model kept an image-bearing body because `isNativeChatRouteEligible` could not see the image, and the native whitelist passthrough forwarded the foreign part verbatim to an OpenAI-compatible upstream that does not accept it. Recognition now lives once in `src/chat/image-parts.ts`, and `normalizeChatImageParts` runs before `routeModel` so the diversion decision and the forwarded wire observe the same parts. A body with no foreign image part is returned by reference and stays byte-identical. A remote reference is recognized and rewritten, never fetched. Separately, the Chat inbound effort allowlist dropped `none`. That is the runtime's disable sentinel, not an unknown value: `src/reasoning-effort.ts` maps it to omitting the wire parameter and the Pi export maps Pi's `off` level onto it. Dropping it let a provider default re-enable thinking the caller had turned off, which is not neutral for Anthropic families that think by default. Audit findings F1 and F7 (2026-09-14). Local verification NOT RUN BY USER INSTRUCTION. * [agent] test: place the new Chat regressions in their seeded layout domain Hosted CI caught this: tests/test-layout-tooling.test.ts reported "chat-inbound-reasoning-none.test.ts: seed responses != server" for both new files. scripts/test-layout/layout.json seeds a `chat-` prefix to the `responses` domain, which is where the sibling Chat-translation tests already live, so registering them under `server` contradicted the seed. Moves both files to tests/responses/ and registers them there. Import depth is unchanged, so no import edits were needed. Local verification NOT RUN BY USER INSTRUCTION. * [agent] docs: correct the stale reason for dropping the opencode none variant Independent review found this comment now states a false fact about this stack's own change. It said the chat ingress allowlist OUTPUT_CONFIG_EFFORTS "has no none", which stopped being true in 63fbe66 when F7 added the disable sentinel to that allowlist. The filter itself is kept, narrowly and on a stated basis: emitting the variant would change what this exporter writes into a user's opencode config, and whether opencode's picker round-trips reasoningEffort "none" back to a wire this proxy reads has not been verified. Re-enabling it is a scoped follow-up needing that check, not a side effect of an ingress fix. MCode and ZCode filter none for their own separate reasons, which remain accurate at their call sites. Comment-only; no behavior change. Local verification NOT RUN BY USER INSTRUCTION.
… failover (#4563) Sanitizes Codex-forward identity metadata and preserves narrowly scoped pre-output target-local failover, so a combo that already took a 429 can try the next healthy target instead of terminating on a request-local 400. Carries #4528 by @RHODIZSECURITY. Verification: local product suite, typecheck, build and install NOT RUN. Hosted Cross-platform CI run 34797124354 succeeded at 2530f4c with 21 jobs and zero failures. Merged through maintainer admin on the project owner's explicit instruction. Co-authored-by: RHODIZSECURITY <180237049+RHODIZSECURITY@users.noreply.github.com>
* fix(cli): escape catalog diagnostics before terminal output * docs(cli): specify diagnostic escaping in translated guides --------- Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
…4573) Records the roadmap for two more delivery rounds against the open backlog: per-lane issue assignment, orchestrator models, explicit per-lane write scopes, the contributor merge queue, and the merge and closure policy the rounds follow. The lane split was audited three times and changed each time. Two round-1 lanes had both claimed the same Responses source file and are now one lane; the Models dashboard and the catalog backend now have one owner each; the reasoning-ladder file has a single owner across both rounds; and because any lane that adds a user-facing setting has to edit the shared config schema, one lane per wave owns that schema while the others land behavior only. Documentation only. Nothing in the build, typecheck or test path reads from devlog/, and this branch touches no source, test, workflow or release file.
…4565) * fix(codex): bound completed entitlement version misses per account * fix(codex): charge roster misses only after flight admission --------- Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
* test(lab): verify busy-lock ownership without wall-clock timing * test(lab): detect repeated live-owner probes without wall-clock timing --------- Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
#4298) * perf(bridge): zero-copy stream chunking for text and reasoning buffers - Accumulate streaming text deltas and reasoning summaries into string[] chunks rather than immediate string concatenation. - Postpone string realization to boundary flushes (flushText, flushSummaryReasoning, and batch assembly), eliminating frequent intermediate string allocations and V8 ConsString flattening pauses on long outputs. - Keep tool-call arguments on string concatenation with explicit rationale for mid-stream JSON parsing. - Add regression coverage for 1000-delta stream assembly and budget limit rejection. * fix(bridge): ignore zero-byte string fragments in chunk accumulators * fix(bridge): restore consistent indentation in batch accumulation path
* fix(cli): stop connect runtime discovery after a valid selection * fix(cli): reuse status runtime selection for readiness --------- Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
) (#4588) Kiro coerced an absent cacheReadInputTokens or cacheWriteInputTokens to 0 and then recorded it as a measured value. Every other usage path omits what it has no reading for, and cacheHitRate is null when unobserved, so this was the one place a silent provider looked like a total cache miss - the exact signal needed to tell whether a routing change preserved the prompt cache. Absence is now unknown; a malformed counter is still a malformed event, and the bridge wire still emits its zero default for strict clients.
…nd serve alpha/search from a configured sidecar (#4586) * fix(web-search): end a mixed-tool bridge leg instead of failing it, and serve alpha/search from a configured sidecar Two web-search gaps, both reported against a deployment with no ChatGPT forward provider. The hosted-search bridge failed closed whenever one upstream leg carried both an intercepted web_search call and a tool call the client has to run: the search cell closed as failed, the client's call was dropped, and the turn died after five reconnects. Such a leg now ends the turn on the leg. The intercepted searches run, their hosted cells complete, the held client calls are released with their call_id and streamed order intact, and the leg's own terminal closes the turn. No continuation is sent upstream, because the client's call is unanswered and the conversation owes the client a turn rather than the gateway, and no tool output is fabricated for a call the bridge cannot execute. A leg whose upstream terminal already ended the turn runs no search at all and closes any cell it opened, so a dead turn is never billed and never leaves a spinner behind. This covers the remainder of issue 4429 on the client-facing side only. The destination still never receives the executed search result: the caller replays the hosted web_search_call cell, which carries the query and its sources but no result text, so the destination's own function_call and function_call_output pair is not reconstructed. Repairing that needs the outbound body rewritten before the first leg is dispatched, which lives outside this module. POST /v1/alpha/search returned 400 whenever no ChatGPT forward candidate existed, before considering any configured backend, so an API-key-only deployment could not use built-in web search at all. When and only when that candidate list is empty, an explicitly configured webSearchSidecar backend of anthropic, xai, gemini, or exa now serves the request with that backend's own credential and answers the shape the client reads. The verbatim relay is untouched while a forward provider exists. An unset or openai backend, a sidecar disabled by enabled:false, and a named backend whose credential is missing all keep the 400 rather than borrowing another paid backend, and a backend that fails answers with its own diagnostic instead of asking for ChatGPT auth. No new configuration field: the fallback reads the webSearchSidecar block that already exists. * fix(web-search): name the missing credential instead of asking for ChatGPT auth A deployment that already chose a web-search backend was still told to configure a ChatGPT forward provider when that backend's credential was absent, which is the exact answer the feature request asked this path to stop giving. Resolution now distinguishes a deployment that named no backend from one whose named backend cannot authenticate: the first keeps the ChatGPT-auth message, the second is refused with a message naming that backend and the credential it could not find, and neither reaches another paid backend.
… pin from overriding it (#4585) * fix(codex): discover Codex App runtimes and defer the prompt probe's version check The prompt probe carried a private four-path POSIX resolver. The Windows Codex App installs codex.exe under %LOCALAPPDATA%\OpenAI\Codex\bin\<version>, which that list can never match, so the probe reported an absent candidate on machines where Codex was plainly installed (issue 4458). - runtime.ts gains an "installed" source that enumerates the Windows Codex App bin root newest-first (deterministic name tie-break) and keeps the four POSIX paths the probe used to hardcode. It ranks after PATH, so PATH stays authoritative. - deps.probeVersion === false selects a spawnable candidate without running `codex --version`. A request-path probe needs something it can spawn, not a version, and ~1s of blocking exec per candidate is what made the probe give up. - That deferred selection gets its own memo and never publishes into runtime authority, because peekCodexRuntimeProcessCache feeds convergence and the bundled catalog, which would read a null version as "unknown version". - ENOENT from the version probe is now program-not-found rather than a generic --version failure, so a missing program stops looking like a broken one. - prompt-text-probe.ts asks the shared resolver, spawns through codexExecInvocation so a Windows .cmd launches correctly, and reports a stable failure kind. Process stderr is read only to classify and never returned. Co-authored-by: Clive Rosfield <64878945+S0RYUASUKA@users.noreply.github.com> * fix(codex): stop a stale auto-discovered runtime from overriding a newer one codex-runtime.json recorded command, source and version, but not how the record got there. resolveAndPersistCodexRuntime writes every automatically discovered selection, so a "configured" entry proved nothing about operator intent - and resolution then stuck to it even when a newer runtime was present. On the reporting machine a still-runnable codex-cli 0.135.0 kept winning over the 0.153.4 the Codex App was actually running, and the catalog derived its reasoning ladder from the older binary (issue 4204). - PersistedCodexRuntimeState gains origin: "pinned" | "discovered". resolveAndPersistCodexRuntime writes "discovered"; a direct persistCodexRuntime call, which is how doctor --fix selects, stays "pinned". A record with no origin reads as discovered, because auto-discovery is what wrote every one of them, and reading it as a pin would leave the bug unfixed on exactly the installs that have it. - An unpinned record hands over only to a strictly newer valid candidate. A pin is never touched, equal versions stick, and an unknown version on either side is not evidence of an upgrade. The handover is reported as supersededDiscovered, kept separate from replacedConfigured so "superseded" never reads as "gone". - The no-discovery fast path still probes the bounded Codex App roots when the persisted record is unpinned. That path is what the catalog's bundled loader uses, so without it the comparison could never run where the bug actually bites. PATH-wide discovery stays off. - The prompt probe's reported runtime and failure detail now go through displayCodexRuntimePath: the response is served over the management API and a Windows Codex App path contains the account name. No config-schema field was added; codex-runtime.json is runtime-owned state. * test(codex): keep the PATH-outranks-installed case on a colon-free PATH entry pathCandidates splits PATH on node's delimiter, which is ":" on the POSIX runners this suite also runs on. The Windows-style "C:\on-path" therefore split into "C" and "\on-path", neither of which produced a candidate the fixture's existsSync recognised, so every PATH candidate failed and the installed Codex App runtime won — the exact opposite of what the test asserts. It failed on macOS 1/2 and test 1/4 and passed nowhere the split occurs. The entry is now colon-free, so the directory survives the split on every platform and the test proves the ranking it was written for. --------- Co-authored-by: Clive Rosfield <64878945+S0RYUASUKA@users.noreply.github.com>
…d agree with preview (#4546) (#4589) Follow-up to #4580, from review of the merged commit. P1: preview and resolve disagreed on the FIRST detour. Preview refused to pick one because pickRoundRobinAccount commits and advances the ring, so it returned null and fell through to the ordinary binding while resolve served from a fresh alternate. Subagent fallback scores the previewed account to decide whether a model is reachable, so it could retire a model over usage the request would never touch. Preview now peeks the same candidate through peekAlternateCodexAccount, which delegates for every strategy except round-robin because that is the only branch with a side effect. P1: when no detour existed the code fell through and deleted the binding. A provider-wide 503 soft-avoids every sibling, which is precisely when the candidate list is empty, so the hold did not cover the failure it was written for. Being unable to send is not the same as forgetting which account owns the conversation: the binding now survives and the bound account is returned, on both the ordinary and model-detour lanes. reset-first could still move a bound thread onto an account with no usage reading, because hasCodexQuotaHeadroom answers true for unknown. The quota strategy excludes those through its strictly-cooler compare; reset ordering has no such compare and now says it explicitly.
* docs(devlog): record the round 2 outcome and close the delivery unit Three wave-A lanes landed with one config-schema owner and no collisions. Unit totals: 22 pull requests closed, 13 issues closed, three issues deliberately left open with their landed scope recorded, three follow-ups filed. * docs(devlog): move the round 2/3 delivery unit to _fin Both rounds delivered and the closure sweep verified against live GitHub state: 22 pull requests terminal, 13 issues closed, three issues deliberately left open with their landed scope recorded.
… turn (#4595) * docs(devlog): plan the regression audit and the 2.55.0 release Names the six source files three separate merges each touched in the dev delta, states that the audit reads the merged state rather than any single diff, and records the one deliberate deviation from the release script preflight along with what covers each skipped check. * fix(web-search): do not release a withheld client call under a failed turn A mixed bridge leg whose upstream terminal was response.failed released its withheld client-executed tool call, because the mixed-tool termination change routed both failed and incomplete terminals through the same endWithoutSearch branch and that branch flushes held calls. The failure path ten lines above documents the opposite rule: releasing a tool call the client would start executing is exactly what must not happen under a turn that is already over. The two terminals differ. An incomplete turn is one the client can still act on, so its held call goes back; a failed turn is over. The decision now carries whether held calls may be released, true only for incomplete, and the emit path drops them otherwise. The hosted cell still closes in both cases, which is what the reordering was for. Found by a cross-merge regression audit of the dev delta before promotion. Records the audit findings for all five contended file groups alongside the fix. * docs(devlog): correct the release plan version-line and CI-event facts preview and main carry two different version lines over the same product tree, because release.yml requires package.json to equal the dispatched version and a preview dispatch must be a prerelease. Only the clean-tree guard and the npm channel-forward check are script-only. And only a push-event CI run on the release branch satisfies the publish gate; a green PR run at the same SHA is refused. * docs(devlog): write the 2.55.0 release runbook The exact ordered sequence with the gate gating each step, including the two version lines over one product tree, the push-event CI requirement, the Service lifecycle prerequisite, and the dev move that must precede the stable publish.
…4546) (#4592) * feat(codex): record why a live binding was kept, moved, or released (#4546) logCtx.affinity was typed and persisted but never assigned, and routing had no reason to report, so an account move was only visible by comparing account labels across log lines. resolveCodexAccountForThreadDetailed now returns the decision and its cause, the pool auth context carries it, and the usage entry persists both move and reason. * fix(codex): hoist the affinity decision to the pool context scope The declaration sat inside the selection block and the spread landed on the main-pool return, so the pool context never carried it and typecheck failed. Reading resolution.affinity through an in-check keeps the fixed-account branch of the union valid. * fix(codex): report the affinity decision on every selection path A first placement returned through the active-account retention path, which carried no decision, so the record was missing for exactly the case that establishes a binding. All selection returns now report, and the detailed-resolver assertions move to toMatchObject because the resolution carries a field they did not previously expect. * fix(codex): carry a release reason from the outcome path to the next resolve A 429 clears the pin inside recordCodexUpstreamOutcome, so the request that pays for the cold prefix arrived with nothing left to explain why. The reason is now held per thread, bounded, and consumed by that thread next resolve. Two routing cases compared whole resolutions to each other and now compare the account, because a first placement and a later reuse legitimately report different decisions. * fix(test): compare the account, not the whole resolution, for model detour independence * fix(test): compare the account for the second model detour lane too * fix(test): tolerate the affinity decision in the 401 replay resolution check
…d-budget owner (#4602) * docs(devlog): plan wp4 send budget at diff level * docs(devlog): correct the wp4 send-budget plan from the audit round Four claims were wrong: the #2981 helper is not the opt-in part and Codex passthrough gets a fresh allowance per leg; the same-request account resend is retryCodexPoolOnAlternateAccount, not applyFailureFailover; continuation repair is already covered on the policy path while empty-completion, rebuildAndRefetch, compact and generic OAuth hops are not; and Retry-After is already shortened by local caps, so treating it as a lower bound is a behavior change. A 3-send ceiling would also break the 3+1 recovery the plan measured. * docs(devlog): locate the send-budget owner and why the passthrough escapes it handleResponses already owns a request-scoped transient budget and documents itself as covering recovery refetches, but the Codex passthrough legs sit in an earlier scope and pass neither attempts nor onSendsConsumed, so each takes a fresh default of 3. That is the source of the measured 4/7/12, and hoisting the owner is the smallest first step.
…oss a no-account resolve (#4598) (#4604) The release reason re-derived a subset of the selectable guards and fell through to a quota fallback, so paused, plan-excluded, cooled-down and quota-avoided releases named a cause routing never used. It now comes from the same predicates in the same order as isCodexAccountSelectable. Separately, a no-account return carried no payload and the pending reason was consumed before selection, so a release that failed to find a replacement was never recorded; the reason is now reported on that return, handed forward, and forgotten only once reported.
…rough (#4546) (#4605) * fix(responses): share one transient send budget with the Codex passthrough (#4546) The budget owner was declared below the passthrough branch, so it was in the temporal dead zone for those four sends and each took the helper fresh default of 3. Hoisting it above the branch and wiring the sends makes one logical request share one transient budget across its recovery legs. The cross-account alternate is untouched because it does not go through the helper, so the 3+1 recovery shape is preserved. * test(responses): pin the shared transient budget across a sanitized rebuild The repeated function-output decrypt case sent 6 times (3 on the first leg, a fresh 3 on the rebuild). With the budget shared it sends 4: the rebuild draws on what is left rather than a new allowance. That count is the regression for #4546. * test(lib): pin the passthrough legs into the shared-budget source oracle The oracle asserted exactly three legs report into the counter. The four Codex passthrough sends now do too, and the oracle names them plus the transientRetryPolicyFor gate that would silently restore a fresh allowance.
) (#4606) * feat(logs): surface the account decision in the route explanation (#4546) The affinity move and its reason were persisted but never exposed, so the record only existed for someone willing to parse usage.jsonl. The route-decision endpoint behind ocx logs explain now carries them, null for rows that have no account decision. * fix(usage): persist the affinity record the writer was already setting appendUsageEntry builds the persisted entry from an explicit whitelist, so affinity and affinityReason were dropped on write and #4592 never reached disk. Both are now normalized against known value sets, and a reason is kept only alongside a move.
…#4546) (#4608) The budget was a counter local to one handleResponsesInner frame, and a combo parent runs a separate child turn per target, so a three-target fan-out took three fresh allowances. It is now a holder on HandleResponsesOptions, minted at genuine ingress and inherited by children through the options spread that already carries comboAttempt and translatorBudget.
…ero (#4546) (#4609) * fix(responses): one send budget per logical request, and zero means zero (#4546) Refs #4546. wp4 steps 2-4 of the cost-guard roadmap. The amplification behind #4546 was never one missing limit. Every layer that can re-send counted its own allowance, so a per-layer 3 composed into a per-request 12. #4605 and #4608 gave the transient layers one shared counter; this gives that counter a policy. src/lib/request-execution-budget.ts carries the guarded text-Codex profile: four model sends per logical request, a base allowance of three shared by the initial send and same-target retries, and ONE final-recovery reserve that an account move and a validated rebuild share rather than taking one each. The permit is consumed immediately before the physical send, not reconciled after the helper returns, because a counter read afterwards cannot stop two legs that both saw the same remainder. Zero now means zero. The Math.max(1, ...) floors in remainingTransientSendBudget and in both retry helpers funded one more send on every recovery leg, which is most of how a bounded per-leg allowance became an unbounded per-request count. A refused send raises the typed SendBudgetExhaustedError, which UpstreamRetryEvidenceError no longer wraps and which transportFailureResponse maps to request_send_budget_exhausted instead of reporting a proxy decision as a 502 upstream fault. Where a reusable upstream answer already exists, the refusal happens before that body is cancelled: the native OAuth 401 replay and the same-target 429 wait now check the remainder in their own conditions, so an exhausted request returns the real 401 or 429 with its Retry-After rather than a synthetic 502. Two holes that survived #4608 are closed. The adapter initial send passed the raw policy on the argument that nothing had been spent yet, which is false for a combo child: it inherited the parent's holder and then took a fresh full allowance anyway. And the cross-account move was bounded by nothing per request -- excludeAccountId excludes only the account that just failed, and the recovery loop can return after the alternate fails too, so one request could walk the pool an account at a time. Deliberately out of scope, recorded rather than hidden: the same-account gated-model 400 ladder keeps its own maxRetrySends bound; compact, Kiro, Cursor and the generic OAuth hops still hold their own allowances. * docs(devlog): record the wp4 slice A audit counterexamples (#4546) * fix(responses): a consumed dispatch permit refuses the next send (#4546) Refs #4546. The single-use contract was written but not enforced: every call site discarded the boolean, so a leg that reached its thunk twice -- an adapter that calls its executor again, or a retry shape that re-enters -- got the second send for free. The return now gates the send.
…end budget (#4546) (#4611) * fix(responses): compact and the Kiro inner retries join the request send budget (#4546) Refs #4546. PRD R04. Compact held its own allowance. Its normal send took a fresh transient three, the stored-pool 401 replay added one, and the 429 alternate added another -- and the guard meant to make those last two mutually exclusive keys on kind === 'pool', so a main-pool credential left it false and really could reach five. The recursive handoff child then forwarded the options object without a holder and minted its own, so one logical compact could reach ten. It now draws the shared remainder for the ladder and spends base-then-reserve for each single send, and the handoff child inherits the holder explicitly. Kiro was the larger multiplier. It nests a three-round throttle loop over a three-attempt reset ladder that can itself run twice per round, so one adapter entry could be eighteen upstream requests, and the text-fallback rebuild constructed a fresh context that dropped whatever core passed. AdapterFetchContext now carries an optional budget, every physical send inside the reset ladder is admitted against it, and the fallback rebuild carries it forward. The field is optional and unlimited when absent so an adapter unit test that calls the transport context-free keeps its own retry shape. Deliberately still out of scope: Cursor rides IncomingMeta rather than AdapterFetchContext, the compact routed fallback mints a fresh budget, and the generic OAuth hops keep their own per-request failover counters. * style(responses): align the sendBudget field with its sibling context keys (#4546)
Refs #4546. PRD R05/wp5, first slice. The per-request budget bounds how many times one request reaches upstream. It cannot bound a fan-out: a worker that spawns seven hundred children, each sending exactly once, never violates a per-request cap and still spends the account. That is the second half of the incident. src/lib/workflow-budget.ts tracks the root workflow -- the user-visible task, identified by the parent thread header -- and gives it a finite physical-send ceiling. Every send charged to the request budget is charged to the root as well, including the cross-account move, and a root that has spent its ceiling is refused before dispatch with workflow_budget_exhausted rather than a synthetic upstream error. An exhausted root is never evicted to make room. Dropping a live entry would hand the fan-out a fresh allowance, which is precisely the laundering the ceiling exists to stop, so eviction skips any root with work in flight. The ledger is process-local and in-memory. It bounds a single proxy process honestly and says nothing about a second process sharing the same account pool; that needs a shared durable store and is declared out of scope rather than implied. The concurrency ceiling and the interactive reserve are implemented in the module but not yet wired, because they need a release path tied to the turn lease.
…4546) (#4613) Refs #4546. Forward fix for the gates failure on 1abc5cc. HandleResponsesOptions.sendBudget is typed as the narrow TransientSendBudget holder so a caller that predates the execution budget can still pass one. AdapterFetchContext needs the full contract, because an adapter that retries internally has to call reserveDispatch. Passing the narrowed value straight through failed typecheck at all three fetchResponse literals. Narrow it once next to the other budget helpers instead of asserting at each call site; an adapter that receives undefined keeps its own retry shape, which is the documented optional behaviour.
…) (#4614) Refs #4546. PRD R06/wp5, second slice, completing the module landed in #4612. A fan-out shares the conversation it serves. Without a reserve, a worker burst takes every concurrency slot under its own root and the interactive turn that started it waits behind its own children. runAdmittedHttpTurn now admits each turn against the root workflow as well as the process-wide turn gate: a request that names a parent thread distinct from its own is treated as that fan-out and may not take the reserved slots, while a top-level request is the conversation and may. The refusal is a local queue-capacity answer, not a synthetic upstream error, and the lease is released on both the normal and the throwing path so a failed turn cannot leak a slot.
…our Retry-After (#4546) (#4616) * fix(codex): promote a healthy detour instead of releasing it, and honour Retry-After (#4546) Refs #4546. PRD R07. When a transient hold outlived its window, routing deleted the whole affinity entry -- including the detour account that had actually been serving the thread -- and re-picked cold. The timer expiring restores the right to re-decide; it is not itself a recovery, and treating it as one threw away the single piece of evidence the request had. A still-healthy detour is now promoted to the binding instead, with the move recorded as rebound/transient_hold_expired so the reason is visible. A detour that has itself gone unhealthy still falls through to the cold path. Retry-After is a lower bound on the transient path. The local maximum delay bounds our own exponential backoff and has no business shortening a wait the provider stated: sending early is a request we already know will be refused, which is the storm the header exists to prevent. It is opt-in per caller so the change lands on the transient path first rather than silently lengthening every adapter's backoff, and an honoured wait is ceilinged at one minute so an hour-long Retry-After cannot park a request. * docs(devlog): record the R07 detour-promotion outcome (#4546)
Promotes the dev product snapshot 62f0222 to the preview train. The 2.55.0 line carries the #4546 cost-guard work: one send budget per logical request with a shared final-recovery reserve, zero-is-zero refusals with a typed error rather than a synthetic 502, compact and the Kiro inner retries admitted against that budget, a finite send ceiling per root workflow with an interactive reserve a fan-out cannot take, and a healthy detour promoted on transient-hold expiry instead of released cold. The previous preview tip 2.54.0-preview.20260914 is already tagged and published and is outranked by v2.54.0, so it could not be re-released; this is a new candidate rather than a re-cut.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
✅ Deterministic PR hygiene checks passed. |
✨ 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 |
⏳ DRAFT
What to do
Its title has been prefixed with |
Summary
Promotes the dev product snapshot
62f02223a0to the preview train as2.55.0-preview.20260914.The 2.55.0 line carries the #4546 cost-guard work: one send budget per logical request with a shared final-recovery reserve (#4609), zero-is-zero refusals that return a typed local error rather than a synthetic 502, compact and the Kiro inner retries admitted against that budget (#4611), a finite send ceiling per root workflow with an interactive reserve a fan-out cannot take (#4612, #4614), and a healthy detour promoted on transient-hold expiry instead of released cold, with
Retry-Afterhonoured as a lower bound on the transient path (#4616).The previous preview tip
2.54.0-preview.20260914is already tagged, released and on npm, and is outranked byv2.54.0, so it could not be re-released even as a dry run. This is a new candidate rather than a re-cut.Scope this preview does NOT claim
The PRD's RG2 set is not complete. Still open: the durable cross-restart reservation ledger, V2 child first placement, the minimum quota/cache domain contract, the transient half-open probe lease, combo hops on the shared budget, Cursor's inner retries, and the sends-per-logical-request surfacing. This preview is for validating the send and workflow ceilings that did land; it is not a claim that #4546 is fully resolved.
Verification
dev tip CI at
9f39e75d4fcompleted success with no failing jobs. Local suite NOT RUN by policy. The preview-branch push CI on the merged SHA is the release gate and is checked separately before any dispatch.Checklist
previewas a release promotion