Skip to content

ACP: turns started by a background task outcome emit no state_update (no running, no idle) #557

Closed

Description

@MartinSchlott

Summary

When a background task (e.g. bash with runInBackground: true) completes while the session is idle, the core correctly starts a new turn and the model answers unprompted. Over ACP, however, that turn has no busy cycle: no state_update {"state":"running"} before it and no state_update {"state":"idle"} after it. An ACP client sees assistant content arrive in an idle session and has no terminator telling it when that turn is over.

Environment

  • aether 0.9.10 (Homebrew), aether acp, ACP v2
  • macOS (Apple Silicon)
  • Model: deepseek:deepseek-v4-pro, mode Build from the batteries-included preset
  • Raw JSON-RPC client (no SDK), every frame logged with a timestamp

Reproduction

  1. initialize → session/new → session/set_config_option (mode = Build)
  2. session/prompt: "Use the bash tool with run_in_background=true to run exactly: sleep 25; echo LATE_TASK_DONE. Do not wait for it. Immediately end your turn by replying only: started"
  3. Keep observing for 90 s after the turn's idle.

Observed frames (seconds since start):

 3.543  agent_message "started"
 3.543  state_update {"state":"idle","stopReason":"end_turn"}    <- turn 0 ends, task still running
 4.196  tool_call_update {status: in_progress}                    (1 Hz heartbeat while idle)
27.245  tool_call_update {status: completed, "LATE_TASK_DONE ..."}
28.148  agent_message_chunk "LATE_TASK_DONE"
28.174  agent_message "LATE_TASK_DONE"
28.174  _aether/session_usage {agent_name: "Build", ...}          <- an LLM call ran
        (no state_update for the rest of the 90 s window)

Expected

The task-initiated turn is framed like any other turn:

27.245  tool_call_update {status: completed}
        state_update {"state":"running"}
28.174  agent_message "LATE_TASK_DONE"
        state_update {"state":"idle","stopReason":"end_turn"}

Second consequence: a prompt in that window absorbs the unprompted reply

Sending session/prompt at the moment the task's completed frame arrives (sleep 15 variant):

17.368  tool_call_update {status: completed}
17.368  -> session/prompt "Reply only with: second"
17.368  <- {"messageId": ...}                  accepted, because the actor is still Idle
17.369  state_update {"state":"running"}
18.560  agent_message "LATE_TASK_DONE"         <- the task-initiated reply ...
19.653  agent_message "second"                 <- ... lands in the prompted turn
19.653  state_update {"state":"idle","stopReason":"end_turn"}

The client can't tell that LATE_TASK_DONE was not a response to its prompt. The window is the full duration of the follow-up LLM call, not a narrow race. With the fix below, the session would visibly be running, and the prompt would get the same -32600 that any prompt during a running turn already gets, which a client can handle.

Cause

Suggested fix

In the runtime.agent_rx branch of the actor loop, when the actor is Idle and the event is AgentEvent::Turn(TurnEvent::Started { .. }), switch to TurnState::Running and send state_update running. The existing turn_outcome() check then emits the matching idle through finish_turn. Started events for prompted turns arrive while the actor is already Running, so they would not be affected.

One edge case to decide on: a task outcome that starts a turn while the actor is in Preparing for an incoming prompt.

I'm happy to send a PR for this if you agree with the approach.

Context

I maintain a Rust crate that wraps several coding-agent CLIs over ACP behind one interface, and I'm evaluating Aether for it. Following up on background work is exactly what other harnesses get wrong. Aether's core already does it right, and this issue is only about the ACP framing.

Activity

  1. jcarver989 commented on Oct 2, 2026

    @jcarver989
    Contributor

    Hey @MartinSchlott thanks for the bug report!

    I think this one should be a pretty easy fix, and should be able to get it into the next Aether release for you (likely on or before Monday).

  2. jcarver989 commented on Oct 3, 2026

    @jcarver989
    Contributor

    #559 Implements a fix for this and will go out with the next Aether release.

    One important thing to note is that Aether's behavior within the core crate has long been to queue incoming prompts while the agent is busy and insert them into context at the next available opportunity. However, this was not exposed via ACP, or available in the TUI ACP client (wisp) that ships with aether-cli -- now it is.

    So for your ACP client you'll have a choice of disallowing sending prompts while the agent is busy, or sending them through. In the event you send them through, they'll be queued

  3. MartinSchlott commented on Oct 3, 2026

    @MartinSchlott
    Author

    Hey @MartinSchlott thanks for the bug report!

    I think this one should be a pretty easy fix, and should be able to get it into the next Aether release for you (likely on or before Monday).

    Thx for the swift response. BTW Your project was in a recommendation of ChatGPT regarding OpenCode alternatives in Rust. Congratulations, it is really a cool project.

  4. jcarver989 commented on Oct 3, 2026

    @jcarver989
    Contributor

    Thx for the swift response. BTW Your project was in a recommendation of ChatGPT regarding OpenCode alternatives in Rust. Congratulations, it is really a cool project.

    Thanks! FYI this fix is included in this release: https://github.com/contextbridge/aether/releases/tag/aether-agent-cli-v0.9.11

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions