[WRONG BRANCH] release: promote dev to main for 2.28.0 - #2186
Conversation
#1686 made a caller that proves admission with one of our own secrets substitute the stored main credential, so that secret never leaves the process. That is right for a route that reaches the ChatGPT backend. It was applied by asking HOW the caller authenticated and never WHERE the request routes, so a request bound for a key-authenticated provider - which carries its own credential and never touches ChatGPT - was gated on a credential it has no use for. An install that deliberately never logged into ChatGPT therefore got 401 "No usable Codex main credential" on every routed request, a regression from v2.23.0 reported in #2132. Gate the substitution on route.codexAccountMode, which is set only for the native openai row and is exactly the test for "this route can consume the stored ChatGPT credential". Both pool and direct keep substituting, so #1686's Direct contract is preserved rather than narrowed to pool. Closes #2132
The ChatGPT backend 400s a gpt-5.6 request that still carries prompt_cache_retention: "Unsupported parameter". GPT-5.6 replaced the field with prompt_cache_options.ttl. Strip it on the canonical ChatGPT forward path for the gpt-5.6 family only. The retired value is not translated into the replacement field: 5.6 carries a different TTL contract and implicit caching still applies, so inventing one would change a caching decision the caller never made. The narrowness is the fix, not an omission. An older model may still honor the field, and a self-hosted or third-party forward gateway may still accept it, so both axes are pinned by non-match tests. Based on @lilinxiong's implementation in #2102, with an exact-or-dashed-prefix family match so a future gpt-5.60 is not swept up. Closes #2092
…s them Two evidence surfaces read per-model config with a bare map lookup while the runtime resolves the same keys through modelRecordValue, so family and case overrides were invisible to them and a prototype-shaped id resolved an Object.prototype member instead of missing. Routing capability evidence therefore gave gpt-oss:120b the provider-wide 8k window instead of the gpt-oss family's 131072, and ignored noVisionModels - values that select candidates, not just logs. The Lab behavior report missed the same overrides, and "constructor" resolved to Object.prototype.constructor, which made jcsStringify throw and silently dropped Lab subjects. Exact-own maps (modelPreferHostedTools, modelOpenRouterRouting) deliberately do not family-spread; that boundary is pinned by tests. Both patches are @ntdatt812's work from #2100 and #2077, applied unchanged. Closes #2100 Closes #2077
parseUsageQuota filled shortPercent and setAccountQuotaFromParsed dropped it, so the 5-hour burst window never reached the cache, the accounts DTO, the dashboard, or routing. A saturated short window was invisible to account selection. Carries @Ingwannu's #2056: shortPercent joins hasKnownQuotaValue, a new snapshotHasShort keeps a short-only snapshot from reading as empty, partial weekly/monthly snapshots no longer clobber a known short window, and updateAccountQuota carries the tuple. Also fixes the blocker raised in review on both #2056 and #2062: the scorer took Math.max over every finite window, so a snapshot carrying only shortPercent: 0 scored a flat 0 and made an account whose long windows were never observed look like the emptiest in the pool - pickLowestUsageAmong would then send every request to it. The burst window now refines a known long-window position instead of standing in for one, and returns CODEX_UNKNOWN_USAGE_SCORE until a governing window is actually observed. The ported test asserted the old behavior directly (computeCodexUsageScore({ shortPercent: 0 }) === 0); it is replaced by a case that pins the corrected contract in both directions. Closes #2047
gpt-daybreak-blue-latest is in the static native set, so catalog sync copied it onto every account selector and Pool could bind a bare Daybreak request to an account whose authenticated roster never contained it. The upstream answered "The 'gpt-daybreak-blue-latest' model is not supported when using Codex with a ChatGPT account." Make the authenticated ChatGPT roster the source of truth: discover per-account entitlement, advertise the gated row only where an eligible account confirms it, and refuse selection of an account that cannot serve it. Discovery failures fail closed - the row disappears rather than being offered on unproven evidence. Carries @Ingwannu's #2101, with three corrections: Selector compact missed the wire rewrite. accountGatedCompactWireModel was derived from the caller's raw model string, and an account-qualified selector like side/gpt-daybreak-blue-latest does not match the gated map, so it still took the native compact endpoint the guard exists to avoid. It now derives from route.modelId, the same value core.ts normalizes from. Direct callers shared one 64-entry roster cache with main/Pool. A burst of distinct Direct callers evicted the very entries the catalog projects from, so the gated row vanished until rediscovery. The two classes now evict separately. A comment in native-models.ts still claimed routing never collapses Daybreak into gpt-5.6-sol, which the wire normalization does exactly. Stacked on #2137: this consumes the substituteMainCredential value that PR corrects, so it must not land ahead of it. Closes #2097
OAuth xai/grok-4.5 and grok-4.6 Codex /v1/responses traffic still used the provider-wide openai-chat adapter, while the official Grok CLI catalog declares api_backend: "responses". Chat Completions compatibility holds the stream until the reasoning turn finishes, so Codex sat blank until the turn was effectively done. Declare the Responses wire default for those two models, scoped to OAuth and to responses-shaped inbound traffic. API-key xAI, Chat/Anthropic translation, other Grok models, and any explicit modelAdapters override all stay on Chat. Native Responses returns before the generic recovery loop, so the OAuth 401 replay never ran on this path. Add the equivalent one-shot: refresh once, rebuild the provider and adapter, replay once. It is a single branch rather than a loop, so a second 401 cannot refresh again. Refresh failures go through the existing public OAuth error projector, which the tests pin against path canaries. Carries @olddonkey's #2104 unchanged. Closes #1886
…abled GET /api/subagent-models built `available` purely from currently-pickable models, so a featured model disabled elsewhere vanished from it. The dashboard filters `chosen` against `available` and then PUTs exactly the rows it holds, which turned a hide into a delete: the next Save wrote the truncated roster to config.json, and the user read it as "ocx service lost my subagent models". Retain a chosen id in `available` when it is not otherwise selectable, appended after the selectable set and deduplicated. Models that are disabled and NOT in the roster stay excluded, so the picker behavior is unchanged for every model the user has not deliberately featured. The combo test asserted the old truncating behavior; it now asserts retention while a roster slot is held, and full exclusion once the slot is released. Closes #2133
…claims opencode-free sent no User-Agent, so Zen saw the bare runtime default (Bun/x.y.z) and rate-limited it harder than a client that identifies itself. Adds "User-Agent: opencode" alongside the existing x-opencode-client: desktop marker. The value is deliberately unversioned. OmniRoute, an independent open-source broker against the same Zen upstream, defaults to exactly this pair and reached it by retreating from its own earlier opencode-cli/1.0.0 pin: a pinned version is a claim about an install we do not have, and it goes stale on the vendor's schedule. The registry edit alone would have shipped to nobody. staticHeaders is documented as merged into every upstream request, but it was only ever copied at seed time, so any config written before a header existed -- or carrying any header of its own -- never received it. routedProviderConfig and buildModelsRequest now fill registry static headers beneath user headers, matched case-insensitively so an override replaces rather than duplicates: spreading "User-Agent" over a user's "user-agent" leaves both keys, which Headers serializes as one comma-joined value. Model discovery gets the same treatment because a provider identified as opencode when it completes but anonymous when it lists its own models reads as two different clients to a rate limiter.
…non-English AgentRouter answers 400 content-blocked when the first user message is not in English (#2074) while the identical English request returns 200. The gateway inspects the opening user content, so an Anthropic system string never reaches the filter -- the framing has to sit in that turn. Two corrections on top of @yzxcj797's #2082. The host test was hostname.includes("agentrouter"), which also matches notagentrouter.example and agentrouter.org.attacker.example. A prompt mutation keyed on a provider's identity has to be keyed on that identity exactly, so this matches agentrouter.org or a real subdomain of it. The original spliced the marker into the user's own string. That edits what the user wrote: logs, retries, and any upstream echo then show a sentence the user never typed as if they had. The framing is now its own leading text block, so the original text survives byte-for-byte. Idempotence is keyed on the leading block being exactly the marker rather than a substring test, so a user who quotes the marker later in their prompt does not suppress their own framing.
…amed A multi-account setup points several provider rows at the same OpenCode Go endpoint under names the registry has never heard of -- opencode-go-2 through -5. Quota dispatch gated on the literal name "opencode-go", so those rows had no dashboard quota panel and no report in `ocx provider quota --refresh --json` even though each one holds a working key for the same upstream (#1924). Identity is now answered by registryEntryForProviderDestination, the predicate this repository already uses for renamed fixed-key rows: it matches on normalized endpoint plus adapter plus key auth. A bare URL comparison would have been enough for the reported symptom but would also probe a row that points at that host through a different adapter, which speaks a different protocol and is not the provider whose quota shape we parse. The defensive canonical-URL check inside fetchOpenCodeGoQuota stays. Whether an API key may be sent to a host must not depend on the dispatch gate above it being correct. Absorbed from #2027 by @yzxcj797.
…d is known Some OpenAI-compatible streamers repeat an already-sent id, name, or arguments as a non-string placeholder on a continuation delta rather than as null. Validation ran before the pending-call lookup, so the whole turn died with a 502 and the tool never ran -- even though the value being repeated was already held in canonical form. The lookup now happens first and tolerance is per field, keyed on that field's own provenance. Two corrections on top of @waw4303's #2155. It gated arguments acceptance on the call having a canonical NAME. A name says nothing about whether arguments was ever sent as a string, so a real argument payload could be silently dropped. PendingToolCall now carries sawArgumentsString; an empty string counts, because it proves the upstream sent the field with the right wire type. It also left a non-string repeated id unconditionally terminal even after a canonical id was stored. Ids now follow the same rule as the other two. Diagnostics are passed from the rejection site instead of rescanned. A stateless rescan stops at the first structurally odd value, so a stream carrying accepted padding on call 0 and a real defect on call 1 blamed call 0.
… to a constant `tests/ws-upstream.test.ts` has two cases failing on Windows since 5a75e57: (fail) an HTTP fallback remains on the configured legacy tee path (fail) an older runtime stays on HTTP SSE without opening a WebSocket Measured, not inferred -- both bisect endpoints were run rather than assumed: dec332c 23 pass / 0 fail 5a75e57 21 pass / 2 fail fix(grok): ... backfill required annotations That commit adds `createResponsesFieldBackfillBlockRewrite()` to `blockRewrites` unconditionally, and the factory returns an `SseBlockRewrite` rather than `undefined`, so the chain is never empty and `needsClientRewrite` in `handleResponses` is now a constant `true`. `isWin32EagerRewrite` is `platform === "win32" && needsClientRewrite` (src/lib/bun-stream-caps.ts:126), so on Windows every Responses stream now takes the eager single-reader relay -- which is exactly what #864 asks for, since all traffic is now rewrite traffic. Instrumented at the gate to confirm the mechanism rather than deduce it: [EAGER] {"forceCodexWsEagerRelay":false,"useEagerRelay":null, "win32EagerRewrite":true,"needsClientRewrite":true, "platform":"win32","blockRewrites":1} So the source behaviour is intended and the assertions are stale. Both cases are about the *WebSocket* path not being taken, and both already assert that directly through `FakeWebSocket.instances`; the `isEagerRelaySseResponse(...)` assertion was a second-order signal that stopped tracking WS selection on win32. Holds it to the documented rule instead of to `false`, so it stays honest on every platform rather than encoding a pre-backfill world. Adds one precondition case pinning the coupling itself -- the rewrite chain being non-empty, and the platform rule -- so if either half moves it fails somewhere that names the real cause instead of inside a WebSocket assertion. Tests only; no src change. 24 pass / 0 fail in the file (was 21/2), and the new case is mutation-checked: forcing `isWin32EagerRewrite` to `false` turns it red. 58 pass / 3 skip / 0 fail across ws-upstream, responses-field-backfill, responses-snapshot-repair-server and subagent-fallback-handle-responses. `bun run typecheck` exit 0.
…ot the factory
The `eager-relay marker preconditions` test asserted only that
`createResponsesFieldBackfillBlockRewrite()` returns a function. That would stay
green if `handleResponses` stopped adding it to `blockRewrites`, so it did not
actually protect the contract the two marker assertions depend on.
Replace it with an integration case in the existing `handleResponses` describe:
drive a Responses stream whose `output_text` part omits the required
`annotations` field, then read the client bytes back. Seeing `annotations: []`
there is only possible if the rewrite is registered and ran, which is exactly
what makes `clientBlockRewrite !== undefined` and `needsClientRewrite === true`.
The platform half stays a pure unit test on the real exported helper.
Verified on win32, exact head:
- bun run typecheck exit 0
- bun test tests/ws-upstream.test.ts 25 pass / 0 fail
Mutation-checked:
- dropping `createResponsesFieldBackfillBlockRewrite()` from `blockRewrites`
fails the new case on `toHaveProperty("annotations")` (3 fail)
- widening `isWin32EagerRewrite` past win32 fails the truth table (1 fail)
Still test-only; no runtime change.
test(ws-upstream): hold the eager-relay marker to the win32 rule, not to a constant
Some relays omit the required id on message, reasoning, and function_call output items, so strict decoders reject the response even after #1941. Synthesize a stable msg_ocx_N / rs_ocx_N / fc_ocx_N id keyed on output_index, and never overwrite an id the upstream actually sent. Carries @bet4it's #2131 implementation and tests. One correction on top: an absent or malformed output_index collapsed to 0, so two such items both became msg_ocx_0 - duplicate ids, which is the defect this backfill exists to prevent. An unusable index now falls back to a monotonic ordinal based far above any plausible real index, so a synthesized id cannot collide with an index-derived one. The well-formed path is unchanged and still produces the stable index-derived id. Locale docs are limited to the English source here; the translated guides in the original PR were uneven and locale parity is not this change's thesis. Closes #2131
A heartbeat is adapter liveness, not turn content. guardTerminalEventStream pushed every nonterminal event into `seen`, and `seen` feeds both the continuation analysis and the rebuilt request. The openai-chat adapter now emits one heartbeat per tool-call delta, so a single large argument payload could grow that array without bound on a provider with terminalContinuationGuard enabled. The empty-completion guard already passes heartbeats through unretained; this matches it. They still reach the consumer, because the bridge needs them to re-arm its stall watchdog. Also corrects the attribution on the heartbeat itself. It was described as fixing #2156, and it does not: the reporter's error is emitted after the adapter reads EOF with pending tool calls, while a stall timeout produces response.incomplete with reason upstream_stall_timeout on a path the bridge has already closed. The heartbeat fixes a real false-stall hazard; #2156 needs the reporter's raw SSE comparison before anyone can say what closed that stream.
The badge is "I · <model>" -- a one-glyph marker plus a model id, sitting inside a narrow table column. The glyph is an icon-shaped affordance, not a word, and its meaning is carried by the tooltip (logs.badge.interceptedHelperTitle), which every locale does translate. Localizing the glyph per locale would make the same badge unrecognizable across a screenshot or a bug report while adding nothing to comprehension, so it joins models.shadowCallOriginal on both intentional-English allowlists rather than being translated. The parity tests were right to flag it; English is the intended rendering, which is exactly what those allowlists exist to record.
…r had Six shard failures in three groups (#2152). None came from main..dev; all three needed a different answer, and none of them was skipping a test that can actually run. Group 1, budgets. watchdogMs is a FLOOR, not a multiplier, so a case calling watchdogMs(30_000) still got exactly 30s -- 'Restore truth' failed at 30,147ms. Windows CI now floors at 45s, under the lane's own 60s per-test timeout so a hung test stays bounded. 'A-reduced' was misread in the issue: its 79,978ms was elapsed time against a 150s ceiling, so the outer budget was never the constraint. The real failure was Fixture.request's unscaled 10s AbortSignal, which aborted the case from inside. It is scaled now like every neighbouring budget. 'E' does not start ocx at all. Its lock holder released after a fixed 3s busy wait, and on a Windows shard the contender's process spawn can outlast that -- the parent then sees 'acquired' where it demands 'busy', which reads as a broken exclusion invariant rather than a hold that expired early. The release-marker handshake still ends the hold early everywhere else; only the ceiling moved. Group 2, skip guard. The issue says an unprivileged Windows user cannot create symlinks, but the GitHub runner can -- so canSymlink was true, the cases ran, and they failed on how the preflight reads mode and access through a Windows symlink. Two neighbouring cases in the same file already skip on process.platform === "win32"; these three now use that same guard, and keep the capability check for unprivileged POSIX. Group 3, crash retry. A Bun panic is a crash in the interpreter, not a test result. The macOS leg has carried a crash-signature retry for this; the Windows shards, a separate matrix job with their own one-shot command, had none. They now use the same wrapper, extended with panic(thread since that is the signature this leg actually printed. An assertion failure returns its status immediately and is never retried. What this cannot prove locally: whether 45s is sufficient under real Windows shard contention, the actual skip result on the runner, and PIPESTATUS behavior in Git Bash. Those need a Windows CI dispatch, which is the evidence to look for on this PR.
…able The Windows retry added for #2152 grepped for `panic(thread`. This repository already learned that is the wrong anchor: Bun emits BOTH `panic(thread 2852)` and `panic(main thread)` for the same class of failure, and devlog/_fin/260731_pr_issue_triage_round/050_windows_ci_flake_rca.md names `Internal assertion failure` as the stable fingerprint. Verified by literal probe -- panic(thread 3960) matched, panic(main thread) did not. The shard would have failed on exactly the crash the retry exists for. All three signature lists -- the macOS inline grep, the new Windows one, and is_bun_runtime_crash in run-bun-test-batches.sh -- now carry the same alternatives. The workflow comment already required them to stay in sync; nothing enforced it, so three copies drifted into two shapes. The contract test now pins the sync itself rather than the text, and pins that no list keys on the thread-numbered form. hasShellCommandHead is added because the existing exact-line matcher rejected the `| tee` the retry requires, while still rejecting an echoed or commented-out copy.
fix(ci): give the Windows leg the budgets and the crash retry it never had
…-attribution feat(gui): show and filter intercepted helper requests in Logs
…eartbeat fix(openai-chat): heartbeat while buffering tool-call deltas
…n shards Run 32340498394 dispatched the Windows leg at the release head and produced three results that were not defects in this repository: 1. shard 1/4 was CANCELLED at 15m12s while still executing tests. That is neither a pass nor a fail, and it silently removed the composed-acceptance cases from the evidence. The other shards finished at 14-15 minutes, so 15 was inside the noise band rather than above it. Raised to 25, which still kills a wedged shard and now also covers the second attempt the crash retry is allowed to make. 2. `Responses previous_response_id state > orphan cleanup obeys scan and cleanup caps` ran 100.6s against a 90s budget on shard 4/4 while doing exactly the work it claims: 521 individually fsync'd durable writes. The number was sized from a ~34s windows-latest measurement and was measuring runner contention, not a hang. BULK_DURABLE_IO_BUDGET_MS now carries a Windows-only 180s ceiling, the same shape as the watchdogMs floor. 3. `Claude Code shell-hook reconciliation > does not treat a non-executable claude file as an installed CLI` writes mode 0o644 and expects claudeCodeCliInstalled() to be false. Windows has no execute-permission bit, so accessSync(path, X_OK) succeeds for any readable file and the fixture cannot express its own precondition. It now skips on win32, as several neighbouring symlink cases already do. Refs #2152.
…n-and-budgets fix(ci): stop the Windows leg from truncating and mismeasuring its own shards
|
✅ 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 (2)
📒 Files selected for processing (138)
📝 WalkthroughWalkthroughThe pull request combines CI hardening, provider and Responses routing updates, Codex entitlement gating, OAuth persistence protection, shell-hook reconciliation, request-log metadata, GUI filtering, release idempotency, documentation updates, and regression coverage. ChangesRuntime and provider behavior
Estimated code review effort: 5 (Critical) | ~120 minutes ✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Biome (2.5.6)tests/codex-model-entitlements.test.tsFile contains syntax errors that prevent linting: Line 9: Declarations inside of a 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 |
| if (!antigravityUsesReplayCache(model) || !Array.isArray(parts) || parts.length === 0) return; | ||
| export function observeAntigravityReplay( | ||
| model: string, | ||
| sessionId: string, |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 96f288d595
ℹ️ 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 bareEligibleAccountIds = providerCodexAccountMode( | ||
| OPENAI_CODEX_PROVIDER_ID, | ||
| config.providers[OPENAI_CODEX_PROVIDER_ID], | ||
| ) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined; | ||
| const availableBareGatedNativeSlugs = availableAccountGatedNativeModels( | ||
| modelEntitlements, | ||
| bareEligibleAccountIds, | ||
| ); |
There was a problem hiding this comment.
Include the Direct caller in model discovery entitlements
When the canonical OpenAI provider is in Direct mode and a remote client authenticates with x-opencodex-api-key while forwarding its own ChatGPT Authorization header, request admission correctly checks that caller's roster, but /v1/models restricts gated bare models to __main__. If the local main account is absent or lacks Daybreak while the caller is entitled, discovery hides gpt-daybreak-blue-latest, so Codex cannot select a model that the subsequent request path would accept. Resolve the caller's entitlement for this Direct, non-substitution case, or otherwise keep discovery consistent with resolveCodexAuthContext.
Useful? React with 👍 / 👎.
| const item = typeof rawIndex === "number" && Number.isInteger(rawIndex) && rawIndex >= 0 | ||
| ? backfillOutputItem(event.item, { kind: "index", index: rawIndex }) | ||
| : backfillOutputItem(event.item, nextSyntheticItemSlot()); |
There was a problem hiding this comment.
Preserve synthesized IDs across an item lifecycle
When an upstream omits both item.id and a valid output_index, every output_item.added and output_item.done event calls nextSyntheticItemSlot() independently. The two events for the same logical item therefore receive different IDs, and the terminal snapshot receives yet another index-derived ID, breaking clients that correlate lifecycle events by item ID. Keep a per-stream mapping for malformed-index items so all representations of one item reuse the same synthesized ID rather than allocating a fresh process-global ordinal for each frame.
Useful? React with 👍 / 👎.
Summary
Promotes
dev(96f288d59) tomainfor the 2.28.0 release. 122 commits since8e01dd4e8(v2.27.0): 46fix, 36docs, 4test, 1feat, the rest merges.Why a minor rather than a patch. One user-visible feature landed — the Logs surface now attributes and filters intercepted helper requests (#2179) — and it adds a new column, badge, and filter to a shipped screen. Alongside it, the range carries provider behaviour changes that alter what the proxy sends and accepts, not just what it does on error: xAI OAuth now streams through Responses, Anthropic frames the opening turn, Google replays thought signatures across stream chunks, the Fast capability gate reaches the native passthrough, and
opencode-free/ OpenCode Go are identified by what they actually do rather than by name. A user upgrading gets new behaviour, not only fewer bugs. That is a minor.Security-relevant content in this range, all fixed and audited:
addRequestLogingressThe full five-pass audit of this range is in
devlog/_plan/260820_bug_pr_backlog_consolidation/100_release_audit.md.Verification
96f288d59in run 32344670867.main...dev(git log origin/main..origin/devis empty for every file involved). Three are process-spawn timing on a contended runner; one is an open behaviour question insrc/lab/public/signature.ts. All four are recorded on [Bug][Windows]: six pre-existing CI shard failures (WP13 startup cost, symlink fixtures, Bun panic) #2152 with stacks.platform-windowsisworkflow_dispatch-only andrelease.ymlgates on the push-event run, which is Linux + macOS + gates — that exclusion is deliberate and documented in the workflow.The release version bump itself is made by
scripts/release.tsonmainafter this merges; this PR carries no version change.Checklist
bun run typecheckpasses (green in the gates job at this head)Summary by CodeRabbit
New Features
Bug Fixes
Documentation