app-server clients cannot use Luna Reserve: supportsLunaReserve exists but has no accept/redeem action
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
What version of Codex CLI is running?
codex-cli 0.154.0 (standalone build at ~/.codex/packages/standalone/current/bin/codex)
What subscription do you have?
ChatGPT Plus
Which model were you using?
gpt-5.6-luna (also reproduced by running gpt-5.6-sol first, then retrying gpt-5.6-luna on the same thread)
What platform is your computer?
Darwin 23.6.0 arm64 arm (macOS 14.8.7)
What terminal emulator and version are you using (if applicable)?
N/A. This is driven headlessly through codex app-server over stdio JSON-RPC (from a third-party client: Paseo daemon 0.8.0).
Codex doctor report
not available
What issue are you seeing?
Third-party clients that talk to codex app-server cannot use the Luna Reserve fallback after the ordinary usage limit is exhausted, even though the account is eligible and the reserve pool is visible over the same protocol.
account/rateLimits/read returns all of the following:
{
"ordinaryUsageAllowed": false,
"rateLimitsByLimitId": {
"codex": { "primary": { "usedPercent": 100, "windowDurationMins": 300 } },
"base_model_inference": {
"limitId": "base_model_inference",
"limitName": "gpt-reserve",
"normalModelSlug": "gpt-5.6-luna",
"primary": { "usedPercent": 6, "windowDurationMins": 10080 }
}
},
"rateLimitUpsell": { "banner_type": "luna_reserve", "title": "You're now using Luna, a faster model for simpler tasks." }
}
So the ordinary codex 5h window is exhausted, but the gpt-reserve pool (which maps to gpt-5.6-luna) is only 6% used and a luna_reserve banner is offered. Nevertheless a turn fails:
error: You've hit your usage limit. Upgrade to Pro (https://chatgpt.com/explore/pro), visit
https://chatgpt.com/codex/settings/usage to purchase more credits or try again at 10:58 AM.
codexErrorInfo: "usageLimitExceeded"
turn/completed status: "failed"
Setting supportsLunaReserve: true on account/rateLimits/read does not change this. The interactive TUI works for the same account, so the entitlement exists; only app-server clients are blocked.
What steps can reproduce the bug?
- Confirm the protocol surface (only one Luna-related field exists, and there is no accept/redeem method):
codex app-server generate-json-schema --out /tmp/gen
grep -r supportsLunaReserve /tmp/gen # only GetAccountRateLimitsParams
- Spawn
codex app-server(stdio) and drive it with JSON-RPC:
initialize { clientInfo: {name:"codex_app_server_daemon",title:"Codex App Server Daemon",version:"0.0.0"},
capabilities: { experimentalApi: true } }
initialized {}
account/rateLimits/read { supportsLunaReserve: true } # returns luna_reserve banner + gpt-reserve pool
thread/start { model: "gpt-5.6-luna", cwd: "/tmp" }
turn/start { threadId, input: [{type:"text", text:"Reply with exactly OK and nothing else."}] }
- Observe the
errornotification withcodexErrorInfo: "usageLimitExceeded"followed byturn/completedwithstatus: "failed".
Minimal repro (Node, ~40 lines) — line-delimited JSON-RPC over stdio:
import { spawn } from "node:child_process";
const child = spawn(process.env.HOME + "/.codex/packages/standalone/current/bin/codex", ["app-server"], { stdio: ["pipe","pipe","pipe"] });
let id = 0, buf = ""; const pending = new Map();
const send = (method, params) => { const i = ++id; child.stdin.write(JSON.stringify({ id: i, method, params }) + "\n");
return new Promise((res, rej) => pending.set(i, { res, rej })); };
const notify = (method, params) => child.stdin.write(JSON.stringify({ method, params }) + "\n");
child.stdout.on("data", (d) => { buf += d; let i;
while ((i = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1); if (!line) continue;
const m = JSON.parse(line);
if (m.id !== undefined && (m.result !== undefined || m.error)) { const p = pending.get(m.id); if (p) { pending.delete(m.id); m.error ? p.rej(new Error(JSON.stringify(m.error))) : p.res(m.result); } }
else if (m.id !== undefined && m.method) child.stdin.write(JSON.stringify({ id: m.id, result: {} }) + "\n");
else if (m.method === "error" || m.method === "turn/completed") console.log(m.method, JSON.stringify(m.params).slice(0, 300));
}
});
const main = async () => {
await send("initialize", { clientInfo: { name: "codex_app_server_daemon", title: "Codex App Server Daemon", version: "0.0.0" }, capabilities: { experimentalApi: true } });
notify("initialized", {});
const rl = await send("account/rateLimits/read", { supportsLunaReserve: true });
console.log("ordinaryUsageAllowed:", rl.ordinaryUsageAllowed, "| banner:", rl.rateLimitUpsell?.banner_type,
"| reserve used%:", rl.rateLimitsByLimitId?.base_model_inference?.primary?.usedPercent);
const ts = await send("thread/start", { model: "gpt-5.6-luna", cwd: "/tmp" });
await send("turn/start", { threadId: ts.thread.id, input: [{ type: "text", text: "Reply with exactly OK and nothing else." }] });
};
main();
I also tried declaring supportsLunaReserve: true, running a blocked gpt-5.6-sol turn, and then retrying gpt-5.6-luna on the same thread and connection. It still fails with usageLimitExceeded.
What is the expected behavior?
One of:
- The app-server protocol should expose the same "Continue with Luna Reserve" recovery the TUI performs (i.e. an RPC to accept/redeem the reserve, or a way to mark a turn/thread as a reserve continuation), or
supportsLunaReserve: trueshould actually enable the server-side fallback for subsequent model requests.
Right now supportsLunaReserve is documented as "allow the backend to record experiment exposure after ordinary usage is blocked", and there is no corresponding action in the protocol. The whole flow appears to live only in codex_tui:
codex_tui::chatwidget::backend_banners::ChatWidget::prepare_luna_reserve_return
codex_tui::chatwidget::backend_banners::ChatWidget::defer_pending_turn_for_luna_reserve
codex_tui::chatwidget::backend_banners::ChatWidget::apply_reserve_fallback_to_pending_turn
codex_tui::chatwidget::backend_banners::ChatWidget::show_unavailable_reserve_recovery
plus the header x-openai-codex-luna-reserve, the state directory ~/.codex/tui-luna-reserve/, and error code luna-reserve-recovery — with no equivalent core/app-server entry point.
Additional information
- Related: #40939 (CLI cannot use Luna Reserve), #41520 (Cannot use gpt-reserve), #42153 (Desktop manual Luna switch stays blocked), #41224.
- Practically, this blocks any integrator that embeds Codex via
codex app-server(Paseo, IDE integrations, custom UIs) from using a reserve the account already has, forcing users back to the interactive TUI. - Console login
codex loginuses the same ChatGPT Plus account; not a multi-account or workspace-owner issue.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the app-server protocol surface around account/rateLimits/read, thread/start, and turn/start, then compare it with the named codex_tui Luna Reserve handlers. Check the generated schema for the missing recovery action and trace how the TUI uses the reserve state and error code. Done means an app-server client can invoke or trigger the same reserve recovery and complete a turn.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, rust
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100