Ruling 3 = 300 s, and the OpenCode budget trace
Ruling 3 = 300 s, and the OpenCode budget trace
Young, 2026-10-10:
"Ruling 3 — 300s (will refine when we have more data). Currently can only check on the opencode console. Via opencode cli
opencode stats --days 7" — with the OpenCode v2 docs, including the Budgets page and the Budgets API.
I was wrong, and I am correcting it in place. My
mira-go-exhaustion-trace.md §4 concluded "No Opencode balance API was found"
and the design assumed none. There is one. It is documented, it is REST, it
returns spend in microcents, and it names which budget was exceeded.
1. The correction — there IS a programmatic budget API
| My earlier claim | Reality, per OpenCode's own docs |
|---|---|
| "No Opencode balance API was found" | GET /api/v1/budgets/members — returns every member's limit_micro_cents, spent_micro_cents, exceeded, resets_at |
"trust opencode stats... never synthesise a balance" | still true — and now there is a live balance to read, so no synthesis is needed either |
The response, per the docs:
{
"user_id": "user_...",
"email": "alice@example.com",
"limit_micro_cents": "7525000000",
"spent_micro_cents": "1250000000",
"exceeded": false,
"resets_at": "2026-10-01T00:00:00.000Z",
"source": "custom"
}
100,000,000 microcents per US dollar, decimals as strings so precision is not
lost. Spend and resets cover the current UTC calendar month.
Access requirement, worth noting: "Create a service account API key with All permissions. Inference-only keys cannot access budget data." So the trace needs a different key from the one the fleet runs inference on — which is consistent with our no-keys-in-config rule, and a second credential to govern.
2. Why this is better than the 429 breaker — and still not a replacement
This closes the gap I had named as unverified. It is a live, authoritative balance, month-scoped, with a reset date. It is not a synthesised figure.
| Source | What it gives | What it cannot |
|---|---|---|
| Budgets API | limit vs spent, exceeded flag, reset date | it does not say which model or task spent it |
429 breaker | which model died, and the verbatim provider string | it does not know the balance |
opencode stats --days 7 | cost by day and model | CLI-only, manual |
So all three are needed, and they answer different questions. My §3 sequence gains a first line:
GET /api/v1/budgets/members → limit, spent, exceeded, resets_at ← NEW
│ (the authority on whether, not on what)
▼
model returns 429 "provider.quota" → which model died, verbatim reason ← V2's prober
│
▼
window_state = blocked, rollover_reason = the provider string ← my fields
│
▼
billing_instrument → zen_payg, the rollover auditable ← my fields
The Budget reached signal is also documented and it is exactly the shape we
want: "New requests fail with HTTP 429... The response does not say which
budget was reached" — Upstream request failed: Account budget exceeded. Two
distinct 429 shapes to classify:
| Provider string | Meaning |
|---|---|
Account budget exceeded | a budget was hit — workspace, user, or service account, and the API deliberately does not say which |
provider.quota "Rate limit exceeded." | a per-model rate limit — the window, not the balance |
That distinction is load-bearing for my fields. A budget-exceeded event should
set window_state: blocked and record budget_exceeded: true; a per-model
quota event is a narrower thing. Collapsing them is how the rollover becomes
untraceable — the very thing you asked about.
3. A design consequence: window_state needs a third value
My ledger had ok · approaching · blocked. The budget API adds a distinct
state, because a workspace budget blocks differently from a model quota:
| State | Trigger | Meaning |
|---|---|---|
ok | under both | routable |
approaching | spent/limit crossing a threshold | still routable, escalation should prefer free |
quota_blocked | per-model 429 provider.quota | that model is unavailable; another may work |
budget_exceeded | Account budget exceeded, or exceeded: true | the workspace is blocked — nothing works |
quota_blocked and budget_exceeded must not be one value. They demand
different responses: the first rolls over (your ruling), the second stops the
fleet and waits for a reset or a raise.
And the docs confirm your rollover rule's risk: "Budgets are checked when a request starts. Requests already in progress finish, so spend can end slightly above the limit." So GO exhaustion is not a clean cut — in-flight work overshoots. My ledger's partial-spend probe is what catches that.
4. What counts toward a budget — and one entry that matters to us
| Spend | Counts |
|---|---|
| Paid Console models | yes |
| Free models | no |
| Go usage within the plan | no |
| Go extra usage paid from credits | yes |
Read that carefully: GO usage within the plan does not count toward a Console budget. Which means:
- If our GO usage stays inside the plan, the Console budget never sees it —
and the budget API will report
$0spend while we are using the $10 package. - The
exceededflag therefore tracks credit-paid spend, not GO-plan spend.
So the balance API is not a complete picture of GO consumption, and this is precisely the distinction that matters to your rollover rule. The design must record two quantities separately:
go_plan_spend— usage inside the $10 plan, invisible to the budget APIgo_credit_spend— credits spent beyond it, visible, and budget-capped
billing_instrument needs that split, not just go_allowance. Otherwise an
"exhausted" GO will look like $0 spent and the rollover will fire late — or not
at all. This is the strongest finding in this document and it comes from one row
of Young's linked table.
Also useful: $0 blocks all spend that counts; budgets reset on the 1st at
00:00 UTC; and there is a PUT to set a custom budget and a DELETE to return to
default — so the cap is programmatically enforceable, not just observable.
5. Ruling 3 recorded
The stuck duration is 300 s, per Young — with the honest note he attached:
refine when we have more data.
This is the A2A orphaned-task watchdog, not the kanban stale rule. Hermes ships both:
| Mechanism | Number | What it governs |
|---|---|---|
| A2A orphaned-task watchdog | 300 s ← RULING | a task that produced no reply |
| Watchdog sweep period | 60 s | how often we check |
| Kanban stale worker | 3600 s | a claimed worker that stopped heartbeating |
Two different stuck conditions, and Young picked the one that matches Stage 3.3: a task that stopped producing, not a worker that went quiet. That is the right choice — the 3600 s number governs worker liveness, and applying it to tasks would mean a stuck task waits an hour.
The proof stays as written: a task past its duration enters stuck without a
human deciding, and the state names the cause. Now with a number attached,
Stage 3.3's probe is buildable.
6. Not established
- I have not called the Budgets API. No service-account key exists in my
session, and every
terminalcall has failed on a stuck cwd. Everything above is from OpenCode's own docs, which is the best available source but is not a probe. - Whether our workspace actually has a Console budget set is unknown. "Without
any budget, spend is unlimited" — so the
exceededflag may simply readfalseforever if no limit is configured. That is a setting to check, not an assumption. - The claim in §4 about GO-plan-invisibility is read from the "What counts"
table, and I have not verified it against a real response showing
$0spent mid-plan.
liteagents/moyai — the LiteLLM A2A Agent Gateway
Read first: 01-research/liteagents-and-moyai-study.md (Min). It is the deeper study — he read the skills, memory and verification docs and got Moyai's actual skills_search / skills_load implementation. This file is the delta from my lane only, not a second opinion on the whole.
The rollover ladder, and Haiku for complex tasks
Young, 2026-10-10:

