Features: usage-limit refusal vs real reply (CB-578 stage A), charter receipt (CB-571)

Dai Ha
2026-08-15 10:06:09 +02:00
parent 8781f01338
commit 76690bca13
+51
@@ -57,6 +57,8 @@ six weeks, and the table alone will not carry it.
| [Answer a question on an async delegation](#answer-a-question-on-an-async-delegation) | automatic | CB-574 | `msg/MessageService` |
| [Learn why a delegation died](#learn-why-a-delegation-died) | automatic | CB-568 | `inject/CompletionResolver` |
| [Watch the fleet's health](#watch-the-fleets-health) | `health:` | CB-573 | `health/FleetHealthMonitor` |
| [Tell a usage-limit refusal from a real reply](#tell-a-usage-limit-refusal-from-a-real-reply) | profile `exhaustedPattern:` | CB-578 | `inject/CompletionResolver` |
| [See which charter a member got](#see-which-charter-a-member-got) | automatic | CB-571 | `peer/CharterReceipt` |
Nearly every knob above lives in one file, on one profile:
@@ -1020,6 +1022,55 @@ and has its own test — it is a known edge, not an oversight. The retries also
---
## Tell a usage-limit refusal from a real reply
**What.** A member can end its turn without calling `bridge_reply`. The bridge then scrapes the pane
and hands that text back as the answer. Sometimes that text is not an answer at all — it is the
backend refusing, because the account hit its usage limit. With this on, the bridge matches the scrape
against a pattern you configure. On a match it resolves the send as `BACKEND_EXHAUSTED` and carries the
matched line as the reason, instead of passing a refusal off as a completed reply.
**On.** Per profile, `exhaustedPattern:` — a regex. Opt-in: leave it out and that profile's completion
fallback behaves exactly as before. Startup logs one line naming which profiles have a pattern and
which do not, so you can see the coverage without reading the config by hand.
**Why.** The old behaviour lied in the worst direction. A lead asked for work, got back a block of
text, and had no way to tell "here is your answer" from "my account is refusing to run". The lead
would then treat a refusal as a result. Keeping the pattern in config, never in Java, is deliberate:
every backend words its refusal differently, so a sentence baked into the code would only ever match
one vendor.
**Gotcha.** The pattern map is built once at startup from the config snapshot, so `exhaustedPattern`
is a **deferred** key — adding one to a profile does nothing until the daemon restarts.
`bridged.example.yaml` does not say this yet. Also, this stage only *classifies*. Nothing yet stops
the fleet spawning another member onto the same exhausted account, and nothing yet saves the work that
member was doing — those are stages B and C of CB-578.
---
## See which charter a member got
**What.** Every member launch records a `CharterReceipt`: the role, where the charter came from, a
sha-256 digest of it, and its size in bytes. It is stored on the session and shown in the roster
(`bridge_list` and `GET /members`). The charter text itself is never recorded.
**On.** Automatic.
**Why.** Charters are per-role config and are re-read on every spawn, so two members of the same role
can get different text without anyone noticing. The digest answers "did this member actually get the
charter I think it got?" without printing prompt text into logs an operator may not be allowed to
keep. It also closed a real leak: the older pane-placement spawn log printed the whole `argv`, and the
charter travels inside `argv`. That argument is now replaced by its digest.
**Gotcha.** `PeerHandle.charterReceipt()` is deliberately **not** a `default` method. It used to be,
and `OpenCodeLauncher`'s wrapping handle forgot to override it — so it answered `null` while the real
receipt sat on its delegate, and `sol` and `terra` silently showed no receipt at all while Claude Code
members showed one. Nothing failed; the roster field was just quietly missing. Removing the `default`
makes the compiler catch that, and any new adapter must now answer the question on purpose. If you add
a `PeerHandle` implementation, this is the line that will not let you skip it.
---
## Backfill status
This page was started after the fact, so it is **not yet complete**. Entries above are written from