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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user