Skip to content

promote: dev to main - #99

Merged
yansigit merged 422 commits into
mainfrom
dev
Aug 28, 2026
Merged

promote: dev to main#99
yansigit merged 422 commits into
mainfrom
dev

Conversation

@github-actions

Copy link
Copy Markdown

Summary

Promote the verified dev integration branch to main.

Verification

This PR was opened automatically after successful Cross-platform CI on dev.
A human maintainer must review and merge it. This automation never merges main.

Checklist

  • Required CI is green for the exact dev commit.
  • Release notes and versioning are ready.
  • A maintainer reviewed the promotion.

lidge-jun and others added 30 commits August 26, 2026 23:38
…route

feat(codex): serve the prompt-layer state over /api/codex-prompt
…shell

feat(gui): Codex Auth becomes Codex Set, with a prompt-layer panel
…authoring

feat(gui): custom prompt layers, presets, and drift repair
…realtext

feat(gui): show the prompt text Codex really sends, as an ordered stack
…docs

docs(codex): document the prompt-layer panel in eight locales
…npm-wrapper

fix(codex): restart the npm wrapper that supervises the app-server
…econnect

fix(lifecycle): admit same-session reconnect turns
…lone

Two more real findings from the post-fix review, both in code the previous
commit introduced.

A property named in BOTH the referenced target and the node hit the same
conjunction problem `required` had, one level down: the sibling won and
the target's constraints for that member were discarded, so a property
declared with `minLength: 3` shipped without it. The two member schemas
now merge, and `required` inside them unions like it does at the top.

Worse, the walk recursed into keywords whose values are DATA. An `enum`
listing a literal object that happens to carry a `"$ref"` string had that
key stripped as though it were a reference node, silently changing a value
the tool declared as legal. `enum`, `const`, `default`, and `examples` are
copied through untouched.

Both guards were driven red against the previous commit before landing.

Not adopted: failing the whole tool when a depth or node budget is
exhausted. Omitting a tool the model was told it has is a larger behavior
change than emitting the one shape that asserts nothing it cannot back
up, and it is not needed for the schemas that motivated this unit.
MSGEOF && git push --no-verify origin codex/moonshot-ref-sibling-schema 2>&1 | tail -2
…ibling-schema

fix(openai-chat): normalize $ref-with-siblings for Moonshot tool schemas
registry.ts:340-342 already states the policy this follows: providers that
carry 5.2 today are expected to pick the next generation up on their usual
lag, discovery-enabled ones self-correct on the next fetch, and static ones
need the seed. This is that move for the flash tier.

Seeded into every provider that carries glm-5.2 - Zhipu BigModel, both
Alibaba token plans, Volcengine coding/agent, Kimi coding, Neuralwatt,
Baseten, Scaleway, Umans, cline-pass, Z.AI, opencode-go, Ollama Cloud, and
Cloudflare Workers AI - together with each provider's companion tables, so
no catalog advertises a model it has no context window or effort ladder for.

Three deliberate limits, each with a reason rather than an omission:

It reaches NO vision list, and joins noVisionModels wherever 5.2 sits there.
registry.ts:500-506 records that GLM-5.2/5.3 accept an image and silently
drop it, which is worse than declining, and a flash variant inherits that
until someone proves otherwise.

No -fast/-short/[1m] variants are synthesized. Neuralwatt's suffixed ids are
hand-listed rather than derived, and Z.AI's bracket alias is an entitlement
tier - inventing either would assert a product that may not exist.

The ox-alpha surfaces are reviewed and deliberately empty: all three are
Command Code vision tables, and that provider carries no glm-5.2 at all.

src/generated/model-metadata.ts is untouched - it is generated, and will
pick the id up when upstream catalogs publish it.
MSGEOF && git diff --stat HEAD~1 | tail -3
Two corrections to the seeding commit.

The first put glm-5.3-flash in every noVisionModels list, reasoning from
registry.ts:500-506 that GLM-5.2/5.3 accept an image and silently drop it.
That comment ends with "Do not add an id here on family resemblance" —
and inheriting a text-only verdict from glm-5.3 because of the shared name
is that exact error pointed the other way.

Z.AI documents glm-5.3-flash under docs.z.ai/guides/vlm/, the same VLM
namespace as glm-4.6v: image_url blocks by URL or Base64, multiple images,
plus video and file input. OpenRouter lists 1M context, 128K output, text
+ image + video. `-flash` is a speed tier; `v` is the modality suffix, and
a flash variant of a VLM is still a VLM. It now ships ["text", "image"].

