aaif-goose / aaif-goose/goose

ACP harness tool calls poison session history: tool_use.name >200 chars causes permanent 400 after switching to Anthropic-format providers

Abierto
#10,807 10 comentarios 0 reacciones 1 asignado Asignado a @lifeizhou-ap Ver en GitHub
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

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.