Skip to content

fix(codex): name the native-main gate reason when the fence returns 503 - #2121

Merged
lidge-jun merged 5 commits into
devfrom
fix/native-main-gate-reason
Aug 19, 2026
Merged

fix(codex): name the native-main gate reason when the fence returns 503#2121
lidge-jun merged 5 commits into
devfrom
fix/native-main-gate-reason

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

A Windows reboot can leave native-main admission fenced until the user runs ocx restart. #2108 reports it, but the report could not be acted on: every fenced request returns the same generic 503 maintenance message, so the gate reason that actually settled was never written anywhere a user could read.

That matters because the two candidate triggers settle to different reasons — an ACL fail-closed settles owner-unavailable (native-profile-startup.ts:138-139), a probe fail-closed settles ownership-unknown (:311) — and nothing on the request path distinguished them. This ships the diagnostic, not the mechanism fix; guessing between two candidates without evidence is how the next reboot report ends up exactly as ambiguous as this one.

CodexMainProfileDrainingError now captures the settled reason in its constructor and warns once per distinct reason. The constructor is the last moment the reason is both in scope and still true — every catch site has already lost it, and re-reading the gate later can observe a recovery that completed in between.

Three other surfaces were considered and rejected, which is why this is a log line rather than something more structured:

  • The message cannot carry it. claude-messages.ts:818-820 matches that string exactly to keep the fence a 503 instead of remapping it to an Anthropic 529. A changed message would tell Claude Code to back off from an upstream that was never involved.
  • A response header cannot reach /api/logs, which populates upstreamError by reading json.error.message back out of the body (request-log.ts:755-759); error.code is likewise pinned to server_is_overloaded for 503 by design (request-log.ts:508-540). The Claude surface also rebuilds its response headers from scratch (claude-messages.ts:823-829).
  • Threading a reason into codexMainProfileDrainingResponse would mean editing all five of its zero-arg call sites, two of which are also edited by open PR fix(codex): gate account-native models by entitlement #2101.

stdout reaches every path this fence fires on, and it is the stream the original report was already quoting from.

The turn-drain claim race (auth-context.ts:326, driven by lifecycle.ts:180) throws this same error while the gate reads ready. That site stays silent rather than borrowing a startup reason, so a reasonless 503 now means "not the startup fence" — a distinction the reporter had no way to make. Only the reason is logged; the snapshot's homeId is derived from a profile directory path and stays out.

Refs #2108. Deliberately not Closes: this makes the next occurrence diagnosable, it does not stop the fence from sticking. Phase 2 (making a boot-time unknown retryable instead of a process-lifetime fence) waits for a field report that names a reason.

Stacked on #2118 to keep the review surface small; retarget to dev once the parents land.

Verification

bun test tests/codex-auth-context.test.ts          47 pass / 0 fail / 173 expect()
bun test <5 native-main gate suites>               80 pass / 0 fail / 470 expect()
bun test tests/chat-completions-endpoint.test.ts \
         tests/claude-messages-endpoint.test.ts   124 pass / 0 fail
bun x tsc --noEmit                                 exit 0
bun run privacy:scan                               exit 0

The five gate suites are codex-auth-context, native-profile-startup, native-profile-drain-server, native-profile-stage-lifecycle, native-main-owner-lifetime. privacy:scan is not ceremony here — this PR adds a log statement, which is what that scan exists to police.

Red-driven, not merely green. With the fix staged as a no-op export, two assertions failed on reason === undefined and the turn-drain silence case passed — proving the assertions themselves were doing the work rather than a module-load error. An independent reviewer then mutation-tested the new tests in a scratch copy: removing the console.warn body goes red (2 fail), and removing the dedup check goes red. Each assertion detects the removal of the behavior it claims to pin.

One honest caveat: these suites ran on macOS. The owner-unavailable branch this diagnostic most needs to name is a Windows icacls path, so this proves the plumbing rather than the field behavior. Worth knowing that no test anywhere currently asserts owner-unavailable — it is produced at native-profile-startup.ts:139 and asserted nowhere in the suite.

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.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner August 19, 2026 14:05
@coderabbitai

coderabbitai Bot commented Aug 19, 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: 1a089c17-3a92-4f2c-882a-b556354ae3b1

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

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

