Skip to content

fix: stop deleting reused branches, and finish the glm-5.3-flash correction - #2691

Merged
lidge-jun merged 6 commits into
devfrom
codex/branch-cleanup-sha-guard
Aug 26, 2026
Merged

fix: stop deleting reused branches, and finish the glm-5.3-flash correction#2691
lidge-jun merged 6 commits into
devfrom
codex/branch-cleanup-sha-guard

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

Stacked on #2689. Three more findings from the same dev-versus-main review, none
of them in the request path.

The closed-PR branch cleanup could delete a branch someone was using. It
matched branches by NAME alone: any branch whose same-name historical pull
requests were all closed became a deletion candidate, with no check 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 was carrying 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 side's SHA is unknown.

That script had no test of any kind, which is how this shipped. It has eight now,
including one that walks every pre-existing safety rule so the tip check cannot be
reordered ahead of them.

glm-5.3-flash was still going through the vision sidecar on eight providers.
The earlier correction 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 received a text description of a picture it could have read itself: no error,
a worse answer, an extra call. 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 Moonshot $ref normalizer relaxed constraints 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 — weaker than either side asked
for. Under 2020-12 $ref is an in-place applicator: both apply, so the emitted
schema is their intersection. Lower bounds now take the max, upper bounds the min.

Verification

bun test tests/closed-pr-branch-cleanup.test.ts        8 pass 0 fail
bun test tests/provider-registry-parity.test.ts       37 pass 0 fail
bun test tests/moonshot-tool-schema.test.ts           16 pass 0 fail
bun test tests/digitalocean-scaleway-provider.test.ts  6 pass 0 fail
bun x tsc --noEmit                                     exit 0
bun run privacy:scan                                   passed
git diff --check                                       clean

Mutation checks:

Mutation Result
tip comparison disabled the reused-branch test fails on its own, the other seven stay green
intersection reverted the looser-bound test fails; it was red before the fix and green after

The Moonshot fix has a test in both directions — a looser sibling must not relax
the target, and a tighter one must still win — so it cannot be satisfied by
always preferring the target, which would discard a genuine narrowing.

The parity assertion for the sidecar classification is written across the whole
registry rather than per provider. The defect was not one entry being wrong, it
was a set of entries drifting apart, and only a global assertion catches the next
provider to seed that id. It also asserts that glm-5.3 stays text-only, so it
cannot pass by declaring every GLM row a VLM.

Two of these three findings are mine, from work merged earlier the same day.

No GUI files are touched, so no screenshot applies.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

The workflow change is the sensitive one. It passes two additional SHAs into an
existing script, adds no permission, and changes the job's behaviour only in the
direction of deleting less.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner August 26, 2026 17:17
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a8d89f03-8d32-4d55-88cd-c17b6b82fbf3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Deterministic hygiene checks failed.

  • new_suppression — A new TypeScript, lint, formatter, or similar suppression was added. Fix the underlying issue or obtain suppression-approved. Paths: tests/closed-pr-branch-cleanup.test.ts.

@github-actions github-actions Bot added the bug Something isn't working label Aug 26, 2026
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed.

Hygiene

Deterministic PR hygiene checks passed.

@github-actions
github-actions Bot marked this pull request as draft August 26, 2026 17:21
@github-actions
github-actions Bot marked this pull request as ready for review August 26, 2026 17:37
@github-actions github-actions Bot removed the intake: hygiene-blocked Deterministic PR hygiene checks failed label Aug 26, 2026
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 70 / 80

설명

이 풀 리퀘스트는 #2689 위에 쌓인 후속입니다. 베이스가 codex/pkgtree-503-false-positive 라서 current dev 에 직접 합칠 수 없고, #2689 가 먼저 들어와야 합니다. 초안이 아닙니다. types.tsconfig.ts 는 건드리지 않습니다.

닫힌 PR 브랜치 청소 스크립트가 이름만 보고 지우던 문제를 고칩니다. 지금 팁 SHA 가 닫힌 PR 의 head SHA 와 같을 때만 삭제하고, SHA 를 모르면 남깁니다. 워크플로가 head.sha 와 브랜치 tip 을 넘기도록 바뀌었고 테스트 여덟 개가 추가되었습니다.

또한 #2687 로 심은 glm-5.3-flash 가 여러 제공자에서 텍스트 전용/사이드카 비전으로 잘못 분류된 것을 고칩니다. ZAI_GLM_53_MODELS 에 flash 를 넣어 노력 사다리와 출력 한도를 맞추고, zai/zhipu/volcengine/nvidia/umans/cline-pass 등에서 네이티브 VLM 으로 선언합니다. Moonshot $ref 정규화는 숫자 bound 를 덮어쓰지 않고 교집합(하한 max, 상한 min) 하도록 고칩니다.

CI 는 test 샤드 실패가 보이고 mergeable_state 는 unstable 입니다. #2689 와 같이 Linux shard 이슈(#2695) 영향을 받을 수 있습니다. 미리보기 배포는 계획에 없습니다.

.github/scripts/closed-pr-branch-cleanup.cjs - 이름만 맞추던 삭제에 tip SHA 가드를 넣은 방향이 맞습니다
src/providers/registry.ts - glm-5.3-flash 를 ZAI_GLM_53_MODELS 와 비전 목록에 맞춘 수정은 #2687 리뷰 지적과 맞습니다
src/adapters/openai-chat.ts - Moonshot bound 교집합은 이전 overwrite 보다 맞습니다
tests/provider-registry-parity.test.ts - 제공자마다 어긋나던 분류를 전역으로 고정합니다
경로/심볼 - 베이스가 dev 가 아니므로 #2689 합치기 전에는 합칠 수 없습니다

메인테이너의 판단이 필요한 지점

너의 추천

#2689dev 에 들어간 뒤에만 합치세요. 내용 방향은 current dev+#2687 문제를 고치는 좋은 후속입니다. CI 가 초록이 될 때까지 기다리세요. #2690openai-chat.ts 를 함께 만지므로 충돌 시 이 PR 의 Moonshot 교집합과 #2690 의 xAI 추출을 둘 다 남기세요. types.ts/config.ts 분할로 닫을 대상이 아닙니다. 미리보기 배포는 계획에 없습니다.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun
lidge-jun force-pushed the codex/pkgtree-503-false-positive branch from de78afb to 8c83b6f Compare August 26, 2026 17:53
@lidge-jun
lidge-jun force-pushed the codex/branch-cleanup-sha-guard branch from cbeda14 to 0bbc4d1 Compare August 26, 2026 17:53
jun added 6 commits August 27, 2026 02:55
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.
@lidge-jun
lidge-jun force-pushed the codex/pkgtree-503-false-positive branch from 8c83b6f to 583f5a2 Compare August 26, 2026 17:56
@lidge-jun
lidge-jun force-pushed the codex/branch-cleanup-sha-guard branch from 0bbc4d1 to f25dab8 Compare August 26, 2026 17:56
@lidge-jun
lidge-jun changed the base branch from codex/pkgtree-503-false-positive to dev August 26, 2026 18:04
@lidge-jun
lidge-jun merged commit 9f7d709 into dev Aug 26, 2026
26 checks passed
@lidge-jun
lidge-jun deleted the codex/branch-cleanup-sha-guard branch August 26, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant