feat(orchestrator): wire turn-decision endpoint and QuotaReported producer

Closes Phase 2 items 1-2 (AUDIT.md): Coordinator.TurnDecision evaluates
occupancy/turn-boundary/handoff state synchronously per turn and returns
continue/prepare_handoff/rotate_now/refuse, exposed via POST
/v1/harness/turn. The Claude Stop hook now calls it on ordinary turn
boundaries instead of no-op'ing, and exits 2 on refuse.

Also closes B7's post-hoc producer: /v1/harness/complete now appends a
QuotaReported event from the completing lease's harness usage, so the
router's quota-availability filter and the brief's quota_consumed stop
evaluating against a permanent zero. Live per-harness push producers
(Claude statusline, Codex rollout tail) remain unbuilt — investigation
recorded in AUDIT.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
kami
2026-07-27 23:17:21 +04:00
parent 0ca78243b9
commit 972845bd98
5 changed files with 382 additions and 21 deletions
+92
View File
@@ -760,3 +760,95 @@ whether Codex's/opencode's own turn-boundary mechanism actually surfaces this
in-pane prompt to the agent before it exits the way Claude Code's Stop hook
does — that's Phase 2 item 4 territory (native Face B per harness), not
Phase 4.
---
## Real harness quota sources — verified locally, 2026-07-27
Investigation, not a code change: B7 says `QuotaReported` has exactly one
producer (the `TaskCompleted` handler in `cmd/orchestra/main.go`, deriving
`consumed` from the harness's self-reported `usage.Numerator()`). The
question was whether any harness exposes its *real* subscription quota so
Orchestra can stop relying on operator-entered static caps plus estimated
token consumption. Two of three do. Everything below was read off this
machine's own installs, not recalled.
### Claude Code — statusline stdin (confirmed)
The JSON blob Claude Code pipes to `statusLine.command` on every render
carries server-reported rate-limit levels. `~/.claude/statusline.sh` already
reads them:
- `.rate_limits.five_hour.used_percentage`
- `.rate_limits.seven_day.used_percentage`
plus `.context_window.{used_percentage,total_input_tokens,context_window_size}`,
`.cost.total_cost_usd`, `.session_id`. These are percentages of the real
subscription pool, not estimates, and the two windows map 1:1 onto
`router.QuotaWindowLimits{FiveHour, Weekly}`. High frequency, zero cost.
Note: `claude.ai/api/organizations/{org_id}/usage` also exists but is
authenticated by **claude.ai session cookies, not an API key** — wiring it
would make Orchestra hold and refresh a logged-in browser session. The
statusline path avoids that entirely and should be preferred.
### Codex — `rate_limits` in the session rollout (confirmed)
Every `token_count` event in `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`
carries a `rate_limits` object, e.g.:
```json
"rate_limits": {
"limit_id": "codex",
"primary": { "used_percent": 20.0, "window_minutes": 10080, "resets_at": 1785650936 },
"secondary": null,
"credits": { "has_credits": false, "unlimited": false, "balance": "0" },
"plan_type": "plus",
"rate_limit_reached_type": null
}
```
Richer than Claude's: `window_minutes` makes the window self-describing
(10080 = weekly) and `resets_at` is absolute. There is **no `codex usage`
subcommand** — the transport is the rollout file, or `codex app-server`,
which emits the same events live.
### opencode — no first-class quota surface
- `opencode stats` is historical accounting only (cost/tokens/tools across
past sessions). No limits, no remaining.
- Zen is an OpenAI-compatible gateway at `https://opencode.ai/zen/v1` (and
`/zen/go/v1`). The binary contains `x-ratelimit-limit` /
`-remaining` / `-reset` / `-reset-after`, so the Zen free tier's **daily**
quota arrives as response headers, not via a queryable endpoint. Capturing
it requires intercepting a response — an opencode plugin
(`~/.config/opencode/plugin/`) is the only clean hook.
### Design consequences (not yet implemented)
1. **Percentages are a level, not a delta.**
`router.QuotaAvailability.sumSince` *sums* `consumed` across
`QuotaReported` events. Feeding a used-percentage into that sum is wrong
by construction. A real-quota feed needs a second `Availability`
implementation that reads the **latest** report per harness. `Availability`
is already an interface (`internal/router/router.go`), so this is a swap,
not a rewrite — and the additive `sumSince` path must stay for the
estimate-based producer (spec §5.2.1 receipts are genuinely additive
across rotations).
2. **Static `quota_limit_5h`/`quota_limit_weekly` become unnecessary** for
Claude and Codex, since the harness reports its own fraction of pool and
the 80% conservative rule applies directly with no operator-entered cap.
Keep the static path as the fallback for opencode.
3. **`QuotaWindowLimits{FiveHour, Weekly}` is too narrow.** Codex's windows
are self-describing via `window_minutes`, and opencode's Zen tier is
**daily** — a window Orchestra has no concept of today. A generic
`[]{WindowMinutes, UsedPercent, ResetsAt}` fits all three; the current
two named fields fit only Claude.
4. **Push path**: statusline script (Claude) and a rollout-tail or
app-server reader (Codex) POST to a new endpoint alongside
`/v1/harness/turn` in `cmd/orchestra/main.go` — the existing hook-ingress
pattern — appending `QuotaReported`. That gives B7 a second, *live*
producer next to the post-hoc one, and covers the case CLAUDE.md already
flags: a harness that never completes a task cleanly (the stuck `wA` pane)
currently under-counts its consumption forever, because the only producer
fires on completion.