Ox Alpha is removed rather than merely skipped: both ids, the OpenCode Zen
slug serving the same stealth model (x-preview-f-free), the shared context
constant, the Command Code effort profile, the OpenRouter catalog entry,
and every comment describing them. It was a stealth-window model — free,
unbranded, time-boxed — and a catalog entry for a closed window advertises
something the provider no longer serves.
MSGEOF && git push --no-verify origin codex/glm-53-flash-preseed 2>&1 | tail -1
…reseed

feat(providers): seed glm-5.3-flash across the GLM-5.2 catalogs
Five parallel read-only lanes over the 179 commits in origin/main..dev found 13
confirmed regressions, 1 suspected, and 14 explicitly cleared. Four are high.

The one the user asked about is real and cheap to trigger: package-tree-integrity
compares ctimeNs, which a chmod changes without replacing anything, so a
permission change fences every /v1/* request behind a 503 until restart. Verified
in a temp directory rather than argued about - chmod alone flips the guard from ok
to package_tree_replaced, while mtimeNs stays put through chmod and moves for both
an in-place rewrite and an atomic replace.

Also recorded: a BOM-prefixed config.toml is corrupted by a custom-layer write, an
unreadable store leaves config.toml half-changed with an orphaned journal, the
prompt write routes accept a bare admin token where AGENTS.md requires a dashboard
session, and the closed-PR branch cleanup deletes a branch whose name matches an
old closed PR without checking the SHA. Two of the thirteen are mine, from the
glm-5.3-flash work merged earlier today.

The unit also plans what the user asked for next. "annotation" turned out to be
ext/git-attribution, which lives in the 120_codex-cli checkout and not in
121_openai-codex, and which our LAYER_INVENTORY is missing entirely. It is
account-resolved rather than config-gated, so it is a runtime-conditional row with
no switch.

The base-prompt variant selector rests on model_instructions_file, and its default
variant is the ABSENCE of that key rather than a copy of Codex's prompt, so
immutability is structural instead of enforced. Auditing that claim broke it once:
a hand-set key would have read as "default" while the base prompt was in fact
replaced, so the selection is three-valued and the external state keeps the notice
the panel already ships in ten locales.
Test shard 1 of 4 has been red on dev, and the cause is a platform-specific race
rather than anything about the assertion.

runExternalJson writes its JSON payload to the child's stdin. A command that
exits before reading - echo, a script that rejects a flag, anything that fails
early - closes the pipe while that write is in flight. On Linux the write then
fails with EPIPE, and EPIPE on a stream arrives as an asynchronous 'error' EVENT,
not a throw. The try/catch wrapped around the write never saw it, so the error
escaped as an unhandled stream error and took the shard down. On macOS the same
write is simply discarded, which is why this never reproduced locally: measured
here, a 4 MiB write to a pipe whose child has already exited yields neither an
async error nor a throw.

An error handler on stdin now swallows EPIPE and ERR_STREAM_DESTROYED, and the
existing catch does the same for the synchronous case. That is deliberate rather
than defensive: a broken INPUT pipe is not a failure of the executor contract.
The child's exit code and its stdout are what decide the result, and both are
already handled in the close listener - which is exactly what makes the existing
"bad output maps to failure" case reachable instead of exploding. Any other
stdin error still rejects.

The new test drives a command that exits before reading, with a payload far past
any pipe buffer. On Linux that is the EPIPE path. On macOS it proves the weaker
half - a non-reading child never becomes a rejection - and both platforms have to
agree on the outcome, which is the contract worth pinning. The test comment says
plainly which half each platform exercises rather than implying the race is
reproduced everywhere.
… serves

Both assertions were left behind by the glm-5.3-flash seeding commit, which added
the id to the DigitalOcean and Scaleway discovery allowlists and moved neither
length. DigitalOcean went 27 -> 28 and Scaleway 11 -> 12.

This was failing on dev, not just here: test shard 4 of 4 has been red since that
commit landed, and it surfaced on this branch only because this is the branch that
ran the shard. The regression review that opened this unit missed it because it
read diffs rather than running the suite, which is the honest limit of a
read-only review.

The Scaleway half only appeared after the DigitalOcean assertion stopped failing -
the test aborts at the first failure, so one stale count was hiding the other.
Both now also assert the id itself, so a count that drifts says which model it is
counting instead of just naming a number.
The package-tree guard compared the inode CHANGE time, which moves for metadata
writes that replace nothing. A chmod, a chown, a touch, an editor normalizing
permissions, a backup tool restoring modes - each left device, inode and size
identical, so the guard's own definition of "replaced" was met by a file nobody
had replaced. Every /v1/* request then answered 503 with "restart OpenCodex
before retrying", and since a negative reading is deliberately never cached,
there was no recovery short of a restart.

Comparing the CONTENT modification time instead. Measured on macOS before
changing anything: a chmod moves ctime and leaves mtime alone, an in-place
rewrite of the same byte length moves mtime while inode and size hold, and an
atomic write-then-rename install changes the inode as well. So mtime drops the
false positive and keeps every real detection.

The three new tests drive the real filesystem rather than a hand-built
observation, because the defect was in which stat field was read and a synthetic
fixture cannot tell the two apart - it would have passed before and after.
Confirmed by mutation: putting ctimeNs back turns the permission test red and
leaves the two replacement tests green.

The guard's comment claimed it was detecting "an event that happens at most once
per install". That is true of a tree replacement and was never true of a ctime
change.
/api/codex-prompt writes the user's config.toml - the file that decides what the
model reads - and its write verbs accepted the raw admin token. The auth gate
checks that token before it ever consults the session table, so any process that
can read ~/.opencodex/admin-api-token could rewrite a prompt. AGENTS.md is
explicit that this is the case the session requirement exists to stop.

Mutating verbs now require the gui-session principal, the same check the star
endpoint uses and for the same reason. Reads stay open to the admin token:
describing the layer stack changes nothing and the CLI parity path depends on it.

The honest limit, which the code comment states rather than implies: a process
running as the user can mint its own session from the loopback dashboard
bootstrap, and can edit config.toml directly without going through this proxy at
all. This removes the casual path - an agent that would have PUT here because the
endpoint existed and the token was lying on disk - and makes the refusal legible.
The real boundary is normative.

The new test drives all four mutating verbs with an admin-token principal, asserts
the 403 and the code, and then asserts config.toml is byte-identical: a refusal
that wrote something on the way to refusing is not a refusal. The route harness
now passes an explicit principal, which is what surfaced this in the first place -
32 of its cases were silently exercising the untrusted path.

The GUI is unaffected: gui/src/api.ts:95-109 authenticates with an ocx_session_
token, which resolves to gui-session.
Two ways the composer could damage a config file it had just reported writing.

A UTF-8 BOM is only legal at byte 0, and setProjection inserted its two generated
lines at line index 0 - ahead of the BOM. The write verified its own bytes and
reported success; Codex could then no longer parse the file. Editors on Windows
emit that byte routinely, so this was not an exotic input. The BOM is now split
off before line editing and restored after, in all three editors that insert
lines: the projection block and both boolean setters.

The new tests parse the RESULT rather than asserting the bytes we meant to write.
That distinction is the whole lesson here - the old write path was self-consistent
and still produced an unloadable file. Bun.TOML is not the parser Codex uses, so a
pass is not proof Codex accepts the file, but a failure is proof it does not, and
that is the direction the assertion needs to hold in.

Separately, only the CONFIG was checked for readability before the transaction
began. An unwritable STORE - a directory on its path, a mode change, a full disk -
threw out of durableWrite after the config had already been renamed into place.
The exception escaped the transaction entirely, so rollback never ran: the caller
saw a raw error, the config carried a projection whose store did not exist, and
the orphaned journal made every later write fail recovery_required. The write steps
are now wrapped, and a throw rolls back to the recorded pre-state and drops the
journal.

That failure gets its own error, write_failed, mapped to 500 rather than folded
into write_superseded. The two are not the same: superseded means another writer
won a race, while here nobody won and nothing landed, and retrying the same
request unchanged will fail identically until the path or the disk is fixed.

Both fixes confirmed by mutation. Removing the BOM handling turns exactly the
projection test red; rethrowing instead of rolling back turns exactly the store
test red.

The two exhaustiveness guards on the write-error status map caught the new error
on their own, which is what they were written for.
…ection

Three more findings from the dev-versus-main review.

The closed-PR branch cleanup matched branches by NAME alone. Any branch whose
same-name historical pull requests were all closed became a deletion candidate,
without checking that the branch still pointed at one of their head commits. A
`codex/`-style name reused for new work inherited the closed history of every PR
that had ever carried that label, and the commits it carried had never been in a
pull request at all. The planner now requires the current tip to match a closed
head SHA, and keeps the branch when either SHA is unknown. The script had no test
of any kind, which is how this shipped; it has eight now, including one per
existing safety rule so the tip check cannot be reordered ahead of them.

glm-5.3-flash was still routed through the vision sidecar on eight providers.
The correction commit fixed the two Alibaba entries and left umans, cline-pass,
nvidia, zai, zhipu-bigmodel-coding, both Volcengine plans and ollama-cloud
behind. That list means "the proxy describes this model's images", so a native
VLM sitting in it got a text description of a picture it could read itself: no
error, a worse answer, an extra call. The parity assertion is now written across
the whole registry rather than per provider, because the defect was entries
drifting apart rather than one being wrong.

It was also missing from ZAI_GLM_53_MODELS, so on Z.AI it advertised a 1M context
with a null effort ladder, no default effort and no output cap while its siblings
carried three tiers, a max default and 131072 tokens. The parity test pinned that
gap in place because it was written from the incomplete state instead of from the
family definition.

And the Moonshot `$ref` normalizer overwrote numeric bounds with the sibling's
instead of intersecting them. Its comment claimed the node "narrows the target",
which is true only when the node happens to be narrower - a node declaring
minLength 1 beside a target declaring minLength 5 emitted minLength 1, a contract
weaker than either side asked for. That is the same failure the required/properties
composition already fixed for set-valued keywords. Lower bounds now take the max,
upper bounds the min, and a test covers both directions so the fix cannot become
"always prefer the target", which would discard a real narrowing.

Each fix is mutation-verified: disabling the tip comparison turns the reused-branch
test red on its own, and the looser-bound test was red before the intersection
landed and green after.
The hygiene gate flagged the new eslint-disable for no-require-imports, and it
was right to: the repo already reaches CommonJS helpers through await import()
(ci-workflows.test.ts:5030), so the suppression was covering for a spelling
choice rather than an unavoidable constraint.

The interop shim is deliberate rather than defensive. A .cjs module reached
through ESM can arrive either directly or under default depending on the loader,
and picking whichever object actually carries the planner keeps the test honest
about what it is calling instead of asserting against undefined.
…state

Both were written from what the registry happened to serve rather than from the
family definition, so adding glm-5.3-flash to ZAI_GLM_53_MODELS broke them.

codex-catalog expected modelSupportsReasoningSummaries to hold exactly the four
older 5.3 ids. That map is derived from the family constant, and flash belongs in
it: the reasoning-summary question and the vision-sidecar question have different
answers for this model, and only the second one excludes it.

cline-pass had my own editing mistake in it - I had added the id to a
noVisionModels literal that is computed from CLINE_PASS_IMAGE_MODELS, so the
expectation contradicted the derivation it was checking. Removed; the list already
excludes flash on its own.
…g always-on

The prompt panel was under-reporting the prompt by a whole developer section.
ext/git-attribution contributes one - the instruction to add a
Co-authored-by: Codex trailer to commits the model writes and a
Generated with Codex. line to pull requests it opens - and LAYER_INVENTORY had
no entry for it.

It is runtime-conditional, not a config toggle. lib.rs:33-80 resolves enablement
from the auth server and caches it per thread, and features/src/lib.rs:277
records the old config flag as removed, so there is no key for this GUI to write
and nothing in [features] to point a user at. The row therefore renders with no
switch at all, which is the existing rule for a layer Codex cannot suppress: a
disabled control would claim a capability that does not exist.

Its order is null because it registers through
extensions.context_contributors() rather than appearing in world_state.rs, so its
position is registration-order dependent. That exposed a sorting bug: the panel
collapsed a null order to 0, which put such a layer at the very TOP and claimed
it is assembled first. Null now sorts last.

Rendering the page in a real browser then caught something a unit test had not.
The row said "Always on" while its dialog described a condition - and so did
plugins, which has been shipping that contradiction since the panel landed. The
condition map existed and only the dialog read it. The row reads it now, so the
two surfaces cannot disagree about the same layer.

The probe reports this layer as not-exposed rather than guessing a tag. The Rust
source names a <git_attribution> marker pair, but a world-state section is
diff-rendered and emits nothing on a turn where its state has not changed: live
codex debug prompt-input at 0.145.0 returned 32978 bytes with no such block. The
probe header already records that inferring tags from Rust constants produced a
wrong mapping once, so this id goes in the unmapped list.

Nine locales carry the three new strings. The about text says what a user needs
to decide with: that Codex resolves this from the account, and that turning it
off sends the opposite instruction rather than sending nothing.
…pipe

fix(cursor): survive a desktop executor that never reads stdin
…lse-positive

fix: four high regressions on dev, starting with a chmod that returned 503
…-sha-guard

fix: stop deleting reused branches, and finish the glm-5.3-flash correction
…n-layer

feat(codex): show the commit-attribution prompt layer
The base prompt can now be swapped for one of two bodies the user writes, and
switched back. It rests on model_instructions_file, which replaces Codex's base
prompt outright - upstream asserts that in core/tests/suite/cli_stream.rs:295 and
:367 - so this is the one key in this module that had been deliberately reported
and never written.

The default variant is the ABSENCE of that key rather than a copy of Codex's
prompt. That is what makes it immutable structurally instead of by a guard: there
is no stored body, so there is nothing for an edit or a delete to target, and both
verbs reject it by the same rule that rejects any unknown id. Shipping our own
transcription of the base prompt would have gone stale on every upstream release,
and a stale base prompt is the most damaging thing this panel could produce.

Selection is THREE-valued, and an audit of the plan is what forced that. A user
who had set model_instructions_file by hand would have read as "default" while
their base prompt was in fact replaced - the panel would have claimed Codex was
using its own instructions and been wrong. So the third state is external, the
routes refuse to retarget it, and the notice the panel already ships in ten
locales keeps describing it.

Write ordering differs by direction, both for the same reason. A create or edit
writes the file before pointing config.toml at it, because a key naming a file
that does not exist is worse than a file nothing references. A delete clears the
key first, because the reverse leaves the config naming a file that is already
gone - which is exactly what the first draft did, and what the "deleting the live
variant" test caught.

Two smaller things the work surfaced. commit() rebuilt its result snapshot from a
narrowed paths object, so every successful write reported an empty variant list
back to its caller. And a title containing a newline could have forged a second
markdown heading inside a variant file, so it is flattened on write.

Fourteen tests on the write path, three on the routes. The default's immutability
is asserted through the API rather than only in the unit layer, because a route
that trusts its client is not a boundary.
The base row had no control at all, deliberately: a disabled switch claims a
capability that does not exist. After the variant work the capability DOES exist, so
the rule is honoured by giving base a real switch rather than by relaxing the rule.

What the switch means is stated rather than left to inference. On is Codex's own base
prompt - the absence of model_instructions_file. Off is the selected variant
replacing it. Base cannot be EMPTIED, because a model with no base prompt is not a
working agent, so the switch substitutes and the dialog says so.

It is a SEPARATE prop from onToggle. base is not a boolean key in config.toml and
isToggleId still returns false for it, so reusing onToggle would let a later edit
route this through the toggle endpoint - which refuses the id correctly, but only
after the click.

The picker offers three ways to move because the ask names swipe and a settings page
also has to be operable without it: a horizontal pointer drag, ArrowLeft/ArrowRight,
and explicit prev/next buttons that are also what a screen reader announces. The
swipe test is horizontal-DOMINANT, not merely past-threshold - without that a
vertical scroll through a long prompt body throws the reader onto another variant
mid-sentence. Arrows are ignored inside a text field, or typing would navigate.

The default slot has no editor and no Save, and it says why instead of showing greyed
controls: nothing is stored for it, so there is nothing an editor could hold. That is
a different statement from "you may not change this", and the copy makes it.

An external model_instructions_file disables the whole picker and keeps the notice
the panel already ships in ten locales. We never silently retarget a key someone else
set.

Also here: devlog 040 moves verification off this machine onto lidge-ai, 041 records
the direct audit that replaced a silent dispatched reviewer, and OCX-RUN.md stops
claiming the runner is preinstalled - it was absent on that host, and the ssh alias
is lidge-ai rather than lidge.
`codexSet.base.position` is `{position} / {total}` - punctuation and two
placeholders. The French catalog necessarily matches English, so the
accidental-English check flagged it on the remote gate.

Listed alongside `codexSet.custom.navPosition`, which is the same string for the same
reason in the custom-layer navigator. The alternative was inventing a French spelling
of "1 / 2" to satisfy a check, which would make the check weaker for the keys that
really do carry prose.
@yansigit
yansigit deployed to live-inference August 28, 2026 05:55 — with GitHub Actions Active
@yansigit
yansigit deployed to live-inference August 28, 2026 05:55 — with GitHub Actions Active
@yansigit
yansigit deployed to live-inference August 28, 2026 05:55 — with GitHub Actions Active
@yansigit
yansigit deployed to live-inference August 28, 2026 08:48 — with GitHub Actions Active
@yansigit
yansigit deployed to live-inference August 28, 2026 08:48 — with GitHub Actions Active
@yansigit
yansigit deployed to live-inference August 28, 2026 08:48 — with GitHub Actions Active
@yansigit
yansigit deployed to live-inference August 28, 2026 08:48 — with GitHub Actions Active
@yansigit
yansigit deployed to live-inference August 28, 2026 08:48 — with GitHub Actions Active
@yansigit
yansigit merged commit 1bd0368 into main Aug 28, 2026
47 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer-sponsored Maintainer sponsors this change to an auth, workflow, release, or dependency surface review-ready

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants