ACP harness tool calls poison session history: tool_use.name >200 chars causes permanent 400 after switching to Anthropic-format providers
- Lenguaje dominante
- Rust
- Estrellas
- 54.2k
- Forks
- 6.2k
- Merge medio
- 3 d 2 h
- PR fusionados (30 d)
- 262
Descripción
## Summary
Any session that ran turns on an **ACP harness** (Codex, Claude Code — any ACP agent) becomes **permanently unusable on Anthropic-format providers** after switching the session to a native Goose model. ACP tool calls are stored in session history with the harness's display *title* — frequently the full shell command — as the tool **name**. Anthropic rejects `tool_use.name` over 200 characters, so every subsequent prompt replays the poisoned history and fails with the same 400. Retrying can never succeed; the invalid data is persisted in the session.
```
Error: Request failed: Bad request (400):
{"message":"messages.313.content.1.tool_use.name: String should have at most 200 characters"}
```
## Repro
1. Start a session on an ACP harness (reproduced with both `codex-acp` and `claude-acp`)
2. Have it run a shell command longer than 200 characters (a long `git commit`/`git worktree` one-liner is enough)
3. Switch the session's model to a native Goose provider that uses the Anthropic message format (reproduced via `databricks_v2` → Claude)
4. Send any prompt → 400 above; every retry fails identically
## Root cause
Two layers combine:
**Write time — `crates/goose/src/acp/provider.rs`:** when converting ACP `SessionUpdate::ToolCall` notifications into session messages, the display title is stored as the tool name:
```rust
// ACP carries no canonical tool name to clients — only
// `title` (display) and `kind` (category). ...
let _ = tx.try_send(AcpUpdate::ToolCallStart {
id: id.clone(),
name: tool_call.title.clone(), // <-- often the full shell command
kind: tool_call.kind,
raw_input: tool_call.raw_input.clone(),
});
```
For shell-type calls, harnesses commonly set `title` to the entire command string. The ACP harness's own API doesn't mind, so nothing fails until later.
**Read time — `crates/goose-provider-types/src/formats/anthropic.rs`:** when formatting history for Anthropic-style APIs, `tool_call.name` is emitted verbatim into the `tool_use` block (`NAME_FIELD: tool_call.name`, two sites) with no length sanitization. Anthropic enforces a 200-character maximum on `tool_use.name`.
Note this formatter already defends against a sibling class of replay-poisoning (placeholder `tool_use` for unparseable calls so `tool_result` isn't orphaned; non-object `input` coercion documented as fixing another 400-on-replay) — tool-name length is the same category of problem, currently unhandled.
## Impact
- Model switching away from an ACP harness silently breaks the session: the user sees a generic request failure and "try again" loops forever
- On one developer machine we found **13 affected sessions** across `codex-acp` and `claude-acp` histories, with stored tool names up to **2,143 characters**
- The failure appears long after the cause (whenever the user later switches models), making it very hard for users to connect cause and effect
## Expected behavior
Switching a session between harnesses/providers should never produce history the destination provider rejects.
## Suggested fix (both layers)
1. **Write time:** store a canonical short tool name for ACP tool calls (e.g. derive from `kind`, or a fixed name like `acp_tool`), keeping the display title in metadata/arguments where the renderer can still show it. `provider.rs` already passes `kind` through `tool_meta` (`goose.acp.kind`), so the display concern is separable from the wire name.
2. **Read time (defensive, and heals existing sessions):** sanitize/truncate `tool_use.name` to the provider's limit when formatting messages in `formats/anthropic.rs` (and audit other formats — Bedrock's converter builds `tool_use` blocks too). Without this, every already-poisoned session stays permanently broken even after the write-time fix.
## Related but distinct
- #10660 — hardens the Goose→ACP handoff memo lifecycle (opposite switch direction; doesn't touch native-provider history replay)
- #10751 — transport-level WebSocket drop on oversized prompts (connection layer, not message-content validity)
Both point at the same underlying gap: harness switching has no principled history-translation layer that shapes prior turns to the destination's constraints.
Guía de contribución
Evaluación
Este issue todavía no se ha evaluado.