jun and others added 4 commits August 19, 2026 23:26
A Windows reboot can leave native-main admission fenced until the user runs
ocx restart, and the report of it was unactionable: every 503 carries the same
generic maintenance message, so the settled gate reason was never written
anywhere a user could read it. Two candidate triggers settle to different
reasons - an ACL fail-closed settles owner-unavailable, a probe fail-closed
settles ownership-unknown - and nothing on the request path could tell them
apart.

Capture the reason in the CodexMainProfileDrainingError constructor and warn
once per distinct reason. The constructor is the last moment the reason is both
in scope and still true: every catch site has already lost it, and re-reading
the gate later can observe a recovery that completed in between.

Three surfaces were considered and rejected. The message cannot carry it -
claude-messages.ts matches that string exactly to keep the fence a 503 instead
of remapping it to an Anthropic 529, so a changed message would tell Claude Code
to back off from an upstream that was never involved. A response header cannot
reach /api/logs, which reads only error.message out of the body, and the Claude
surface rebuilds its headers from scratch. Threading a reason into
codexMainProfileDrainingResponse would have meant editing all five of its
zero-arg call sites. stdout reaches every path this fence fires on, and is the
stream the original report was already quoting from.

The turn-drain claim race throws this same error while the gate reads ready.
That site stays silent rather than borrowing a startup reason, which makes a
reasonless 503 mean "not the startup fence" - a distinction the report could not
make. Only the reason is logged; the snapshot's homeId is derived from a profile
directory path.

Refs #2108

Verification: red-driven with the fix staged as a no-op export (2 fail on
reason === undefined, 1 pass on the silence case), then 80 pass / 0 fail / 470
expect() across the five native-main gate suites, 124 pass / 0 fail on the
Claude and chat surfaces, tsc --noEmit exit 0, privacy:scan exit 0. An
independent reviewer mutation-tested the new assertions in a scratch tree and
confirmed each goes red when the behavior it pins is removed. Suites ran on
macOS, so the owner-unavailable icacls branch is proven as plumbing rather than
field behavior.
@lidge-jun
lidge-jun force-pushed the fix/cursor-abort-teardown branch from 2c4e00e to b571284 Compare August 19, 2026 14:26
@lidge-jun
lidge-jun force-pushed the fix/native-main-gate-reason branch from 05628d4 to 50c6ccd Compare August 19, 2026 14:26
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 70 / 80

#2108 재부팅 후 native-main이 막히면 전부 같은 503이라 원인이 안 보임. ACL fail-closed는 owner-unavailable, 프로브 fail-closed는 ownership-unknown. 이 PR은 그 이유를 CodexMainProfileDrainingError 생성자에 넣고, 사유마다 console.warn 한 번. 수리 아님. 고의로 Closes 안 함.

메시지에 이유를 넣으면 안 됨. claude-messages.ts가 그 문자열을 정확히 매칭해서 503을 유지함. 바꾸면 Claude가 529로 착각하고 업스트림 백오프함. 헤더도 /api/logsjson.error.message만 봄. codexMainProfileDrainingResponse에 스레드하면 호출 5곳 + #2101이랑 겹침. stdout이 리포트가 이미 보던 길이라 그거 맞음.

턴 드레인 레이스는 게이트가 ready인데도 같은 에러를 던짐. 여긴 침묵. 이유 없는 503은 스타트업 펜스가 아님. homeId는 로그에 안 넣음. privacy:scan 통과했다고 함.

베이스가 dev가 아니라 fix/cursor-abort-teardown(#2118)임. MERGEABLE. 부모 머지 전에 dev로 리타깃하지 말 것. 스플릿 메가파일은 안 건드림. src/codex/auth-context.ts, native-profile-startup.ts, tests/codex-auth-context.test.ts + devlog.

macOS에서만 돌려서 owner-unavailable(윈도 icacls) 필드는 증명 안 됨. 그 브랜치를 assert하는 테스트도 원래 없음. Phase 2(부팅 unknown을 프로세스 평생 펜스에서 빼기)는 사유가 찍힌 리포트 온 다음.

해결방안: #2118 먼저 머지하고 리타깃. 로그 포맷 고정해서 다음 #2108 리포트가 사유를 붙이게. 503 문구/헤더는 그대로.

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

@lidge-jun
lidge-jun changed the base branch from fix/cursor-abort-teardown to dev August 19, 2026 14:53
@lidge-jun
lidge-jun merged commit fbc6f26 into dev Aug 19, 2026
27 checks passed
@lidge-jun
lidge-jun deleted the fix/native-main-gate-reason branch August 19, 2026 15:38
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