Skip to content

fix(cli): user-initiated abort no longer kills the ACP runner process - #1678

Open
FelixShowX wants to merge 1 commit into
slopus:mainfrom
FelixShowX:fix/acp-abort-kills-runner
Open

fix(cli): user-initiated abort no longer kills the ACP runner process#1678
FelixShowX wants to merge 1 commit into
slopus:mainfrom
FelixShowX:fix/acp-abort-kills-runner

Conversation

@FelixShowX

Copy link
Copy Markdown

Problem

When the user taps "stop" in the mobile app during an ACP agent turn (gemini / opencode / any generic ACP backend), the CLI process crashes and the whole session dies, instead of just cancelling the in-flight turn.

Root cause

AcpBackend.cancel() emits status: 'stopped' after a successful session/cancel:

// AcpBackend.ts
async cancel(sessionId: SessionId): Promise<void> {
  ...
  await this.connection.cancel({ sessionId: this.acpSessionId });
  this.emit({ type: 'status', status: 'stopped', detail: 'Cancelled by user' });
}

But runAcp treats any stopped status as fatal backend death:

if (msg.status === 'error' || msg.status === 'stopped') {
  stopRunnerFromBackendStatus(msg.status, msg.detail); // → shouldExit + reject pending turn → process exits
}

So a perfectly healthy user abort is indistinguishable from the backend dying, and the runner shuts the session down. The turn IS cancelled correctly on the agent side — the runner just kills itself right after.

Fix

Track user-initiated aborts and treat the resulting stopped as a normal (cancelled) turn end:

  • handleAbort() sets userAbortInFlight = true before calling backend.cancel()
  • When a stopped status arrives with that flag set: reset thinking/keepAlive, resolve the pending turn (instead of rejecting it), and mark the turn cancelled — the runner stays alive and the next prompt works
  • The flag is cleared at every new turn start, and also if cancel() throws (the abort never reached the agent). An abort that arrives while the session is idle — e.g. network latency delivers it right after the turn ended naturally — would otherwise leave the flag set, and a later genuine backend failure would be misread as a user abort, leaving the runner alive with a dead backend. Bounding the flag's effect to its own turn prevents that
  • turn-end is reported as 'cancelled' instead of 'completed'

Repro

  1. happy acp <agent> (any ACP agent)
  2. From the phone app, send a long-running prompt (e.g. "count from 1 to 1000, one per line")
  3. While it streams, tap stop
  4. Before: CLI process exits with Error: <agent> backend stopped: Cancelled by user. After: turn ends as cancelled, session stays online, next prompt works.

Notes

  • No behavior change for genuine backend failures: a stopped status without a preceding user abort (same-turn) still shuts the runner down
  • AcpSessionManager.endTurn already accepts 'cancelled' — no other files touched
  • Verified against kimi-code 0.34.0 via the ACP protocol; patch compiles and passes the full upstream unit suite (792/792) on a clean upstream/main checkout

AcpBackend.cancel() emits status 'stopped' after a successful
session/cancel, which runAcp treated as fatal backend death — so a
user tapping stop in the app killed the whole session process instead
of just cancelling the turn.

Track user-initiated aborts (userAbortInFlight) and treat the matching
stopped status as a normal turn end (marked 'cancelled'). The flag is
cleared at every new turn start and when cancel() throws, so a stale
flag can never mask a genuine backend failure.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant