Skip to content

[WRONG BRANCH] promote: dev to preview for v2.26.0-preview.20260819 - #2088

Merged
lidge-jun merged 166 commits into
previewfrom
dev
Aug 19, 2026
Merged

[WRONG BRANCH] promote: dev to preview for v2.26.0-preview.20260819#2088
lidge-jun merged 166 commits into
previewfrom
dev

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Promote dev (f2e5e76) to the preview train for the 2.26.0 prerelease.

Head validated: Cross-platform CI success on 7535186 lineage + release-audit fix #2087; lidge full isolate suite 13393 pass / 0 fail on the campaign head; release-regression audit (3 lanes) clear after the staging-hoist fix.

Release: bun scripts/release.ts 2.26.0-preview.20260819 --publish (OIDC).

youngchangjo and others added 30 commits August 16, 2026 08:43
…ng for API-key preset

The `commandcode` (API-key) registry entry was missing the official
model-profile reasoning-facts table that the OAuth `command-code` entry
carries. Two symptoms followed:

1. The Codex catalog advertised no reasoning levels for API-key models
   (deepseek-v4-flash/pro, GLM-5.x), so clients forced effort none and
   requests failed with 400 'messages.content.type is invalid'.
2. The router's known-ids decode source missed the native slash ids, so
   Codex-facing slugs like `commandcode/deepseek-deepseek-v4-pro` were
   forwarded upstream verbatim and rejected with 400 'unsupported_model'.

Also fix the GLM table keys to match the exact upstream ids
(`zai-org/GLM-5.3` not `zai-org/glm-5.3`) and make the effort lookup
case-insensitive, and add GLM-5 / 5.1 / 5.2-Fast (verified high/max from
their official profiles).
…ical behavior

