Skip to content

Machine-generated messages detach a foreground wait:true bash and abort sync bash_watch #366

Closed as not planned

Description

@iceteaSA

Problem

A message the operator did not type, such as a subagent's completion delivered to its parent session, detaches a running foreground bash with wait:true and aborts a sync bash_watch. The tool description promises "A new user message detaches this wait" (packages/opencode-plugin/src/tools/bash.ts:170), but in practice any message detaches it, including one whose text parts are all synthetic.

Cause (at f00d270)

In the chat.message hook, packages/opencode-plugin/src/index.ts:

  • :925 calls signalSyncWatchAbort(sid) for every message, before any check of the config or the message.
  • :931-936 detaches whenever shouldDetachBashWaitOnUserMessage is true. That function (bash-wait-detach.ts:73) returns detach_on_user_message || containsStandaloneDetachKeyword(text). detach_on_user_message defaults to true (config.ts:980, crates/aft/src/config.rs:50-52), and the text isn't consulted on that branch.

extractUserMessageText already drops synthetic and ignored parts (bash-wait-detach.ts:25-46), so for a machine-generated message it returns "". But nothing checks for that before detaching.

Impact

This has been reproduced with a subagent completion arriving while the parent's wait:true bash was running: the wait detached. In our install it let a scheduled module restart run twice: the restart script's foreground wait was detached by an incoming non-operator message, a second copy of the script then started, and the two restarts landed 41 ms apart.

Suggested fix

Only operator input should detach or abort. Detach when the extracted non-synthetic text is non-empty, or when it contains &detach as today, and gate signalSyncWatchAbort on the same condition. Messages that some other component injects as plain, unmarked text would still detach; marking those synthetic is up to whoever injects them.

Activity

  1. aft-alfonso commented on Sep 26, 2026

    @aft-alfonso

    Thanks for the detailed report. I checked it against the code, and it's accurate. In the chat.message hook, the sync bash_watch abort fires for every message before anything is checked. With detach_on_user_message on (the default), the wait:true detach doesn't look at the message text at all. So a message made only of synthetic parts, such as a subagent completion or AFT's own background-completion notice, detaches the wait just like typed input.

    We're going with the fix you suggested. Only operator input will detach a wait or abort a watch: non-synthetic text, or the &detach token as today. One check will decide both, so the watch abort will also respect detach_on_user_message: false, which it currently ignores. We're applying the same rule to the OpenCode 2 and Pi message paths. It should ship in 0.58.0.

  2. aft-alfonso commented on Sep 26, 2026

    @aft-alfonso

    A correction to my earlier reply: we're not making this change. Machine-generated messages, such as a subagent's completion, detaching a waiting bash is intended. It lets the agent act on the result instead of sitting out the rest of a long wait. The duplicate restart comes from the script being re-run after the detach, not from the detach itself. A wait that must not be interrupted can turn off bash.detach_on_user_message.

  3. iceteaSA commented on Sep 26, 2026

    @iceteaSA
    ContributorAuthor

    Understood, and fair on the restart: the duplicate came from the script being re-run after the detach, not from the detach itself.

    One gap remains in the workaround, and it's the half your first reply called out. Turning off bash.detach_on_user_message keeps a wait:true bash blocking, but it doesn't protect a sync bash_watch. index.ts:925 calls signalSyncWatchAbort(sid) for every chat.message, before the config is loaded at :931, and signalSyncWatchAbort (sync-watch-abort.ts:22-24) sets the abort flag without checking the setting. So with the setting off, any message, whether typed or machine-generated, still turns a sync watch async. That's at 48c3790.

    Since machine messages detaching is intended, the bash description (tools/bash.ts:170, "A new user message detaches this wait") may also want to say "any new message".

  4. aft-alfonso commented on Sep 26, 2026

    @aft-alfonso

    Thanks, that's the gap we've fixed: a waiting bash_watch now follows bash.detach_on_user_message the same way a wait:true bash does, so with the setting off only &detach interrupts either one. We're also changing the description to say any new message detaches the wait. Both ship in 0.58.0.

  5. aft-alfonso commented on Oct 1, 2026

    @aft-alfonso

    We went the other way on this one: any new message, including a machine-generated completion notice, now detaches a waiting bash or bash_watch, so the agent can act on the result right away instead of idling until the wait ends. That shipped in v0.58.0 (bcda39d and 6dc637e). If you prefer waits to keep blocking, set bash.detach_on_user_message: false. Closing; thanks for the detailed report.

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