Repository navigation
Agents panel: Codex limits always "unavailable" — rpc_request misses replies already buffered by readline() #13773
Description
System
- Omarchy 4.0.4-1
- CPU: 13th Gen Intel Core i7-13700H
- GPU: Intel Raptor Lake-P (Iris Xe)
- codex-cli 0.156.1 (logged in with ChatGPT, Plus plan)
What happens
The agents panel shows Codex usage from local stats, but never its plan or limits.
~/.local/state/omarchy/agents/usage/codex.json always ends with:
"limits": [],
"tierLabel": "",
"usageStatusText": "Codex limits unavailable",
"authHelpText": "account/read""account/read" is the TimeoutError(method) raised by rpc_request in
bin/omarchy-agent-usage-codex.
Expected
Plan and the 5-hour / weekly limits, which the app-server returns fine:
account/read answers in ~0.5 s and account/rateLimits/read in ~0.8 s.
Steps to reproduce
- Codex 0.156.1 logged in with ChatGPT.
omarchy agent usage update --force codexjq '{usageStatusText, authHelpText, limits}' ~/.local/state/omarchy/agents/usage/codex.json
→Codex limits unavailable/account/read/[], every run.
Cause
Right after initialized, codex 0.156.1 emits two notifications
(remoteControl/status/changed, account/updated). They arrive in the same
chunk as the reply to account/read.
rpc_request waits with select.select([proc.stdout], ...) and then calls
proc.stdout.readline(). proc.stdout is a buffered text stream: the first
readline() pulls the whole chunk into Python's buffer and returns one line.
The reply is now in that buffer, not in the pipe, so select() never reports
the fd readable again and the loop spins until the 4 s timeout. The exception
also skips account/rateLimits/read, so no limits are recorded.
Traced from inside the collector:
0.1s <- {"id":1,"result":{...}} (initialize)
0.5s <- {"method":"remoteControl/status/changed",...}
0.5s <- {"method":"account/updated","params":{"authMode":"chatgpt","planType":"plus"}}
total 4.3s -> "Codex limits unavailable", "account/read"
With the only change being to skip select() (blocking readline()), the same
code returns in 0.9 s with tierLabel: "plus" and both limit windows.
The code is unchanged on master (select.select([proc.stdout], [], [], 0.25)
in rpc_request), so this affects any codex version that sends notifications
before a reply.
Suggested fix
Don't mix select() with a buffered reader. For example, read stdout on a
background thread into a queue and wait on the queue with the timeout:
import queue, threading
def start_reader(proc):
lines = queue.Queue()
def pump():
for line in proc.stdout:
lines.put(line)
lines.put(None)
threading.Thread(target=pump, daemon=True).start()
return lines
def rpc_request(proc, lines, request_id, method, params=None, timeout=8):
payload = {"id": request_id, "method": method, "params": params or {}}
proc.stdin.write(json.dumps(payload) + "\n")
proc.stdin.flush()
deadline = time.time() + timeout
while (remaining := deadline - time.time()) > 0:
try:
line = lines.get(timeout=remaining)
except queue.Empty:
break
if line is None:
break
try:
message = json.loads(line)
except Exception:
continue
if message.get("id") == request_id:
return message
raise TimeoutError(method)(fetch_codex_rpc creates lines = start_reader(proc) once after Popen and
passes it to each call.) Alternatively, open the pipe unbuffered in binary mode
and split lines from os.read() yourself.
Activity
jandrusk commentedon Oct 1, 2026
Clear root-cause write-up — thank you.
This is the same buffered-
readline/selectrace already tracked by the Codex limits cluster. Prefer consolidating on the open fix PRs rather than a third parallel diagnosis:Suggest closing this as a duplicate of #13703 / tracking via #13816 once one of those lands. UX follow-up for method names leaking into
authHelpTextis #13740 / #13059 (separate from the race).