Conversation
…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.
…nto codex/resolve-sync-108
fix(codex): default upstream responses to HTTP SSE and harden WebSocket transport
# Conflicts: # docs-site/src/content/docs/reference/configuration/providers.md # docs-site/src/content/docs/reference/proxy-formats.md # tests/ws-upstream.test.ts
fix(ci): revalidate sync gates after CI completes
sync: upstream v2.34.0 (80fff9a)
…ckmerge-main-supervisor
Back-merge main supervisor hotfix into dev
fix: preserve native login error responses
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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