Skip to content

claude-code-cli: async_bash followUp notifications ingested as user prompts, triggering autonomous turns in interactive mode #3168

@curtiswang96

Description

@curtiswang96

Problem

In interactive mode with the claude-code-cli provider, async_bash job completion notifications delivered via deliverAs:"followUp" are treated as the user's next instruction. The agent autonomously acts on the job output without any user prompt — in this case, immediately launching into code fixes the user had not yet approved, and visually pushing the prior response out of the window.

Root Cause

Two defects combine to produce the failure:

Defect 1 — extractLastUserPrompt in stream-adapter.ts ingests followUp messages as user prompts

File: src/resources/extensions/claude-code-cli/stream-adapter.ts (~line 80, extractLastUserPrompt)

When pi.sendMessage({deliverAs:"followUp"}) appends an async_job_result to pi's context, it lands as a user-role message. extractLastUserPrompt iterates backwards through context.messages and returns the first role:"user" message — which is now the job result. That string ("**Background job done: bg_e7c23baa**…\n\n=== live play ===\n\"started\"\n…") becomes the Claude Code SDK's prompt. The SDK runs a new full turn, and the agent acts on the job output as if the user requested it.

// Current — no guard for followUp/custom messages
function extractLastUserPrompt(context: Context): string {
    for (let i = context.messages.length - 1; i >= 0; i--) {
        const msg = context.messages[i];
        if (msg.role === "user") {
            // Returns job result content as the SDK prompt ← BUG
            if (typeof msg.content === "string") return msg.content;
            ...
        }
    }
    return "";
}

Fix: Skip messages with customType === "async_job_result" (or any message flagged as a followUp) when walking backwards:

function extractLastUserPrompt(context: Context): string {
    for (let i = context.messages.length - 1; i >= 0; i--) {
        const msg = context.messages[i];
        if (msg.role === "user") {
            // Skip follow-up notifications — informational only, not user instructions
            if ((msg as any).customType === "async_job_result") continue;
            if ((msg as any).isFollowUp === true) continue;
            if (typeof msg.content === "string") return msg.content;
            ...
        }
    }
    return "";
}

Defect 2 — No interactive-mode equivalent of clearQueue() for claude-code-cli sessions

File: src/resources/extensions/gsd/auto/run-unit.ts (lines ~121–133)

Auto-mode calls clearQueue() after each unit completes to discard pending followUp messages. Interactive mode has no such call. With native pi providers this is tolerable because deliverAs:"followUp" does not trigger a new turn when idle. With the claude-code-cli provider the followUp becomes the prompt on the next turn (Defect 1), so there is no safe idle state — every job completion that finishes after the agent's response will cause an autonomous turn.

Expected Behavior

  • async_job_result followUp messages should display in the TUI but never become the Claude Code SDK prompt.
  • The agent should remain idle after delivering a response; job completions should not trigger autonomous follow-up turns in interactive mode.
  • The fix in async_bash job results trigger spurious LLM turns after agent completion #875 (deliverAs:"followUp") should apply correctly across all providers, including claude-code-cli.

Environment

  • GSD version: 2.58.0
  • Provider: claude-code-cli (via @anthropic-ai/claude-agent-sdk)
  • Mode: interactive (non-auto-mode)

Reproduction Context

  1. In an interactive session with the claude-code-cli provider, launch 4+ parallel async_bash jobs.
  2. Agent produces a response (turn ends).
  3. Background jobs complete → onJobComplete fires → pi.sendMessage({deliverAs:"followUp"}) queues results.
  4. FollowUp messages appear in TUI, pushing prior response out of view.
  5. On the next conversation cycle, extractLastUserPrompt returns the last job result string as the user's instruction.
  6. Claude Code SDK runs a new turn treating job output as a user prompt.
  7. Agent acts autonomously on the results (in this case, immediately began implementing code fixes before the user had reviewed the audit findings).

Forensic Evidence

  • 4 async_bash jobs started during an audit (bg_fb7d16ae, bg_e7c23baa, bg_d3fe95de, bg_ee6d8891).
  • Agent gave a complete audit response, then job results arrived one by one.
  • Each result (**Background job done: bg_e7c23baa** (test live viewport controls, 1.5s)\n\n=== live play ===\n"started"\n…) appeared as a new user-turn prompt.
  • Agent acted on results without user instruction, launching into code fixes.

Prior Art


Auto-generated by /gsd forensics

Metadata

Metadata

Assignees

No one assigned

    Labels

    High PrioritybugSomething isn't workingcloses-on-mergeWill close automatically when linked PR mergeshas-conflictsLinked PR has merge conflicts

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions