blackjax-devs / blackjax-devs/agent-team
Agent.halt() / urgent interrupt wedges the recipient's MCP bridge (CLI session-persistence mode)
- Dominant language
- Python
- Stars
- 3
- Forks
- 1
- Avg merge
- 21m
- Merged PRs (30d)
- 1
Description
## Summary
In the channel (sagent `AnthropicCLI` provider, **session-persistence** mode), interrupting an agent mid-turn via `Agent.halt()` — our `urgent=true` peer message / web-UI interrupt / `/api/interrupt` — **permanently wedges the recipient**. Every subsequent turn fails with:
```
SubprocessTransportError: AnthropicCLI: MCP bridge catalog not fetched within Ns
(CLI MCP client never connected); respawning rather than running a tool-less turn
```
…recoverable only by a **full server restart**. We've disabled the urgent preempt as a workaround (`3e62f0e`); filing this to track the root cause + a question for the sagent maintainer.
## Impact
- The interrupt feature is unsafe: halting an agent breaks it until the whole channel restarts.
- Observed live **twice** on 2026-06-19: `swe` (a runtime cancel, ~10:22 UTC) and `tech-writer` (operator `urgent=true` "STOP", 13:44 UTC). Each → repeated MCP-catalog errors, agent stuck idle, only a full restart cleared it.
- A **per-agent** restart (`Agent.clear()`) does **not** recover it — and `clear()` itself can *hang* on the wedged subprocess.
## Repro signature
1. Run a session-persistent `AnthropicCLI` agent (`--session-id` / `--resume`).
2. Cancel its model-call mid-turn (`Agent.halt()` → runtime cancels the task → `CancelledError` reaches the provider's `stream`).
3. Send the agent another turn.
→ The new (cold) subprocess's MCP client never connects; `wait_listed` times out → `SubprocessTransportError`. Repeats every turn thereafter.
## Root-cause analysis (sagent @ `e67b89a`)
The cancel path differs between the two CLI modes:
- **Stateless path** (`providers/anthropic_cli.py:816-826`): `_interrupt_active_proc()` (SIGINT) → `respawn_after_transport_failure()`, which swaps to a **pre-warmed HotSpare** subprocess (already connected to the bridge). **Recovers.**
- **Session-persistent path** (`providers/anthropic_cli.py:951-960`, the mode a persistent channel must use): `_interrupt_active_proc()` (SIGINT) → `await proc.close()` → `self._active_proc = None`. **No warm spare** — the next turn spawns a **cold** subprocess whose MCP-catalog fetch never lands (`wait_listed(baseline, _MCP_CONNECT_TIMEOUT_SEC)` at `:1371-1372` times out → raises at `:1375`). **Wedges.**
Mid-turn cancellation *is* an intended operation in CLI mode — `_interrupt_active_proc` (`:563`) is documented as *"the only mid-turn cancellation surface"* — but it's only *recoverable* on the spare-backed stateless path. The session-persistent cold-respawn can't re-establish the bridge after a mid-MCP-session SIGINT (the SIGINT appears to leave the streamable-http bridge/session in a state the next subprocess can't connect to).
For contrast, the **API** provider deliberately **coalesces/stacks** a mid-turn message (`providers/anthropic.py:1450`) — no SIGINT, no preempt. So "interrupts don't halt" is by-design *there*; this issue is specifically about CLI **session** mode.
## Open question (for the sagent maintainer)
Is mid-turn halt meant to be **recoverable** in CLI session-persistence mode — i.e., is the cold-respawn-can't-reconnect-the-bridge a **bug** — or is **no-preempt the intended model** there too (like API mode), in which case host code simply shouldn't call `halt()` for a preempt?
- If **bug**: fix is presumably to reset/recover the bridge (or use a spare) on the session-persistent cancel path, mirroring the stateless path.
- If **by-design**: our workaround (disable urgent) is permanent and we drop the mid-turn interrupt feature.
## Workaround (shipped)
- `3e62f0e` — disabled the `urgent` preempt host-side: `urgent=true` now queues like a normal send (still delivered, never preempts). UI / MCP tool / prompt updated to say so.
- `5bacb5d` — separately raised the MCP-connect ceiling 8→25s + earlier compaction. That mitigates the *related* big-tape slow-start variant of the same error, but does **not** fix the halt-wedge.
## Refs
- Workaround commit: `3e62f0e`; tape mitigations: `5bacb5d`.
- sagent pin: `e67b89a` (rekursiv-ai/sagent).
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in providers/anthropic_cli.py at _interrupt_active_proc and the stateless and session-persistent paths around lines 816-826 and 951-960. Reproduce the session-persistent halt, then trace wait_listed and the MCP timeout around lines 1371-1375, comparing the cold respawn with the stateless HotSpare path. Done means the intended halt behavior is established and a subsequent turn no longer wedges the MCP bridge or requires a server restart.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend, cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100