block / block/buzz

buzz-acp: model fallback list for headless agents whose model hits a usage limit

Open
#5,605 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

### Context

`buzz-acp` applies a single `BUZZ_ACP_MODEL` to every session it creates. That works when a person is watching. It does not when the harness runs as a systemd unit and the agent is expected to answer while nobody is at the desk.

When the configured model's subscription limit is exhausted, the agent keeps running and keeps answering, but every reply is the provider's limit notice.

### What happens today

With `claude-agent-acp` against a Claude subscription whose Fable 5 limit is used up:

```
$ claude -p "Reply with exactly: OK" --model claude-fable-5
You've reached your Fable 5 limit. Run /usage-credits to continue or switch models with /model.
$ echo $?
0
```

The same call with `--output-format json`:

| field | value |
|---|---|
| `type` | `result` |
| `subtype` | `success` |
| `stop_reason` | `stop_sequence` |
| `is_error` | `true` |
| `result` | the limit notice text |

The process exits 0 and the turn completes normally. Through ACP this arrives as ordinary agent text, so `buzz-acp` posts the limit notice into the channel as the agent's answer and keeps doing so on every subsequent mention.

### Why the existing lever does not cover this

`switch_model` already exists and works well. `handle_switch_model_control` validates against the cached catalog, cancels an in-flight turn, and requeues the batch under the new model, so the recovery itself is solved.

What is missing is the trigger. The control frame has to be owner-signed and sent from a client, and `ModelPicker` in the desktop is currently the only sender. A headless agent has nobody to press it.

### Proposal

Let `BUZZ_ACP_MODEL` accept an ordered list, for example `claude-fable-5[1m],opus[1m],sonnet`. When the current entry is exhausted, `buzz-acp` advances to the next one through the same path `switch_model` uses and retries the turn once. An observer event would let clients show that it happened.

### The detection problem

There is no error to key on. A quota-exhausted turn is a successful turn whose text happens to be the provider's notice. Two ways out, and the second looks better:

1. `buzz-acp` matches the notice text per adapter. Brittle, and it breaks whenever the wording changes.
2. The adapter surfaces it. `claude-agent-acp` already sees `is_error: true` next to `subtype: "success"`; propagating that as an ACP error, or as a distinguishable stop reason, would let any client handle it correctly. That part may belong in the adapter rather than here.

Glad to send a PR for the list-and-retry side if the shape sounds right.

### Versions

- `buzz-acp` built from `bba3e06`
- `@agentclientprotocol/claude-agent-acp` 0.64.0

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.