Phase A1 of the FastWire umbrella (#1886): FastWire
descriptors, FastPolicyAuthority capture with a precomputed
modelWireOverrideAllowed guard, the shared resolveFastPolicy() four-level
adapter resolver with capability/eligibility separation, and the TierDecision
state machine. Fast-mode injection no longer mutates parsed._rawBody; the
Responses adapter applies the settled decision to a detached outbound body.
The legacy Chat serializer gate survives as legacyChatEligibility() until the
B1 migration, and no registry provider declares a descriptor yet, so outbound
wire bytes and catalog bytes are unchanged. The A0 characterization suites
pass unmodified except the raw-body observation test whose A0 comment
scheduled this exact update. Full suite at this commit: 12970 pass / 10 skip
/ 0 fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…conversion

chatCompletionsToResponsesBody dropped service_tier, so a tier declared by a
/v1/chat/completions caller vanished before the responses pipeline could see
it — fast override, the capability gate, and serialization all behaved as if
no tier was requested, while the same request through /v1/responses worked.
Copy the field under the converter's existing optional-scalar convention and
let the downstream pipeline keep owning the semantics.

Flips the A0 known-bug characterization for this exact behavior (FastWire
umbrella #1886, independent bug-fix unit), and adds
converter + end-to-end regressions including the fail-closed strip on a
supportsServiceTier:false route. Full suite at this commit: 12750 pass / 10
skip / 0 fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
806/806 green locally is not the same as stable for a Windows user, and the
reason is structural: platform-windows is gated on workflow_dispatch
(.github/workflows/ci.yml:547-552), the aggregation job accepts skipped, and
release.yml:181-201 asks for a push-event CI run that Windows never joins. Every
release so far published without executing a Windows test.

Three independent Pro audits were run against a zip of the v2.24.2 tree with the
GitHub connector, on orthogonal briefs: platform primitives, runtime and
distribution, and user-visible failure modes plus CI coverage. Every finding
carried into this unit was reproduced against the working tree in the same
session; the rest were dropped, including two that turned out to be already
fixed.

Seven verified findings, nine dependency-ordered phases. The sharpest one is
src/service.ts:2361, which uses the exact PowerShell argv that
src/codex/user-identity.ts:222-224 forbids under #1589 -- it survived because
the regression test at tests/windows-deploy-close-regressions.test.ts:43 is
bound to src/update/job.ts alone.

The icacls/CIM request-path latency class is deliberately excluded: both audits
rank it first, but this session measured nothing, and an unverified claim next
to seven verified ones devalues all of them. It is recorded at the end of 001 so
the next cycle inherits it.

No production code changes.
Round r1-20260817113441 returned FAIL with six blockers and four citation
defects. All verified before acting on them; the auditor was right on every one.

Citations corrected in 001: the updater's bare -like match is job.ts:1381 not
:1377, and the service copy's token boundaries are service.ts:2350-2355 not
:2330. Two overstatements withdrawn -- "every release ran zero Windows tests"
ignores that service-shaped releases separately require service-lifecycle.yml,
which does include a Windows job (release.yml:224-234); and service.ts:1983
argues the ACL-is-authoritative principle for an elevation staging directory,
not for credential writers generally.

Sequencing (002) was mostly wrong and is rewritten. Only 030->031 and
060 stage 1->070 are structural; the rest was file-overlap. 010->020 was
backwards, 050->051 was fake and had produced an impossible verification claim,
and "everything before 060" was false since nothing here makes the suite red
today. 060 stage 1 and 051 now start immediately.

040 no longer writes a redacted table or a scratch pointer when it finds an
exposure -- per AGENTS.md that is itself disclosure. It goes silent in the tree
and reports verbally.

060 stops claiming a merge gate it cannot create: dev has no branch protection
(MAINTAINERS.md:121,125). Stage 3 is a convention gate, stage 4 is the real one
because release.yml reads conclusions directly. The hosted-vs-self-hosted
contradiction is now a decision to make before stage 1 rather than two
incompatible sentences.

031 names its module, counters, retrieval path and the constraint that the
publisher label can never be path-derived. 030 specifies a new neutral module
because config.ts:47 already imports config-ownership, so the obvious placement
would close a cycle. 050 picks 600s and spells out the %TIME% midnight-wrap and
space-pad traps. 080 drops to five implementable jobs plus two marked
investigate-and-record.

Missing verification commands added: typecheck on the source phases, full suite
on 030, prepush on the workflow phases per .github/AGENTS.md:25.
Round r2 returned FAIL with five findings. Verified each; all five were real.

The 050 %TIME% advice was wrong in a way that would have failed at runtime.
set /a reads a leading zero as octal, so a minute or second component of 08 or
09 is a hard error -- confirmed on this machine, "set /a a=08" prints Invalid
number. The doc now names four traps (octal, space padding, midnight wrap,
delayed expansion inside the parenthesized branch given the wrapper's plain
setlocal at service.ts:1522) and promotes the state-file approach from fallback
to recommendation, since four traps plus an expansion-mode change is not
arithmetic worth defending in review.

080's self-update job was not testing self-update. ocx update resolves its
target from the registry (update/index.ts:167) and installs a resolved version
(update/index.ts:106); there is no tarball injection seam, so a locally packed
candidate cannot drive the real command. Renamed to a package replacement smoke,
which is still worth having because npm replacing a live global install is the
step that produced #1849, and the doc now says what covering ocx update would
actually require.

031 named a producer but not a consumer. It now names system-routes.ts:49, a
sibling endpoint rather than appending to /api/system/memory, the response
shape, and the route test.

060 no longer says stage 1 depends on nothing and then gates it on the runner
decision. Option 2 is downgraded honestly: the cleanup step removes stale
checkout files, not installed services or registry state, which this product
writes as normal behavior. Option 1 is the only one that closes the
contradiction.

Phase headers on 020, 050 and 080 still claimed dependencies that 002 had
already withdrawn. Synchronized, and 002 now states it is authoritative on what
is structural.
tests/system-routes.test.ts does not exist -- confirmed. Current
handleSystemRoutes coverage sits in memory-watchdog.test.ts:171 and
codex-restart-route.test.ts:11. 031 now says to create the file rather than
extend it, and its verify block runs it.

The state file was oversold in 050. It removes the delayed-expansion problem
because the counter is read fresh each iteration, but it does not remove the
elapsed-time arithmetic: a stored start timestamp still has to be parsed and
subtracted, so the octal, padding and midnight-wrap rules apply either way. The
doc now splits it -- state file for the retry counter, documented arithmetic for
the 600s uptime reset -- and names the file's own questions: location, what
happens when the write fails (treat as a fresh counter, never fail the restart),
and removal on uninstall alongside the wrapper and launcher.
Independent final-gate review returned FAIL with three findings and three
citation corrections. All verified; all correct.

031 claimed privacy:scan enforces the fixed-literal publisher label. It does
not -- privacy-scan.ts:187 is a textual scanner over file content matching home
paths, emails and token shapes, and it cannot see that a runtime value was
path-derived. Replaced with a closed union type so a path-derived string fails
typecheck instead, plus a test asserting the snapshot keys are a subset of it.
privacy:scan stays in the verify block as a backstop, not as the mechanism.

031 also claimed CI would assert the counters stay zero across the Windows
suite. The counters are process-local and the suite runs across four sharded
runners in many short-lived processes with no endpoint to query, so that
assertion needs a suite finalizer and a collection step -- a design of its own.
The CI claim is withdrawn rather than left as an instruction nobody could
follow, and the evidence section now says plainly that local runs and voluntary
bug reports are the only sources.

040's seed list missed config.ts:3937, the invalid-config backup, which copies
the whole config including any secrets in it. Added, and the phase now says to
re-derive every chmodSync call rather than trust the seeds -- an incomplete seed
list is exactly the false negative that phase exists to avoid.

Citations: the updater's bare -like match is job.ts:1383 not :1381; the skipped
allowance is the jq filter at ci.yml:769-772; release.yml service enforcement
runs to :241 with the failure at 235-239.
3937 declares backupInvalidConfig; the chmodSync(backupPath, 0o600) call is
line 3942. Corrected in 001 F5 and in 040's seed list, with the declaring
function named so the citation stays legible if the file shifts.
src/service.ts spawned PowerShell with "-WindowStyle", "Hidden" as argv
elements. src/codex/user-identity.ts:222-224 already forbids exactly that:
Bun 1.3.14 can fail the direct CLI pair before the command runs (#1589), and
the process-level windowsHide flag is what actually suppresses the window.

The call is not decorative. stopServiceIfInstalled() uses it because
schtasks /end can leave the wscript.exe/cmd.exe wrapper alive to respawn the
proxy, and it ignores spawnSync's exit status. Under #1589 wrapper termination
silently does nothing, so ocx stop, restart and update report success without
sticking.

The invariant survived as prose plus a single-file assertion:
tests/windows-deploy-close-regressions.test.ts:43 pins the argv only for
src/update/job.ts (the variable is bound at line 13), so service.ts was never
covered. Replaced with a sweep over every src/**/*.ts, which found exactly one
offender.

The sweep matches the argv form only. Six call sites legitimately pass
-WindowStyle Hidden inside a PowerShell script string handed to Start-Process
(windows-elevation.ts 622/660/687/736, tray/windows.ts:489, update/job.ts:574);
Bun never parses those. A second test pins that discrimination in both
directions so the sweep cannot quietly become vacuous or start failing correct
code.

Driven red first: the sweep reported ["service.ts"] before the fix.

Verification: bun test tests/windows-popup-fix.test.ts (7 pass), bun test
tests/service.test.ts tests/windows-deploy-close-regressions.test.ts
(131 pass), bun run typecheck clean.
Grok CLI was pinned to api_backend = "chat_completions" because opencodex
emitted response.heartbeat as a typed SSE event. That is not a valid Responses
variant, so Grok-build's strict enum deserializer crashed with "unknown variant
response.heartbeat". The keep-alive now emits an SSE comment line instead, which
re-arms the idle timer without triggering deserialization on any client.

With heartbeats fixed, Grok can finally use the Responses passthrough path.
This gives Grok clients the same protocol fidelity Codex already enjoys and
removes the chat to responses translation layer from the hot path.

Some upstream relays (e.g. sub2api) omit annotations on output_text content
parts even though the Responses spec marks it as a required Vec field. Strict
clients, including Grok-build's async-openai fork, fail with "missing field
annotations". A new stateless SSE/JSON backfill adds annotations: [] on any
output_text part that lacks it, on both the streaming and bounded-JSON
passthrough paths. The rewrite is unconditional and safe for all clients
because the field is always valid on the wire.

The /v1/responses handler now surfaces grok-tagged requests as surface=grok
in the log context, matching the chat-completions handler.

Stale comments referencing grok-build's decoder and the old chat_completions
pin have been corrected in the tests.
src/service.ts and src/update/job.ts each carried a copy of the same
teardown logic, and the copies drifted in both directions. service.ts matched
canonical full paths as complete command-line tokens; update/job.ts matched the
bare filenames with -like '*name*'. Meanwhile update/job.ts had received the
#1589 argv cleanup that service.ts had not.

The bare-filename matcher is the defect. Two OpenCodex homes under one Windows
account means a dashboard update for home A can force-terminate home B's
scheduler wrapper, and any unrelated process whose command line contains either
filename matches as well.

Extracted the service.ts implementation, which was the correct one, into
src/lib/windows-service-wrappers.ts and pointed both callers at it. The updater
now passes its own config dir instead of bare names.

windowsWrapperKillScript is exported because the matching rule is the entire
point of the module and the spawn reports nothing: the script it builds is the
only observable surface, which is the same source-level convention
windows-deploy-close-regressions.test.ts already uses.

New tests/windows-service-wrappers.test.ts pins that another home's path is not
among the patterns, that matching is token-bounded rather than substring, that
the caller excludes itself, and that neither file keeps a private matcher. The
last assertion was driven red first: both files failed it before the extraction.

windows-deploy-close-regressions.test.ts asserted "$_.ProcessId -eq $PID"
against update/job.ts. That string moved, so the assertion follows it to the
shared module rather than being dropped.

Verification: bun test over service, windows-deploy-close-regressions,
windows-popup-fix and the new file (143 pass), bun run typecheck clean.
Keep-alives are now SSE comment lines (': opencodex heartbeat') instead of
response.heartbeat events, so the bridge-lifecycle RC3 test and the transport
architecture docs no longer describe a parser-ignored response.heartbeat event.
The grok-build guides' api_backend examples were still chat_completions; they
now match the Responses backend the proxy emits.
…er owning it

src/config.ts:102-123 knew that Windows can refuse rename with EBUSY, EPERM or
EACCES while a scanner or sync client still holds the target, and retried twice
(25ms then 50ms). Nothing else did. Eight durable publishers called renameSync
directly:

- src/codex/prompt-journal.ts, whose journal carries full config.toml bytes --
  losing that publish is what breaks journal restore;
- src/lib/config-ownership.ts, the uninstall manifest;
- src/claude/agents-inject.ts, the generated agent definitions;
- src/lab/automation/persistence.ts and config-persistence.ts;
- src/lab/ledger/purge.ts, the rewritten ledger;
- src/storage/cleanup.ts, both the satellite backup (1094) and the
  restore-pending state file (2438);
- src/tray/windows.ts, the tray's owned-file publisher.

None corrupts anything on failure; they throw rather than publish a partial
file. But under a real-time scanner holding the target they turn a momentary
hold into a user-visible failure, and the tolerance to survive it already
existed one module away.

Three renameSync calls in src/storage/cleanup.ts are deliberately NOT converted.
1639 and 2616 move directories between staging and trash, and 1667 moves one
back on rollback: these relocate a directory rather than publishing a temp file
over a destination. Windows directory-move failures are a different problem with
a different fix, and the callers already handle them.

The loop moves to src/lib/windows-atomic-replace.ts rather than becoming an
export of config.ts: config-ownership.ts is one of the callers and config.ts
already imports config-ownership.ts, so the obvious placement would close an
import cycle. config.ts re-exports renameAtomicFile because callers use it;
renameAtomicFileAsync stays internal, as it was before.

The envelope is unchanged at two retries, and tests/windows-atomic-replace.test.ts
now pins it: which codes count as transient, that POSIX never retries, and that
the bound is two rather than hopeful. The extracted module had no test of its
own before -- config.test.ts exercises it only through atomicWriteFile -- so an
accidental widening would have gone unnoticed. That matters because the next
commit adds counters specifically to decide whether widening is justified.

Verification: bun run typecheck clean; bun test over claude-agents-inject,
windows-atomic-replace, config, storage-cleanup and windows-tray.
…gued from evidence

Both plan audits wanted the 75ms retry envelope widened. Neither could show it
failing in the field, and one explicitly declined to raise its severity for
exactly that reason. Counting is the honest next step: if these stay at zero
across a release the envelope is fine, and if they do not, the change cites
numbers.

Counters are keyed by publisher AND error code. The code is the diagnostic
half: EBUSY from a scanner, EACCES from a permissions problem and EPERM from a
lock are three different stories, and collapsing them would leave the counters
unable to answer the question they exist for. Separate retried and exhausted
totals per key.

Exposed as GET /api/system/windows-replace-retries -- a sibling of
/api/system/memory rather than a field on it, because that payload is
memory-shaped and appending filesystem counters would make both harder to
consume.

The publisher label is a closed union (ReplacePublisher), and that union is the
privacy enforcement, not privacy:scan. The scanner reads file text
(scripts/privacy-scan.ts:187) and cannot tell that a runtime string came from a
path -- a path could carry a username. A closed union makes the same mistake a
typecheck failure instead, which the new test pins with @ts-expect-error.

Every converted publisher carries its own label, so nothing reports under the
"config" default it does not belong to: storage-cleanup for the satellite backup
replace and tray for the tray's owned-file publisher, alongside the four from
the previous commit.

Scope note, stated because the plan originally overreached here: these counters
are process-local. Asserting they stay zero across the Windows suite would need
a finalizer aggregating many short-lived sharded processes, which does not
exist. Evidence comes from local runs and voluntary bug reports.

tests/system-routes.test.ts is new; handleSystemRoutes coverage previously lived
scattered in memory-watchdog.test.ts and codex-restart-route.test.ts.

Verification: bun test tests/system-routes.test.ts (10 pass), bun test
tests/windows-atomic-replace.test.ts (8 pass), bun run typecheck clean, bun run
privacy:scan passed.
Six blockers across three rounds, all verified before acting. The two worth remembering: the counters first collapsed three error codes into one number, and the wrapper tests asserted the generated script contained IndexOf/before/after - which a broken substring matcher would also satisfy. Also records that 030's instruction to sweep for remaining renameSync calls read as complete and was not: six more publishers were left behind, two of them found only in the second review round.
luvs01 and others added 25 commits August 18, 2026 22:59
The public OAuth error projection from #1842 collapsed the fixed
reauth-identity remediation messages (identity mismatch, unverifiable
legacy identity) into the generic authentication failure, so the
dashboard could no longer tell the user to sign in with the selected
account. Represent both outcomes as bounded typed errors
(OAuthReauthIdentityMismatchError, OAuthReauthIdentityUnverifiedError)
whose messages carry no account, token, or email data, allowlist them
in publicOAuthAuthenticationErrorMessage, and cover them in the
projector allowlist and management status-polling regressions.

Resolves the unresolved P2 review on #1842.
Credit: original redaction work by @luvs01 in #1842.
fix(oauth): redact public authentication errors (#1842)
structure/04's native Chat passthrough paragraph claimed canonical Fast
follows the resolved Fast policy without chatServiceTier. The code does
not do that: chat-native.ts has no tier wiring at all — tier resolution
lives only in the Responses pipeline feeding adapter buildRequest, which
the passthrough bypasses. On the native path every caller service_tier
is forwarded raw and only under chatServiceTier: true, and fastMode
injects nothing (openai-chat.ts:121, chat-native.ts:54-72).

Campaign wp9 docs-drift fix; provenance and code-verification record in
devlog/_plan/260818_bug_pr_resolution/040.
docs(structure): scope canonical Fast injection to the bridged Chat path
- 090 (windows program): transactional update with rollback — stage-to-side,
  verification manifest, swap protocol, failure-mode table, boot probe
  (#1942/#1849 remaining half, diff-level).
- 051 (campaign): thought-signature credential scope (keyFor v4 + preferred
  discriminator) and the six-site emit-after-commit barrier design (#1926
  remaining half, diff-level).
- 010-050: campaign phase records (WP-V audit, wp6 redesigns, wp7
  dispositions, wp9 sweep + docs drift, wp10 plan), all audit-reviewed.
docs(devlog): wp10 follow-up designs + campaign phase records
24/24 disposition rows terminal or recorded-hold; WP-V stabilization
(12 failures fixed forward, no reverts), wp6/wp7/wp9/wp10 evidence, and
the wp11 outcome ledger with final gates (lidge decisive run 13316 pass /
0 fail on a5ec641; push CI success on e446607 with docs-only delta).
Security gate: prior public disclosure via issue #1926 (r10-corrected
rationale).
docs(devlog): close the 260818 bug-PR resolution campaign to _fin
feat(clients): ZCode (Z.ai desktop client) managed integration — ocx zcode
#1897 inserted a CCA display-name slug at the TOP of
pickerModelIdForDiscoveredWireId, which returned before every collapse
rule below it. CCA labels a tier row "Gemini 3.7 Flash (High)", and that
slugs to `gemini-3.7-flash-high` — so the picker published one row per
reasoning tier, none of which is a key in ANTIGRAVITY_MODEL_EFFORTS.
The effort ladder was never deleted; nothing could reach it anymore, and
live discovery stamped the split rows with `reasoningEfforts: []`.

Demote the label to a last resort and guard it with
collapsesIntoKnownPickerModel: a slug that reduces to a known base model's
tier is a rung, not a model. The label still resolves an id Google renamed
on the wire while keeping a stable public name, which is the one case no
other rule can answer.

Restore two suppressions the same commit dropped:

- Compatibility aliases must not be admitted as independently discovered
  rows. CCA still serves retired 3.5/3.6 Flash tiers, so admitting them
  republishes exactly the dead ids the alias map exists to retire.
- `gemini-3.1-flash-image` is agent-callable but grouped under image
  generation, so it never appears in agentModelSorts and needs its
  explicit registration back.

Also stop live discovery from overriding a model that owns an effort
ladder. A collapsed row reports ONE representative wire id, so it can name
a rung but never a ladder: gemini-3.1-pro low and high both collapsed onto
gemini-pro-agent, and gemini-3.7-flash sent thinkingLevel=low against the
`-high` wire id — a request that contradicts itself.

Verified against the live CCA :fetchAvailableModels response, not a
fixture: 3.7 Flash low/medium/high each reach gemini-3.7-flash-tiered with
the matching thinkingLevel, 3.1 Pro splits low/high across its two wire
ids, and a saved gemini-3.6-flash-high still routes to 3.7 carrying its
tier.

The PR had frozen the regression into three tests (including renaming
"stale discovery cannot republish" to "live discovery follows"), so those
expectations are restored to the original contract.
Co-authored-by: Ingwannu <ingwannu@users.noreply.github.com>
…ever survives nameless (#2061)

* fix(codex): strip app-rewritten provider sub-tables so the provider never survives nameless

A Codex app config rewrite re-serializes the provider's inline
env_http_headers table into a separate [model_providers.opencodex.env_http_headers]
sub-table. removeOcxSection() ends its removal scope at the next table header,
and the sub-table header is one, so an inject/restore removed the main table
(with name/base_url) but kept the sub-table: model_providers.opencodex then
exists with only env_http_headers and no name, and Codex rejects the whole
config on startup ('provider name must not be empty'). Because every cleanup
guard matched the exact string '[model_providers.opencodex]', the orphan never
matched again, was journaled as baseline, and re-persisted on every
inject/restore cycle.

Recognize [model_providers.opencodex.*] sub-table headers in removeOcxSection()
and in the three cleanup guards (injectCodexConfig, stripOpencodexConfigResult,
hasOpencodexRouting) so the main table and its sub-tables are removed together.
A user's similarly named table ([model_providers.opencodex_backup]) stays out
of scope: the match requires the 'opencodex.' dot boundary.

* fix(codex): recognize trailing comments on provider table headers

TOML v1.0 permits a comment after a table header, so
[model_providers.opencodex] # comment is a valid root header form. The exact
string compare missed it, skipping provider cleanup on inject and restore and
letting hasOpencodexRouting miss the table. Match the root header by regex
with optional whitespace + trailing comment; the sub-table prefix check
already tolerates trailing comments by construction. Regression tests for
commented root and sub-table headers.

---------

Co-authored-by: jzli <jzli@alauda.io>
…2066)

Claude-on-Antigravity rejects a history that ends on a model turn as
assistant prefill. Context compaction and interrupted-turn replay can
produce that shape. Mirror the anthropic adapter's user (continue)
nudge on the CCA Claude path only.

Closes #2065
Co-authored-by: Ingwannu <ingwannu@users.noreply.github.com>
…t wire (#2042)

`noStructuredOutputModels` is documented, in every locale, as "Exact model IDs
whose `openai-chat` endpoint rejects `response_format`. Only an exact
requested-model match omits the field; structured-output translation stays
enabled for every other `openai-chat` model." The Responses ingress enforces
that, and tests/openai-chat-hardening.test.ts already pins a `:tag` sibling
keeping the field there.

The native Chat passthrough added in #1467 matched through `modelInList`
instead, which also matches the pre-colon prefix. On a provider that serves
Ollama-style tags -- ollama-cloud ships `gpt-oss:120b`, `qwen3-coder:480b`,
`qwen3.5:397b`, `gemma4:31b` -- a `noStructuredOutputModels: ["gpt-oss"]`
entry therefore stripped `response_format` from `gpt-oss:120b` on
/v1/chat/completions while /v1/responses kept it. The caller asked for JSON
and silently got prose, on a model the operator never opted out.

That is the failure #1424 called out when it chose the exact boundary:
a wider match "would silently return prose for siblings that support JSON
Schema". The sibling gates on the lines above keep `modelInList` --
`noVisionModels` is documented as tolerating an Ollama `:size` tag, this one
is not -- so the comment now says why this gate differs.

Four tests, next to the existing Responses-side assertions so the two
ingresses read as a pair: exact id opts out, a `:tag` sibling does not, the
full `:tag` id does when listed, and an unrelated model is untouched. The
`:tag` sibling case fails on current dev.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…them (#2059)

`resolveProductionBehaviorValues` is documented as "authoritative effective
values emitted by the production route/model/adapter resolver", and its hash
is the behavior fingerprint that keys Lab evidence. Ten of its rows are
membership tests over the provider's `no*Models`-style lists, and they went
through a local

    function includesModel(list, modelId) {
      return Array.isArray(list) && list.includes(modelId);
    }

while every runtime gate those rows describe matches through `modelInList`,
which also accepts a bare entry for a tagged id.

ollama-cloud serves `gpt-oss:120b`, `qwen3-coder:480b`, `qwen3.5:397b` and
`gemma4:31b`, and the same registry row writes the bare `gpt-oss` into
noVisionModels -- the bare-prefix form is how these lists are meant to be
written. So with `noTemperatureModels: ["gpt-oss"]` the adapter omitted
temperature from the request for `gpt-oss:120b` while the report said
`sampling.omitTemperature: false`. Same for omitTopP, omitPenalties,
reasoning.budgetMode, reasoning.splitMode, reasoning.toggleMode,
reasoning.supported, reasoning.replayMode's two flags, and
tools.choiceRestrictions.

The blast radius is confined to subjects that were being described wrongly.
Fingerprints measured on the same config, before and after:

    gpt-oss:120b   54154e19bd2c8ee4 -> 45a73577c5c257f9   (was wrong)
    gpt-oss        45a73577c5c257f9 -> 45a73577c5c257f9   (unchanged)
    glm-5.3        54154e19bd2c8ee4 -> 54154e19bd2c8ee4   (unchanged)

Note the first line against the third: a model whose sampling gates were
applied hashed identically to one where they were not. It now hashes with
`gpt-oss`, the id it actually behaves like. Recorded evidence for unaffected
subjects keeps its fingerprint, so `resolverVersion` is left at 2 rather than
invalidating every recorded subject globally -- say the word if you would
rather draw a clean generation boundary and I will bump it.

Tests assert the wire the adapter really builds first, then hold the report to
that same wire, so the pair cannot drift apart silently. The five report cases
fail on current dev.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ingwannu <ingwannu@users.noreply.github.com>
* feat(cursor): add HTTP/1.1 compatibility transport

* feat(gui): expose Cursor HTTP transport setting

* fix(cursor): harden HTTP/1.1 failure handling

* fix(http): reject unsupported version pin targets

* fix(cursor): close HTTP/1.1 transport races

* fix(cursor): pace HTTP/1.1 compatibility requests

* docs(cursor): document HTTP/1.1 alias
Co-authored-by: Ingwannu <ingwannu@users.noreply.github.com>
…#2076)

* docs(devlog): 260819 triage-execution campaign roadmap (r2-audited)

* fix(codex): emit modern env_key admission line on non-loopback inject

codex-cli 0.146+ documents env_key as the bearer auth form for custom model
providers: it sends Authorization: Bearer $VAR and hard-errors on a missing
variable instead of silently omitting the header. The server side already
admits and substitutes this bearer (#1686 chain), so the injector was the
stale half. Loopback Design B still emits no auth line; the #2061 orphan
sub-table cleanup keeps handling historic env_http_headers configs.

Closes #2073

* test(codex): pin non-loopback legacy env_http_headers -> env_key upgrade
…d persist visibility (#2078)

* docs(devlog): #2064 RCA — fixed on dev by #2016, version-ancestry proven

* fix(replay): scope durable thought signatures per credential and bound persist visibility

The durable thought-signature store keyed entries by thread + destination +
model but not by credential, so account A's Gemini signatures could replay
under account B on the same destination (#1926 gap 1). Keys now carry a
salted-HMAC credential identity (installation-local salt persisted beside the
store, full 256-bit output — never an unsalted digest of key material) derived
from the persisted OAuth account-slot id, the API key, or the Codex account
handle; a scope that cannot produce one fails closed instead of sharing a
durable slot. STORE_VERSION 3 -> 4 drops old rows on load (not upgradable —
no credential info was recorded).

Gap 2: terminal frames could become externally visible before the queued
signature persist settled. All async terminal paths (completed, truncation
and adapter-EOF incompletes, failed) and both buffered JSON returns now await
a bounded (250ms) durability barrier; the sync stall-timeout kill path keeps
the pre-existing best-effort behavior, documented in place.

Closes #1926

* fix(replay): wire durable lookup at adapter serialization; trust only pool account handles; harden salt perms

Security-review fold-back: (1) the parser's durable lookup ran before the
credential scope was bound, so the store was write-only at runtime — the
google adapter now falls back to the durable store at serialization time,
when the scope identity exists (regression-pinned); (2) the codex-forward
durable handle no longer accepts the client-supplied chatgpt-account-id
header (trusted pool context only; direct-forward fails closed); (3) the
salt file re-asserts 0600 on every load.
…2079)

* fix(update): stage, verify, and swap npm self-updates with rollback

The npm self-update installed straight into the live global tree, so any
failure after npm removed the old files left a file-less package skeleton
with no recovery (#1849) and nothing verified the new tree before it went
live (#1942). The launcher now stages the target version into a sibling
directory (npm --prefix, same volume), verifies a manifest inside the stage
(package.json version, launcher integrity, sentinel deps), moves live aside
to a sibling backup, swaps the staged tree in, re-verifies, and rolls back by
reverse rename on failure — writing a recovery marker with a one-line restore
on double fault. A boot probe restores the newest backup over a broken live
tree (power loss mid-swap) and reaps stale backups once live verifies.

Staging and backup are SIBLINGS of the package dir, never children — a child
would travel with the live rename and the live tree cannot move into its own
subtree (design defect caught by the plan audit and fixed here).

Closes #1942
Closes #1849

* fix(update): harden transactional swap per adversarial review

- verify the bundled Bun binary by size (>=10MB), not just its package.json,
  so the boot probe can never call a Bun-less tree healthy and reap the only
  backup
- bounded EPERM/EBUSY/EACCES rename retry for the Windows AV/indexer class
- staging/backup mkdir failures return phase errors with live untouched; an
  unexpected throw now reports and stops instead of falling back to the
  destructive in-place install
- the Windows service wrapper (outside the package tree) restores the newest
  .ocx-backup-* sibling before declaring the install incomplete — the exact
  power-loss window the in-launcher probe cannot reach
… contract (#2081)

* docs(devlog): 260819 triage-execution campaign records (010/020/030)

* test(update): re-pin launcher invariants to the transactional install contract

#2079 replaced the direct global npm install spawn with the staged
transactionalNpmUpdate call, breaking three source-invariant pins that
anchored on the removed spawn line (dev-head CI run 32204396229). The
invariants themselves still hold — stop precedes the destructive step, the
history-restore warning precedes it, and every npm spawn goes through the
hardened npmInvocation resolver — so the pins now anchor on the transactional
call and the runNpm callback's invocation spawn.
…ge (#2087)

Release-audit blocker: local-style npm install hoists bun/zod to
stageRoot/node_modules, so the swapped-in package directory shipped without
its dependencies and every post-upgrade 'ocx update' failed verification.
Stage with npm install -g --prefix instead — the global layout nests
dependencies inside the package exactly like the live tree — and resolve the
staged path across the POSIX lib/node_modules and Windows node_modules
shapes. Proven against the real registry: staging @bitkyc08/opencodex@2.25.0
verifies and swaps end-to-end with dependencies inside the package.
@lidge-jun
lidge-jun merged commit f4cc168 into preview Aug 19, 2026
24 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot changed the title promote: dev to preview for v2.26.0-preview.20260819 [WRONG BRANCH] promote: dev to preview for v2.26.0-preview.20260819 Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.