Repository navigation
[OpenAI/Codex] K12 denial under detail.code is misclassified and cross-account thread switching returns 403 #2046
Description
Client or integration
Codex App
Provider or upstream service
OpenAI/Codex (K12)
OpenCodex version
2.24.1
Endpoint or capability
Authentication and account/thread routing
Current behaviour
The published denial classifier recognizes structured codes only at the top level or under error.code. The observed response shape is:
{"detail":{"code":"codex_workspace_access_denied","message":"..."}}A local regression test returned denial = null before the patch. Adding an own-property check for detail.code returned denial = "workspace", and the incorrect needs-reauth state disappeared.
A fresh task bound exactly to the K12 selector completed with HTTP 200 and was routed to the K12 pool account. Switching an existing Plus-bound task to the same exact K12 selector produced four reproducible HTTP 403 responses without fallback.
Expected behaviour
K12 should continue to work in clean tasks, and workspace denials nested under own-property detail.code must be classified as workspace denials rather than credential failures.
When an existing Plus-bound thread is switched to an exact K12 selector, OpenCodex must either start clean/remove incompatible thread metadata or return a clear thread/account incompatibility. It must never mark the account needs-reauth for this case.
Minimal redacted request or reproduction
1. Reauthenticate a valid K12 account and verify the upstream plan is K12.
2. Start a fresh task explicitly bound to that K12 account and select `k12test/gpt-5.6-sol`; observe HTTP 200.
3. Start a task under Plus, then switch that existing task to the same exact K12 selector.
4. Observe reproducible HTTP 403 responses with no fallback.
5. Unit-test denial classification with `{ detail: { code: "codex_workspace_access_denied" } }`; current result is `null` instead of `workspace`.Actual response or error
Fresh exact-K12 task: HTTP 200.
Existing Plus-bound task switched to exact K12: four reproducible HTTP 403 responses, no fallback. The nested denial envelope is:
{"detail":{"code":"codex_workspace_access_denied","message":"..."}}
OpenCodex incorrectly transitions to `needs-reauth` unless `detail.code` is handled.Upstream documentation
No public upstream specification appears to document this private WHAM denial envelope. The concrete client requirement is established by the observed response shape, successful clean-task routing, and the reproducible cross-account/thread case.
Suggested mapping or implementation notes
Extend structured denial extraction to accept own-property detail.code, alongside the existing top-level and error.code paths. Add a regression test: an existing Plus-bound thread switched to an exact K12 selector must either shed incompatible metadata/start clean or return an explicit thread/account incompatibility, never needs-reauth.
Additional context and attachments
Follow-up to #1789. Reauthentication succeeded, credentials were valid, and the authenticated upstream check returned HTTP 200 with the K12 plan. This confirms that K12 works through OpenCodex in a clean task; the remaining failure is specific to cross-account/thread metadata.
No additional private data should be necessary. If maintainers need a diagnostic build tested, I can offer one anonymized K12 run.
Checks
- I searched existing provider and compatibility issues.
- The request and response were redacted.
- The expected behaviour is based on an upstream specification or a concrete client requirement.
Activity
Ingwannu commentedon Aug 18, 2026
Confirmed on current
dev.structuredDenialCodecurrently accepts an own stringcodeonly at the document root or undererror; the reported own-propertydetail.codeshape therefore falls through and a 403 is misclassified as a credential failure.I will fix the classifier narrowly to accept only an ordinary object-valued
detailwith an own stringcode, add positive and fail-closed regressions, and verify that the routing outcome preserves the credential/account binding instead of marking it for reauthentication.The existing-thread 403 is a separate compatibility boundary: the classifier fix can stop the false reauthentication state, but it cannot invent upstream workspace access. I will keep that part open until the exact thread metadata carried across the Plus-to-K12 switch is identified rather than guessing at a destructive thread reset.
Ingwannu commentedon Aug 18, 2026
Implemented the verified classifier fix in #2055.
The PR accepts only exact own string codes under object-valued
detail, preserves the existing fail-closed behavior for malformed or ambiguous bodies, and adds positive workspace/entitlement plus negative non-string coverage. Focused tests (166), typecheck, privacy scan, and the exact-range security diff review passed.I am leaving this issue open for the second reported behavior: identifying which upstream thread/workspace metadata makes an existing Plus-bound thread incompatible after an exact K12 switch. #2055 intentionally does not reset or discard thread state without that evidence.
yzxcj797 commentedon Aug 18, 2026
Fixed the classification half —
structuredDenialCodenow performs the own-propertycodelookup at the top level, undererror, and underdetail(the envelope K12 actually sends:{"detail":{"code":"codex_workspace_access_denied",...}}). Regression tests pin the detail-nested workspace and entitlement denials, plus a detail object carrying a non-denial code still readingdenial = null(fail-closed preserved).The cross-account thread-switching half (Plus-bound task switched to an exact K12 selector → 403 without fallback) is not included: that's a thread-metadata/account-affinity design decision — whether to start clean on incompatible switch or return a clear incompatibility error — which the maintainers should weigh in on before I guess.
lidge-jun commentedon Aug 19, 2026
Classification half is now on
dev: #2055 merged as 2648ffa.detail.codeworkspace/entitlement denials are classified fail-closed and no longer flip accounts to needs-reauth.Remaining half of this issue — the cross-account thread-switch 403 (Plus-bound thread switched to an exact K12 selector gets four 403s with no fallback or clear incompatibility signal) — is not addressed by #2055 and stays open here.
jojodat commentedon Aug 19, 2026
Additional reproduction on OpenCodex 2.26.0:
Clarification:
k12testis only the arbitrary local OpenCodex account namespace for the authenticated K12 account;gpt-5.6-solis the actual model ID. It is not a custom provider or model. An account-qualified selector such ask12test/gpt-5.6-solpins routing to that account, whereas a plain model ID uses Pool mode and may fail over to Plus.k12test/gpt-5.6-soland continues to complete with HTTP 200.permission_deniedresponses, after which the same launch fell back to Plus and completed with HTTP 200.This narrows the remaining behavior: a newly created Codex task is not necessarily “clean” at the upstream account/thread-affinity level. The evidence suggests that the new task already carries metadata compatible with Plus before OpenCodex selects K12 from the pool. K12 rejects that metadata, while the Plus fallback succeeds. A plain model masks the incompatibility through pool failover; an exact K12 selector cannot.
Please add a regression covering a newly created Codex task whose first request is pool-selected to K12. The task/thread metadata should be compatible with the selected account; otherwise OpenCodex should skip K12 for that task or return a clear account/thread incompatibility.
No further private data should be necessary. I can run one anonymized diagnostic build if needed.
lidge-jun commentedon Aug 19, 2026
리뷰 · 우선순위 46 / 80
K12 403이
{detail:{code:"codex_workspace_access_denied"}}모양이면 분류기가 못 읽고needs-reauth로 떨어졌다. 새 태스크를 K12 셀렉터에 묶으면 200이고, Plus로 시작한 태스크를 같은 K12 셀렉터로 바꾸면 403이 네 번 나고 폴백이 없다. 자격 있는 계정 재로그인을 강요하는 쪽이라 46이다. 기본 설치 직후 사고는 아니고, 계정 전환/워크스페이스 거절 호환이다.분류기 일부는 현재
dev에서 이미 넓혀져 있다.src/codex/quota-rejection.ts의structuredDenialCode()는 최상위code다음에 own-propertyerror와detail을 본다.{detail:{code:"codex_workspace_access_denied"}}는 이제workspace가 되어야 한다.WORKSPACE_DENIAL_CODES에 그 문자열이 있다.src/codex/routing.ts의classifyCodexUpstreamOutcome()는 403+denial이면credential이 아니라workspace다. 2.24.1에서denial = null이던 단위 테스트는dev에서 다시 돌려야 한다.스레드 전환 403은 분류기와 다른 층이다. Plus 태스크에 묶인 스레드 메타데이터(워크스페이스/스레드 id)를 K12 계정이 받아 거절하는 모양이다. 코드에서 “Plus 스레드를 K12로 바꿀 때 메타를 지운다”는 함수는 찾지 못했다. 패치 위치를 새로 짓지 않는다. 새 태스크가 200인 것과 맞으면, 기존 스레드를 재사용하지 말고 명확한 불일치 에러를 주는 쪽이 안전하다.
needs-reauth로 올리면 유효한 K12 자격 증명을 폐기한다. #1789 주석이 그 루프를 경고한다. 분류기가workspace를 주면 재로그인 루프는 닫혀야 한다. 그래도 403이 폴백 없이 네 번이면, 분류 다음의 재시도/스레드 바인딩이 워크스페이스 거절을 소모성 재시도로 취급하는지를 봐야 한다.해결방안
분류기 회귀 테스트로
{detail:{code:"codex_workspace_access_denied"}}→denial: "workspace"를dev에서 확인한다. 이미structuredDenialCode()가 그 경로를 보면 분류기 패치는 중복이다. 남은 건 Plus-bound 스레드를 정확 K12 셀렉터로 바꿀 때다. 호환되지 않는 스레드 메타를 지우거나, 계정 불일치를 명시하고needs-reauth를 쓰지 않는다. 그 변환 함수 이름은 추측하지 않는다. 새 태스크 정확 바인딩 200은 유지한다. 계정 id·토큰·스레드 id는 로그에 남기지 말고 상태 코드와detail.code만 남긴다.이 댓글은 grok-bot이 작성했습니다
lidge-jun commentedon Aug 19, 2026
리뷰 · 우선순위 66 / 80 · 유지
점수 정정임. 46 → 66. K12가
{"detail":{"code":"codex_workspace_access_denied"}}로 거절하는데, 분류기가 top-level/error.code만 봐서denial = null→needs-reauth로 감. 새 작업은 200. Plus 스레드를 K12로 바꾸면 403 네 번, 폴백 없음. 계정은 멀쩡함 (#1789 후속).유지함. 자격 만료가 아닌데 재로그인으로 몰면 풀이 죽음. #2047(숏윈도우 쿼타 드롭)이랑 이웃. draft #2063이
detail.code쪽.해결방안: own-property
detail.code도 workspace denial로. Plus→K12 전환은 메타 버리거나 명시 비호환.needs-reauth로 가지 말 것.이 댓글은 grok-bot이 작성했습니다
jojodat commentedon Aug 20, 2026
Additional evidence from OpenCodex 2.28.0:
k12test/gpt-5.6-solcompleted with HTTP 200 on the first attempt. The routing record showsexplicit-account, the K12 account namespace, and no fallback.needs-reauth.This separates the cases more clearly: fresh exact-K12 routing works; the failure appears after Plus has successfully handled the task.
I also tested several possible causes independently. A standalone
session_id,prompt_cache_key,x-codex-parent-thread-id, and replay of the stored Plus continuation items were each accepted by K12. None is sufficient by itself to reproduce the denial.The strongest remaining code-level candidate is the combined forwarded affinity metadata:
FORWARD_HEADERSincludessession_id,thread-id,x-codex-parent-thread-id,x-codex-turn-metadata,x-codex-turn-state, window/installation headers, and other caller metadata.materializeCodexUpstreamAuthcopies those headers first and, for a selected pool account, replaces onlyauthorizationandchatgpt-account-id.session_id,thread_id,parent_thread_id, andforked_from_thread_id.Therefore a Plus-to-K12 switch changes the credentials/account header while potentially retaining the original task's complete thread/turn affinity envelope. Header presence alone is not the problem—working and failing requests both carry turn metadata—but the retained account-bound values or their combination are a concrete target for instrumentation.
Suggested regression:
A diagnostic build could safely log only the presence and stable hashes of individual affinity fields before and after account substitution. No raw IDs, metadata values, credentials, or other private data are needed. I can run one anonymized K12 diagnostic test if useful.
Ingwannu commentedon Aug 20, 2026
Thank you — the 2.28.0 result rules out the four obvious single-field explanations. I also verified the current implementation:
materializeCodexUpstreamAuth()copies the complete curatedFORWARD_HEADERSset first, then replaces onlyauthorizationandchatgpt-account-idfor the selected account. The remaining boundary is therefore the combined affinity envelope or provenance, not the mere presence of one of the four fields you tested.I do not want to “fix” this by deleting all turn metadata. That would be destructive, could break valid continuation/subagent behavior, and the current evidence does not justify it.
I am taking you up on the anonymized diagnostic run. The diagnostic must be opt-in and observational only:
The useful capture is one Plus-success turn followed by the failing exact-K12 turn in the same OCX process. That will tell us which affinity values remain bound across the credential substitution without exposing their contents. I am keeping this open and will link the diagnostic change here; the eventual production fix will be limited to the field combination the capture actually proves.
Ingwannu commentedon Aug 20, 2026
The observation-only diagnostic is now in #2196 and is ready for maintainer/security review.
It is deliberately narrower than a fix: provider debug emits one
[ocx:codex:affinity]record after each canonical ChatGPT forward response. The record contains only fixed header names, coarse size buckets, and equality tags under a fresh random per-process HMAC key. It excludes credentials, account IDs, attestation, raw session/thread IDs, raw turn metadata, request bodies, and continuation items. Oversized values are categorized without hashing, and the code cannot strip metadata, retry, switch accounts, fall back, or reset a thread.After #2196 is reviewed and available in a diagnostic build, the useful reproduction is:
In the same OCX process, capture one successful Plus turn and then the failing exact-K12 turn from the same task. Please send only the two
[ocx:codex:affinity]JSON records plus the HTTP status/denial class; do not include surrounding provider logs. Then disable capture withocx debug provider off.Focused tests, typecheck, privacy scan, and the 393-page docs build pass on the PR head. Because this touches the forwarded header/credential boundary, I have requested explicit review from @lidge-jun and @Wibias and will not self-merge it.
Ingwannu commentedon Aug 21, 2026
The privacy-bounded diagnostic from #2196 is now merged into
devas98848575df9b9343a901dd4d1c5b23026b752931.@jojodat, the next useful step is the same-process capture you offered:
In one OCX process, please record:
[ocx:codex:affinity]JSON records and the corresponding HTTP status/denial class.Please omit surrounding provider logs and any raw IDs, headers, metadata, request bodies, or credentials, then disable capture with
ocx debug provider off.If you are testing a packaged release rather than current
dev, first confirm that the build contains commit98848575d; older 2.28.0 builds will not have this diagnostic. I will keep the issue open until that evidence identifies the exact combined affinity field boundary. We will not strip the full turn envelope or reset threads speculatively.jojodat commentedon Aug 21, 2026
Diagnostic follow-up from the packaged OpenCodex 2.29.0 release (including #2196 /
98848575d) with Codex CLI 0.147.0.Result summary
The behavior is client-path dependent:
main/gpt-5.6-solturn succeeded and the same task then succeeded as exactk12lab/gpt-5.6-sol(HTTP 200 -> 200). In the stronger control, the task completed two Plus turns before the K12 switch. Both account-qualified records hadfixedAccount=true, and there was no account retry, fallback, denial, orneeds-reauth.detail.code=codex_workspace_access_denied.main/...andk12lab/...entries. Therefore the App reproduction is a manual account selection in Pool mode, not an exact selector: the diagnostic correctly reportsfixedAccount=false. I did not modify the normal catalog merely to manufacture an exact App selector.This test environment has two independently authenticated K12 accounts in the OpenCodex pool.
k12labis only an arbitrary local namespace for the second K12 account; no account identity is included below.Requested same-process App capture
Plus success (
status=200, denial none):{"sequence":32,"authKind":"main-pool","accountMode":"pool","fixedAccount":false,"credentialSubstituted":true,"accountGatedModel":false,"wireModelNormalized":false,"status":200,"inbound":[{"name":"originator","size":"1-31","tag":"d2bcfedffcd5"},{"name":"session-id","size":"32-127","tag":"8f3c3dd3ff9e"},{"name":"thread-id","size":"32-127","tag":"4b2a10bbe6db"},{"name":"x-client-request-id","size":"32-127","tag":"67fdcf93fdcb"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"86358030db12"},{"name":"x-codex-window-id","size":"32-127","tag":"9131b55c439d"}],"outbound":[{"name":"originator","size":"1-31","tag":"d2bcfedffcd5"},{"name":"session-id","size":"32-127","tag":"8f3c3dd3ff9e"},{"name":"thread-id","size":"32-127","tag":"4b2a10bbe6db"},{"name":"x-client-request-id","size":"32-127","tag":"67fdcf93fdcb"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"86358030db12"},{"name":"x-codex-window-id","size":"32-127","tag":"9131b55c439d"}],"inboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"afafabaa8fea","kind":"string"},{"name":"thread_id","size":"32-127","tag":"8699e7adcd30","kind":"string"}],"unknownFieldCount":13},"outboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"afafabaa8fea","kind":"string"},{"name":"thread_id","size":"32-127","tag":"8699e7adcd30","kind":"string"}],"unknownFieldCount":13},"inboundTurnState":{"shape":"absent"},"outboundTurnState":{"shape":"absent"}}First K12 denial for the same App task (
status=403, denial classworkspacefromdetail.code):{"sequence":36,"authKind":"pool","accountMode":"pool","fixedAccount":false,"credentialSubstituted":true,"accountGatedModel":false,"wireModelNormalized":false,"status":403,"inbound":[{"name":"originator","size":"1-31","tag":"d2bcfedffcd5"},{"name":"session-id","size":"32-127","tag":"8f3c3dd3ff9e"},{"name":"thread-id","size":"32-127","tag":"4b2a10bbe6db"},{"name":"x-client-request-id","size":"32-127","tag":"67fdcf93fdcb"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"a259281b6e59"},{"name":"x-codex-window-id","size":"32-127","tag":"9131b55c439d"}],"outbound":[{"name":"originator","size":"1-31","tag":"d2bcfedffcd5"},{"name":"session-id","size":"32-127","tag":"8f3c3dd3ff9e"},{"name":"thread-id","size":"32-127","tag":"4b2a10bbe6db"},{"name":"x-client-request-id","size":"32-127","tag":"67fdcf93fdcb"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"a259281b6e59"},{"name":"x-codex-window-id","size":"32-127","tag":"9131b55c439d"}],"inboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"afafabaa8fea","kind":"string"},{"name":"thread_id","size":"32-127","tag":"8699e7adcd30","kind":"string"}],"unknownFieldCount":13},"outboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"afafabaa8fea","kind":"string"},{"name":"thread_id","size":"32-127","tag":"8699e7adcd30","kind":"string"}],"unknownFieldCount":13},"inboundTurnState":{"shape":"absent"},"outboundTurnState":{"shape":"absent"}}What the capture establishes
authKindchanges frommain-pooltopool.session-id,thread-id,x-client-request-id, andx-codex-window-idare identical across the credential/account change.session_idandthread_idfields inside turn metadata are also identical.x-codex-turn-metadatachanges between turns, as expected, but inbound and outbound tags remain identical within each request. Nox-codex-turn-statewas present for this specific App pair.needs-reauth.This narrows the remaining defect substantially: OpenCodex 2.29.0 can switch the same persisted task from exact Plus to exact K12 through the CLI, but the Codex App's manual-account/Pool path retains the App task affinity values and receives repeatable workspace denials. It also retries the denied user turn and its associated helper request across the K12 pool rather than failing once with a clear task/account incompatibility.
If it would help distinguish App metadata from Pool routing, I can temporarily expose exact account-qualified entries in the App catalog and run one additional anonymized Plus-to-K12 App test.
No raw account, thread/session, request, network, credential, metadata, or reset identifiers are included, and no further private data should be necessary.
Ingwannu commentedon Aug 21, 2026
Thank you — this capture proves one additional local routing gap, but it still does not justify stripping the App affinity envelope.
I rechecked current
dev@236e03342:resolveCodexAuthContext()derives Pool thread affinity only fromx-codex-parent-thread-id.session-id,thread-id, and turn-metadata ids are forwarded upstream and used for log correlation, but they do not key the Codex account-affinity map.PUT /api/codex-auth/activeclears the existing affinity map and seeds the selected account. Subsequent App reconnects withoutx-codex-parent-thread-idare therefore unbound Pool selections, so a rotating strategy can visit both K12 credentials. This explains the observed cross-account retries; it does not explain the first workspace denial.caller, so it correctly does not mark either credentialneeds-reauth. There is also no server-side 403 alternate-account retry in this path; the six Sol attempts are client reconnects arriving as new unbound requests.I will keep the two boundaries separate:
session-id/thread-idbut no parent-thread header, so one App task does not rotate accounts across reconnects.Please run the additional exact account-qualified App test you offered, without changing any other setting: one successful Plus turn, then the same App task switched to an exact K12 catalog entry, in the same OCX process. Please provide only the two
[ocx:codex:affinity]records and status/denial class.I will not delete all turn metadata, auto-reset the task, or treat workspace denial as a credential failure.
jojodat commentedon Aug 21, 2026
I ran the requested additional exact account-qualified Codex App test on packaged OpenCodex 2.29.0, in one OCX process and one fresh App task. The account-qualified picker was enabled before creating the task; no routing or account setting was changed between the two turns.
Exact Plus success (
main/gpt-5.6-sol, HTTP 200, denial none):{"sequence":63,"authKind":"main-pool","accountMode":"pool","fixedAccount":true,"credentialSubstituted":true,"accountGatedModel":false,"wireModelNormalized":false,"status":200,"inbound":[{"name":"originator","size":"1-31","tag":"d2bcfedffcd5"},{"name":"session-id","size":"32-127","tag":"f915cc669b49"},{"name":"thread-id","size":"32-127","tag":"09f45b6628bf"},{"name":"x-client-request-id","size":"32-127","tag":"f36ff1c9f792"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"596b496faf8d"},{"name":"x-codex-window-id","size":"32-127","tag":"8a376ad98b2c"}],"outbound":[{"name":"originator","size":"1-31","tag":"d2bcfedffcd5"},{"name":"session-id","size":"32-127","tag":"f915cc669b49"},{"name":"thread-id","size":"32-127","tag":"09f45b6628bf"},{"name":"x-client-request-id","size":"32-127","tag":"f36ff1c9f792"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"596b496faf8d"},{"name":"x-codex-window-id","size":"32-127","tag":"8a376ad98b2c"}],"inboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"1f222d9daf06","kind":"string"},{"name":"thread_id","size":"32-127","tag":"8fc64b5b04e2","kind":"string"}],"unknownFieldCount":13},"outboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"1f222d9daf06","kind":"string"},{"name":"thread_id","size":"32-127","tag":"8fc64b5b04e2","kind":"string"}],"unknownFieldCount":13},"inboundTurnState":{"shape":"absent"},"outboundTurnState":{"shape":"absent"}}First exact K12 denial for the same App task (HTTP 403, denial class
workspacefromdetail.code=codex_workspace_access_denied):{"sequence":77,"authKind":"pool","accountMode":"pool","fixedAccount":true,"credentialSubstituted":true,"accountGatedModel":false,"wireModelNormalized":false,"status":403,"inbound":[{"name":"originator","size":"1-31","tag":"d2bcfedffcd5"},{"name":"session-id","size":"32-127","tag":"f915cc669b49"},{"name":"thread-id","size":"32-127","tag":"09f45b6628bf"},{"name":"x-client-request-id","size":"32-127","tag":"f36ff1c9f792"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"089548332fe5"},{"name":"x-codex-window-id","size":"32-127","tag":"8a376ad98b2c"}],"outbound":[{"name":"originator","size":"1-31","tag":"d2bcfedffcd5"},{"name":"session-id","size":"32-127","tag":"f915cc669b49"},{"name":"thread-id","size":"32-127","tag":"09f45b6628bf"},{"name":"x-client-request-id","size":"32-127","tag":"f36ff1c9f792"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"089548332fe5"},{"name":"x-codex-window-id","size":"32-127","tag":"8a376ad98b2c"}],"inboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"1f222d9daf06","kind":"string"},{"name":"thread_id","size":"32-127","tag":"8fc64b5b04e2","kind":"string"}],"unknownFieldCount":13},"outboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"1f222d9daf06","kind":"string"},{"name":"thread_id","size":"32-127","tag":"8fc64b5b04e2","kind":"string"}],"unknownFieldCount":13},"inboundTurnState":{"shape":"absent"},"outboundTurnState":{"shape":"absent"}}Result: exact App routing still returns 403. Both decisive records have
fixedAccount=true. The equality tags for outersession-id,thread-id,x-client-request-id, andx-codex-window-id, plus the known innersession_idandthread_id, are identical across the account switch. Onlyx-codex-turn-metadatachanges between turns, as expected; nox-codex-turn-statewas present.This rules out the manual Pool-selection/
fixedAccount=falsepath as a necessary condition for the denial and leaves the App task affinity envelope as the distinguishing boundary from the successful exact CLI control.The App also emitted interleaved generic Luna helper/reconnect requests; their records are intentionally omitted here because you requested only the two decisive affinity records. No raw identifiers or surrounding provider logs are included.
Ingwannu commentedon Aug 21, 2026
Thank you. This exact account-qualified App control removes the manual Pool selection /
fixedAccount=falsepath as a necessary condition. The first workspace denial still occurs withfixedAccount=true, while the same App task keeps the stable outer session/thread/request/window tags and stable inner session/thread tags across the Plus-to-K12 credential change.That proves the remaining first-403 boundary is in the Codex App task envelope, but it does not yet identify which field is incompatible. Equality tags prove stability, not causation, and
x-codex-turn-metadatalegitimately changes between turns. I will not strip the whole envelope or reset the task speculatively.The minimum next comparison is the successful exact CLI control under the same diagnostic build: in one OCX process and one persisted CLI task, capture the successful exact Plus turn followed by the successful exact K12 turn, and provide only those two
[ocx:codex:affinity]records plus status/denial class. Comparing App versus CLI field presence and equality should identify the smallest candidate field set before any mutation probe.Separately, current
dev@236e03342has a confirmed local Pool-affinity gap:resolveCodexAuthContext()keys onlyx-codex-parent-thread-id, which these App requests omit. Therefore reconnects are unbound selections and can rotate across the two K12 accounts. I will handle a privacy-bounded App session/thread affinity fallback as a separate patch; it will stop cross-account reconnect rotation, but I will not claim that it fixes the first upstream 403.jojodat commentedon Aug 21, 2026
Here is the requested successful exact CLI control from the same packaged OpenCodex 2.29.0 diagnostic build and the same OCX process as the later exact App capture. This was one persisted CLI task: an exact Plus turn followed by an exact K12 turn.
Exact Plus CLI success (HTTP 200, denial none):
{"sequence":24,"authKind":"main-pool","accountMode":"pool","fixedAccount":true,"credentialSubstituted":true,"accountGatedModel":false,"wireModelNormalized":false,"status":200,"inbound":[{"name":"originator","size":"1-31","tag":"1d4b8d3e5f14"},{"name":"session-id","size":"32-127","tag":"54ea7b534f56"},{"name":"thread-id","size":"32-127","tag":"13193ce0e2cd"},{"name":"x-client-request-id","size":"32-127","tag":"0d842ebd6063"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"128-511","tag":"de2f4eb3641d"},{"name":"x-codex-window-id","size":"32-127","tag":"4f78792b7096"}],"outbound":[{"name":"originator","size":"1-31","tag":"1d4b8d3e5f14"},{"name":"session-id","size":"32-127","tag":"54ea7b534f56"},{"name":"thread-id","size":"32-127","tag":"13193ce0e2cd"},{"name":"x-client-request-id","size":"32-127","tag":"0d842ebd6063"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"128-511","tag":"de2f4eb3641d"},{"name":"x-codex-window-id","size":"32-127","tag":"4f78792b7096"}],"inboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"0fe317954c5b","kind":"string"},{"name":"thread_id","size":"32-127","tag":"b5df5d106ec4","kind":"string"}],"unknownFieldCount":6},"outboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"0fe317954c5b","kind":"string"},{"name":"thread_id","size":"32-127","tag":"b5df5d106ec4","kind":"string"}],"unknownFieldCount":6},"inboundTurnState":{"shape":"absent"},"outboundTurnState":{"shape":"absent"}}Exact K12 CLI success for the same persisted task (HTTP 200, denial none):
{"sequence":26,"authKind":"pool","accountMode":"pool","fixedAccount":true,"credentialSubstituted":true,"accountGatedModel":false,"wireModelNormalized":false,"status":200,"inbound":[{"name":"originator","size":"1-31","tag":"1d4b8d3e5f14"},{"name":"session-id","size":"32-127","tag":"54ea7b534f56"},{"name":"thread-id","size":"32-127","tag":"13193ce0e2cd"},{"name":"x-client-request-id","size":"32-127","tag":"0d842ebd6063"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"128-511","tag":"52fcf69119cb"},{"name":"x-codex-window-id","size":"32-127","tag":"4f78792b7096"}],"outbound":[{"name":"originator","size":"1-31","tag":"1d4b8d3e5f14"},{"name":"session-id","size":"32-127","tag":"54ea7b534f56"},{"name":"thread-id","size":"32-127","tag":"13193ce0e2cd"},{"name":"x-client-request-id","size":"32-127","tag":"0d842ebd6063"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"128-511","tag":"52fcf69119cb"},{"name":"x-codex-window-id","size":"32-127","tag":"4f78792b7096"}],"inboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"0fe317954c5b","kind":"string"},{"name":"thread_id","size":"32-127","tag":"b5df5d106ec4","kind":"string"}],"unknownFieldCount":6},"outboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"0fe317954c5b","kind":"string"},{"name":"thread_id","size":"32-127","tag":"b5df5d106ec4","kind":"string"}],"unknownFieldCount":6},"inboundTurnState":{"shape":"absent"},"outboundTurnState":{"shape":"absent"}}Both exact CLI records have
fixedAccount=true, status 200, and no denial. The outer session/thread/client/window equality tags and the known inner session/thread tags remain stable across the CLI account switch; onlyx-codex-turn-metadatachanges between turns, as expected.The visible structural difference from the exact App 200→403 pair is narrow: the header-name set is the same and
x-codex-turn-stateis absent in both clients, but CLI turn metadata is in size bucket128-511withunknownFieldCount=6, whereas the App pair is512-2047withunknownFieldCount=13. This narrows the candidate boundary to the additional App turn-metadata shape/content, without attributing causation to any individual field.No raw identifiers or surrounding provider logs are included.
Ingwannu commentedon Aug 21, 2026
Thank you — this is the control we needed, and it narrows the boundary substantially.
The same-process CLI Plus→K12 switch succeeds with
fixedAccount=trueon both turns, while the exact App pair fails only on the K12 turn. That rules out K12 entitlement, credential substitution, exact account selection, and the stable outer session/thread/window identifiers as sufficient causes. The larger Appx-codex-turn-metadatablob is now the leading boundary, but the current comparison still has one important confound: the successful CLI is Codex CLI0.147.0, while the App uses bundled0.146.0-alpha.9.2.Current upstream Codex constructs
x-codex-turn-metadataas one canonical payload containing core-owned fields plus app-serverresponsesapiClientMetadata; the latter is flattened into the same JSON after reserved-key filtering. ThereforeunknownFieldCount=13versus6can represent App-supplied metadata, a client-version schema difference, or both. It does not yet justify stripping the entire header or any guessed field.The next decisive control is the same binary, not another ordinary CLI run:
OpenAI.CodexBeta 26.727.4816.0in CLI mode, outside the App UI, through the same packaged OCX process.main/gpt-5.6-sol, then exactk12lab/gpt-5.6-sol.If bundled-CLI succeeds while the App fails, we can isolate the seven additional App-supplied metadata fields. If bundled-CLI also fails, the boundary is the bundled
0.146.0-alpha.9.2runtime/schema rather than the Desktop UI itself.If invoking the bundled binary directly is blocked by WindowsApps ACLs, please post the exact package executable path discovery result and do not change permissions. I will prepare the next privacy-bounded diagnostic to emit HMAC-tagged unknown field names/kinds rather than raw values; that lets us compare the additional field set without exposing identifiers, paths, workspace data, or metadata contents.
jojodat commentedon Aug 21, 2026
Bundled-App CLI control completed on the currently installed Desktop build.
One important version caveat: the Desktop App has updated again since the earlier failing App pair. The currently bundled runtime reports codex-cli 0.148.0-alpha.21, not the previously observed 0.146.0-alpha.9.2. Therefore this is a control using the current bundled binary, not a byte-for-byte control of the earlier failing App runtime.
I invoked that exact bundled binary outside the UI through the same running packaged OpenCodex 2.29.0 process. In one fresh persisted task:
The CLI explicitly reported that it was resuming the same session with a different model. These are the only two corresponding affinity records:
{"sequence":97,"authKind":"main-pool","accountMode":"pool","fixedAccount":true,"credentialSubstituted":true,"accountGatedModel":false,"wireModelNormalized":false,"status":200,"inbound":[{"name":"originator","size":"1-31","tag":"1d4b8d3e5f14"},{"name":"session-id","size":"32-127","tag":"e669fdf53291"},{"name":"thread-id","size":"32-127","tag":"deb4f7cbe32a"},{"name":"x-client-request-id","size":"32-127","tag":"4b47d90c1dd5"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"a39ee8fa69e8"},{"name":"x-codex-window-id","size":"32-127","tag":"af365b7ced9e"}],"outbound":[{"name":"originator","size":"1-31","tag":"1d4b8d3e5f14"},{"name":"session-id","size":"32-127","tag":"e669fdf53291"},{"name":"thread-id","size":"32-127","tag":"deb4f7cbe32a"},{"name":"x-client-request-id","size":"32-127","tag":"4b47d90c1dd5"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"a39ee8fa69e8"},{"name":"x-codex-window-id","size":"32-127","tag":"af365b7ced9e"}],"inboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"f9c8b8ddf704","kind":"string"},{"name":"thread_id","size":"32-127","tag":"ee0b275461d4","kind":"string"}],"unknownFieldCount":12},"outboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"f9c8b8ddf704","kind":"string"},{"name":"thread_id","size":"32-127","tag":"ee0b275461d4","kind":"string"}],"unknownFieldCount":12},"inboundTurnState":{"shape":"absent"},"outboundTurnState":{"shape":"absent"}} {"sequence":100,"authKind":"pool","accountMode":"pool","fixedAccount":true,"credentialSubstituted":true,"accountGatedModel":false,"wireModelNormalized":false,"status":200,"inbound":[{"name":"originator","size":"1-31","tag":"1d4b8d3e5f14"},{"name":"session-id","size":"32-127","tag":"e669fdf53291"},{"name":"thread-id","size":"32-127","tag":"deb4f7cbe32a"},{"name":"x-client-request-id","size":"32-127","tag":"4b47d90c1dd5"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"35425e8d54b8"},{"name":"x-codex-window-id","size":"32-127","tag":"af365b7ced9e"}],"outbound":[{"name":"originator","size":"1-31","tag":"1d4b8d3e5f14"},{"name":"session-id","size":"32-127","tag":"e669fdf53291"},{"name":"thread-id","size":"32-127","tag":"deb4f7cbe32a"},{"name":"x-client-request-id","size":"32-127","tag":"4b47d90c1dd5"},{"name":"x-codex-beta-features","size":"1-31","tag":"73834bcc80b8"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"35425e8d54b8"},{"name":"x-codex-window-id","size":"32-127","tag":"af365b7ced9e"}],"inboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"f9c8b8ddf704","kind":"string"},{"name":"thread_id","size":"32-127","tag":"ee0b275461d4","kind":"string"}],"unknownFieldCount":12},"outboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"9ec5bf39a3d7","kind":"string"},{"name":"session_id","size":"32-127","tag":"f9c8b8ddf704","kind":"string"},{"name":"thread_id","size":"32-127","tag":"ee0b275461d4","kind":"string"}],"unknownFieldCount":12},"inboundTurnState":{"shape":"absent"},"outboundTurnState":{"shape":"absent"}}Within this successful bundled-CLI pair, the outer session/thread/client/window tags remain stable, the inner session/thread tags remain stable, and only the turn-metadata tag changes. Notably, both requests already have x-codex-turn-metadata size 512–2047 and unknownFieldCount 12. The failing Desktop-App K12 request had the same size bucket and count 13.
So this control argues against metadata size alone being the boundary. It narrows the difference toward the identity/type/value of the App-only field(s), but a matching Desktop-UI retest on the current 0.148.0-alpha.21 build is still needed before treating this as a strict UI-vs-runtime isolation. The proposed privacy-bounded diagnostic with HMAC-tagged unknown field names/kinds should discriminate that cleanly.
No additional private data should be necessary.
Ingwannu commentedon Aug 21, 2026
The new bundled-CLI control changes the next step.
0.148.0-alpha.21succeeds on the exact Plus→K12 switch even with turn metadata in the same512-2047size bucket and 12 unknown fields. Therefore metadata size is not the cause, and the earlier Desktop failure's 13th field cannot be treated as causal without a same-version Desktop retest. Please rerun the exact account-qualified Plus→K12 transition in the current Desktop UI on this updated bundled runtime and report only the two bounded affinity records plus status/denial class. If it now succeeds, the first-403 symptom was upstream/client-version-specific; if it still fails, I will extend the opt-in diagnostic to emit process-local HMAC tags for unknown field names and coarse value kinds, never raw names or values.Separately, the capture already proves one OpenCodex defect that does not require another upstream guess: App requests without
x-codex-parent-thread-idare unbound in Pool mode, so reconnects can rotate across K12 accounts. I will fix that independently and will not claim it fixes the first 403:x-codex-parent-thread-idas the primary affinity key;session-idandthread-idheaders;I will open that patch against
devfor independent review. The first workspace denial remains open pending the current Desktop UI control.jojodat commentedon Aug 21, 2026
Current Desktop-UI control completed. The failure still reproduces on the updated Desktop build whose bundled runtime reports codex-cli 0.148.0-alpha.21.
In one fresh Desktop task:
APP_MAIN_OKThe denial class is workspace, from the already documented own-property
detail.code = codex_workspace_access_denied. No raw response identifiers are included.As requested, here are only the successful Plus record and one representative exact-K12 denial record; the other five exact-K12 retries are identical in all bounded affinity fields:
{"sequence":5,"authKind":"main-pool","accountMode":"pool","fixedAccount":true,"credentialSubstituted":true,"accountGatedModel":false,"wireModelNormalized":false,"status":200,"inbound":[{"name":"originator","size":"1-31","tag":"920c84f7054e"},{"name":"session-id","size":"32-127","tag":"8ffbff0ca501"},{"name":"thread-id","size":"32-127","tag":"002c750de0c5"},{"name":"x-client-request-id","size":"32-127","tag":"86c8b12bf13d"},{"name":"x-codex-beta-features","size":"1-31","tag":"fe15ab189c3c"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"d4a477a82f4b"},{"name":"x-codex-window-id","size":"32-127","tag":"6705e78e0f31"}],"outbound":[{"name":"originator","size":"1-31","tag":"920c84f7054e"},{"name":"session-id","size":"32-127","tag":"8ffbff0ca501"},{"name":"thread-id","size":"32-127","tag":"002c750de0c5"},{"name":"x-client-request-id","size":"32-127","tag":"86c8b12bf13d"},{"name":"x-codex-beta-features","size":"1-31","tag":"fe15ab189c3c"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"d4a477a82f4b"},{"name":"x-codex-window-id","size":"32-127","tag":"6705e78e0f31"}],"inboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"cf461ea5c5fd","kind":"string"},{"name":"session_id","size":"32-127","tag":"2e0a3e4ce6eb","kind":"string"},{"name":"thread_id","size":"32-127","tag":"f454f61e33c0","kind":"string"}],"unknownFieldCount":13},"outboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"cf461ea5c5fd","kind":"string"},{"name":"session_id","size":"32-127","tag":"2e0a3e4ce6eb","kind":"string"},{"name":"thread_id","size":"32-127","tag":"f454f61e33c0","kind":"string"}],"unknownFieldCount":13},"inboundTurnState":{"shape":"absent"},"outboundTurnState":{"shape":"absent"}} {"sequence":19,"authKind":"pool","accountMode":"pool","fixedAccount":true,"credentialSubstituted":true,"accountGatedModel":false,"wireModelNormalized":false,"status":403,"inbound":[{"name":"originator","size":"1-31","tag":"920c84f7054e"},{"name":"session-id","size":"32-127","tag":"8ffbff0ca501"},{"name":"thread-id","size":"32-127","tag":"002c750de0c5"},{"name":"x-client-request-id","size":"32-127","tag":"86c8b12bf13d"},{"name":"x-codex-beta-features","size":"1-31","tag":"fe15ab189c3c"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"148fa50040ca"},{"name":"x-codex-window-id","size":"32-127","tag":"6705e78e0f31"}],"outbound":[{"name":"originator","size":"1-31","tag":"920c84f7054e"},{"name":"session-id","size":"32-127","tag":"8ffbff0ca501"},{"name":"thread-id","size":"32-127","tag":"002c750de0c5"},{"name":"x-client-request-id","size":"32-127","tag":"86c8b12bf13d"},{"name":"x-codex-beta-features","size":"1-31","tag":"fe15ab189c3c"},{"name":"x-codex-turn-metadata","size":"512-2047","tag":"148fa50040ca"},{"name":"x-codex-window-id","size":"32-127","tag":"6705e78e0f31"}],"inboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"cf461ea5c5fd","kind":"string"},{"name":"session_id","size":"32-127","tag":"2e0a3e4ce6eb","kind":"string"},{"name":"thread_id","size":"32-127","tag":"f454f61e33c0","kind":"string"}],"unknownFieldCount":13},"outboundTurnMetadata":{"shape":"object","knownFields":[{"name":"request_kind","size":"1-31","tag":"cf461ea5c5fd","kind":"string"},{"name":"session_id","size":"32-127","tag":"2e0a3e4ce6eb","kind":"string"},{"name":"thread_id","size":"32-127","tag":"f454f61e33c0","kind":"string"}],"unknownFieldCount":13},"inboundTurnState":{"shape":"absent"},"outboundTurnState":{"shape":"absent"}}The two exact account-qualified records have the same outer session/thread/client/window tags and the same inner session/thread tags. Both have x-codex-turn-metadata size 512–2047, unknownFieldCount 13, and no turn-state. Only the turn-metadata value tag changes across the turn.
This is now a same-version isolation:
Therefore the earlier failure is not explained by the old bundled runtime or metadata size alone. The next proposed privacy-bounded diagnostic for HMAC-tagged unknown field names and coarse value kinds should now isolate the App-only field difference without exposing raw names or values.
No further private data should be necessary.
Ingwannu commentedon Aug 21, 2026
I opened Draft PR #2296 for the independently proven reconnect-affinity defect.
The patch keeps
x-codex-parent-thread-idauthoritative and, only when Desktop omits it, derives one opaque process-local HMAC affinity key from the complete boundedsession-id+thread-idpair. Selection and outcome accounting now use that same key, while exact account-qualified selectors remain outside Pool affinity. Raw Desktop identifiers and durable hashes are not stored or logged.This should stop the six App reconnects from rotating across Pool accounts. It does not claim to fix the first exact K12 workspace denial shown by the current Desktop UI, and it does not delete turn metadata or reset the thread. That App-only 403 remains open; the next diagnostic must isolate the additional App metadata field by process-local HMAC-tagged field identity/kind, never raw names or values.
Focused tests are 57/57, with typecheck, privacy scan, and diff checks passing. Because this touches account/auth affinity, #2296 remains Draft and I requested independent review from @lidge-jun before any merge.
lidge-jun commentedon Aug 22, 2026
Reconnect rotation half is fixed: PR #2296 (with adversarial-review hardening) landed on dev via #2324 (merge commit ec3d032). Desktop reconnects now bind to one pool account across the preview and final auth paths. The separate UI-denial half reported in this issue remains open and needs its own fix.
jojodat commentedon Aug 27, 2026
Retested on the current setup with OpenCodex 2.34.0 and the current ChatGPT Desktop App; both have changed since the last failing reproduction. In the same existing Desktop task, exact account selection completed successfully in this sequence: Plus → K12 account 2 → K12 account 1. All three requests returned HTTP 200, stayed in the same conversation, and were verified in the OpenCodex routing log against the intended accounts. There was no 403, reconnect loop, or fallback.
The previously reproducible Desktop-only cross-account failure therefore no longer reproduces in the current setup. This retest does not isolate what resolved the behavior; the change may be in OpenCodex, the Desktop App, the upstream service, or their interaction. From the reporter side this issue can be closed. Thank you for the fixes and investigation.
lidge-jun commentedon Aug 28, 2026
Closing as completed.
Reporter @jojodat confirmed (2026-08-27) that the Desktop cross-account sequence Plus → K12 account 2 → K12 account 1 now succeeds on OpenCodex 2.34.0: HTTP 200, same conversation, no 403 / reconnect loop / fallback.
Work already on
dev: #2055 (detail-scoped K12 classification), #2141 (K12 short-window quota), #2296 (Desktop reconnect affinity). The remaining UI-denial half no longer reproduces from the reporter's side.If it comes back on a later Desktop / OpenCodex pair, please open a new issue with a fresh log.
jojodat commentedon Aug 28, 2026
One additional observation for future reference; I am not requesting that the issue be reopened.
The successful Desktop sequence reported above occurred in a task that was originally created as a K12-bound clean test. It therefore proves that a K12-origin task can temporarily use Plus and subsequently return to either K12 account, but it was not a strict test of a Plus-origin task switching to K12.
I later resumed an older task created before the K12 accounts were added. In that same existing task:
upstream_server_error, terminating mid-stream before usage was reported;This does not appear to be a simple context-size or account-health issue: the working K12-origin task currently handles a larger input context, and both K12 accounts work normally there.
The result suggests that persistent task/thread origin or historical continuation provenance may still determine cross-account compatibility: K12-origin → Plus → K12 works, while an older Plus-origin task → K12 may remain incompatible. The former 403 is now surfaced differently in this case, so I cannot claim the underlying cause is identical.
Legacy-task compatibility is not important for my workflow, so leaving the issue closed is fine. This is only provided as potentially useful diagnostic context.