From b421b1b479ee9ea0863792282eee4ffeb50923dc Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 29 Sep 2026 12:20:11 +0200 Subject: [PATCH] Read Codex limits without waiting on account/read (#13733) Codex 0.158's app-server can leave account/read unanswered, and asking it first lost the limits whenever it did, leaving the agents panel showing "Codex limits unavailable". The limits name the plan themselves, so they're asked first and account/read is only a short fallback when they don't. Co-authored-by: Claude Opus 5.5 --- bin/omarchy-agent-usage-codex | 18 +++++++++++++----- test/shell.d/agent-usage-codex-scanner-test.sh | 17 +++++++++++++++-- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/bin/omarchy-agent-usage-codex b/bin/omarchy-agent-usage-codex index 972c164d..6e2d918f 100755 --- a/bin/omarchy-agent-usage-codex +++ b/bin/omarchy-agent-usage-codex @@ -544,12 +544,20 @@ def fetch_codex_rpc(): rpc_request(proc, 1, "initialize", {"clientInfo": {"name": "omarchy-agent-usage", "version": "1"}}, timeout=8) proc.stdin.write(json.dumps({"method": "initialized", "params": {}}) + "\n") proc.stdin.flush() - account_msg = rpc_request(proc, 2, "account/read", timeout=4) - limits_msg = rpc_request(proc, 3, "account/rateLimits/read", timeout=4) - - account = (account_msg.get("result") or {}).get("account") or {} + limits_msg = rpc_request(proc, 2, "account/rateLimits/read", timeout=8) limits = (limits_msg.get("result") or {}).get("rateLimits") or {} - plan = limits.get("planType") or account.get("planType") or account.get("type") or "" + plan = limits.get("planType") or "" + + # The limits name the plan themselves. account/read is only a fallback + # for when they don't, and never a reason to lose them: Codex 0.158's + # app-server can leave it unanswered for good. + if not plan: + try: + account_msg = rpc_request(proc, 3, "account/read", timeout=2) + account = (account_msg.get("result") or {}).get("account") or {} + plan = account.get("planType") or account.get("type") or "" + except TimeoutError: + pass result["tierLabel"] = str(plan) if plan else "" for window in (limits.get("primary"), limits.get("secondary")): diff --git a/test/shell.d/agent-usage-codex-scanner-test.sh b/test/shell.d/agent-usage-codex-scanner-test.sh index 3ce4883c..4b0b7fb6 100644 --- a/test/shell.d/agent-usage-codex-scanner-test.sh +++ b/test/shell.d/agent-usage-codex-scanner-test.sh @@ -26,10 +26,12 @@ while read -r request; do jq -cn --argjson id "$id" '{id: $id, result: {}}' ;; account/read) - jq -cn --argjson id "$id" '{id: $id, result: {account: {}}}' + # Codex 0.158 can leave this one unanswered for good. + [[ -n ${CODEX_ACCOUNT_READ_HANGS:-} ]] || + jq -cn --argjson id "$id" '{id: $id, result: {account: {}}}' ;; account/rateLimits/read) - jq -cn --argjson id "$id" '{id: $id, result: {rateLimits: {}}}' + jq -cn --argjson id "$id" --argjson limits "${CODEX_RATE_LIMITS:-{\}}" '{id: $id, result: {rateLimits: $limits}}' ;; esac done @@ -603,3 +605,14 @@ result=$(HOME="$INTERRUPTED_HOME" CODEX_HOME="$INTERRUPTED_HOME/.codex" XDG_CACH [[ $(jq -r '.todayTotalTokens' <<<"$result") == "9" ]] || fail "Codex collector does not reuse a snapshot from an interrupted scan" "$result" pass "Codex collector does not cache an interrupted opencode scan" + +# The limits name the plan themselves, so an account/read that never answers +# costs nothing: the limits still arrive, and quickly. +started=$(date +%s) +result=$(HOME="$TEST_HOME" CODEX_HOME="$TEST_HOME/.codex" XDG_DATA_HOME="$TEST_HOME/.local/share" PATH="$TEST_HOME/bin:$PATH" \ + CODEX_ACCOUNT_READ_HANGS=1 CODEX_RATE_LIMITS='{"planType":"pro","primary":{"usedPercent":36,"windowDurationMins":10080}}' \ + "$ROOT/bin/omarchy-agent-usage-codex" --limits-only) +(( $(date +%s) - started < 4 )) || fail "Codex collector doesn't wait on account/read when the limits name the plan" +[[ $(jq -c '{tierLabel, usageStatusText, limits: [.limits[] | {label, percent}]}' <<<"$result") == '{"tierLabel":"pro","usageStatusText":"","limits":[{"label":"Weekly (7-day)","percent":0.36}]}' ]] || + fail "Codex collector reads limits even when account/read never answers" "$result" +pass "Codex collector reads limits even when account/read never answers"