gemini-cli - ✅(Solved) Fix Feature Request: stream_output flag for run_shell_command (push background stdout to conversation) [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
google-gemini/gemini-cli#25803Fetched 2026-04-23 07:45:03
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×2labeled ×2

Error Message

  • Logs: tail -f app.log | grep ERROR — alert on errors in real-time

Fix Action

Fix / Workaround

This creates a "Blind Gap": if a background watcher detects a file change, a build fails, or an inter-agent message arrives, the AI remains unaware until the user happens to interact. The only workaround is blocking the CLI with a synchronous command (freezing user interaction) or relying on the user to mediate.

PR fix notes

PR #25825: feat(shell): stream_output flag for run_shell_command (PR 1/2)

Description (problem / solution / changelog)

Summary

Introduces a stream_output: true flag on run_shell_command that, when combined with is_background: true, forwards each stdout line from the background process to the ACP client in real time as tool_call_update events. Closes the "blind gap" where the model has no visibility into a backgrounded process until the user sends the next prompt.

This is PR 1 of a planned 2-PR rollout. PR 1 ships in-turn streaming. PR 2 (follow-up branch pmenic:feat/stream-output-async, already pushed and end-to-end verified) detaches the stream from the spawning turn so events keep flowing across turn boundaries — the foundation for ConsultaSkill-style auto-reactive workflows via a small client-side sidecar (no upstream ACP protocol extension needed).

Details

Five pieces, each isolated in its own commit so reviews can be incremental:

  1. LineBuffer utility (packages/core/src/utils/lineBuffer.ts) — splits \r?\n, preserves lone \r (progress-bar redraw is not a line terminator), 64 KiB per-line cap with a discard-to-next-newline recovery path.
  2. Parameter schema — new SHELL_PARAM_STREAM_OUTPUT constant; new boolean property in getShellDeclaration().
  3. ACP updateOutput bridge (acpClient.ts:runTool) — previously invocation.execute({ abortSignal }) dropped any incremental output. Now passes an updateOutput callback that emits tool_call_update(in_progress) with the current callId. Side effect: the shell tool's existing foreground progress callbacks (binary detection, periodic throttled progress) are now forwarded to the ACP client too.
  4. ToolResult.backgroundedStreamId — when the shell tool takes its early-return background branch AND stream_output is true, it sets this field to the child pid. The ACP wrapper, on seeing it, emits tool_call_update(in_progress) instead of the terminal completed and registers two completion paths — onBackgroundComplete (real process exit) and abortSignal (turn cancel); first wins — so subsequent streamed lines remain valid against the same toolCallId and never race past a terminal status.
  5. Subscribe + debounce (shell.ts inside the is_background block) — registers an ExecutionLifecycleService.subscribe(pid, …) listener before the BACKGROUND_DELAY_MS timeout so no chunks are lost, pipes chunks through LineBuffer, coalesces completed lines on a 200 ms batch window (matches Claude Code's Monitor reference), and force-flushes on abort / exit.

Coexistence with existing tools

read_background_output continues to read the full output from the disk log; our live listener is a parallel observer of ExecutionLifecycleService and does not consume, intercept, or close the existing WriteStream. An integration test locks this in (see below).

What's NOT in this PR (deferred to PR 2)

  • Events flowing across turn boundaries: the PR 1 stream is scoped to the spawning turn — a new prompt (or user cancel) would tear it down. PR 2 removes that abort wiring and tags each streamed update with _meta['gemini-cli/stream_output'] so sidecars can filter — already pushed at pmenic:feat/stream-output-async and end-to-end verified through Zed with a 60-second watcher that survived two prompt boundaries (30 TICKs, all referencing the original toolCallId, _meta present on every event). Will be opened as a separate PR after this one merges to avoid a confusing diff.
  • Forwarding of PTY-mode AnsiOutput chunks. The current LineBuffer path intentionally skips non-string chunks; plain-text pipe workaround (| grep --line-buffered …) is documented in docs/tools/shell.md. On Windows the shell tool additionally forces shouldUseNodePty: false when is_background + stream_output are both set, so the default interactive-shell setting doesn't silently disable streaming.

Related Issues

Related to #25803.

Does not fully close the issue — the ConsultaSkill use case (idle agent reacting to file-watcher events) needs PR 2 plus a client-side FIFO sidecar (documented in docs/tools/shell.md). PR 1 enables the CI-log / long-build / live-progress cases and lands the entire infrastructure PR 2 builds on.

How to Validate

Unit + integration tests (all green on this branch):

npx vitest run packages/core/src/utils/lineBuffer.test.ts
npx vitest run packages/core/src/tools/shell.test.ts
npx vitest run packages/cli/src/acp/acpClient.test.ts
npx vitest run packages/core/src/tools/shellBackgroundTools.integration.test.ts

Visual end-to-end demo (included in the PR):

npx tsx scripts/demo-stream-output.ts

Expected output — five live: lineN messages arriving ~1 s apart as the real child process emits them, followed by the exit event:

[00.0s] demo: subscribed to pid <n>
[00.2s] live: line1
[01.2s] live: line2
[02.2s] live: line3
[03.3s] live: line4
[04.3s] live: line5
[05.3s] demo: process exited, unsubscribed

End-to-end validated via Zed (2026-04-22): ran a 5-line + 30-line (PR 2) bash command; every stdout line arrived at the client as tool_call_update(status: in_progress, toolCallId: <original>, content: [...]) in real time, terminal completed landed after the last line.

Manual regression — invoke run_shell_command without stream_output and confirm the existing Command moved to background (PID: X) return and read_background_output behavior is byte-identical.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed) — docs/tools/shell.md
  • Added/updated tests (if needed) — +31 tests (19 LineBuffer unit, 6 shell stream_output unit, 3 ACP updateOutput / backgroundedStreamId, 1 integration, 2 abort/exit teardown)
  • Noted breaking changes (if any) — none; the flag is optional and default-off, all existing paths unchanged
  • Validated on required platforms/methods:
    • MacOS (not run locally)
    • Windows — npm run development env, full test suite + demo script + end-to-end Zed test
    • Linux (not run locally; trusted to CI)

Changed files

  • docs/tools/shell.md (modified, +53/-0)
  • packages/cli/src/acp/acpClient.test.ts (modified, +196/-0)
  • packages/cli/src/acp/acpClient.ts (modified, +101/-9)
  • packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap (modified, +8/-0)
  • packages/core/src/tools/definitions/base-declarations.ts (modified, +1/-0)
  • packages/core/src/tools/definitions/dynamic-declaration-helpers.ts (modified, +6/-0)
  • packages/core/src/tools/shell.test.ts (modified, +296/-0)
  • packages/core/src/tools/shell.ts (modified, +96/-1)
  • packages/core/src/tools/shellBackgroundTools.integration.test.ts (modified, +88/-0)
  • packages/core/src/tools/tools.ts (modified, +13/-0)
  • packages/core/src/utils/lineBuffer.test.ts (added, +166/-0)
  • packages/core/src/utils/lineBuffer.ts (added, +133/-0)
  • scripts/demo-stream-output.ts (added, +133/-0)

PR #25834: feat(shell): stream_output out-of-turn (PR 2/2) — stacked on #25825

Description (problem / solution / changelog)

Summary

Follow-up to #25825 (PR 1). Makes stream_output: true events keep flowing after the spawning turn ends, so a client-side sidecar can react to background file-watcher events even when no user prompt is in flight — the ConsultaSkill use case on top of issue #25803.

⚠️ Stacked on top of #25825. The diff visible here includes all of PR 1's commits plus 4 new ones. Review only the commits listed in "Details" below, or wait until #25825 merges and I'll rebase this onto main for a clean diff.

Details

Four new commits on top of #25825 (branch pmenic:feat/stream-output-async):

  1. feat(stream_output): detach stream from turn abort — survive turn boundaries — removes the turn-scoped abortSignal teardown listeners from both shell.ts (LineBuffer-driven forwarder) and acpClient.ts (terminal-status emitter). Teardown now happens exclusively on the process's own exit event. A new user prompt (which aborts the previous pendingSend) no longer prematurely closes a stream whose process is still producing output the model may want to react to later.

  2. feat(stream_output): tag tool_call_update with _meta marker — extends ExecuteOptions.updateOutput with an optional second _meta arg that the ACP layer forwards verbatim to tool_call_update._meta. The shell tool's stream_output path tags every streamed batch with { 'gemini-cli/stream_output': true, pid: <number> } so sidecars can filter the ACP wire precisely for stream events (vs binary-detection notices, periodic throttled progress, etc.) without parsing text or relying on tool-name heuristics. ACP spec explicitly reserves _meta for exactly this (types.gen.d.ts: "MUST NOT make assumptions about values at these keys").

  3. docs(shell): update stream_output docs for PR 2 out-of-turn semantics — documents the new lifecycle (_meta marker, streams outlive spawning turn, terminal emit only on real process exit) and adds a "Reacting automatically while the agent is idle" section explaining the ACP protocol limit (agents cannot initiate turns) and the recommended client-side FIFO-queue sidecar pattern. No upstream ACP extension required.

  4. test(cli/acp): PR 2 regression — no terminal emit on turn abort — locks in the core PR 2 contract: a tool that returns backgroundedStreamId must not get a terminal tool_call_update on turn-abort. Test snapshots emitted updates, yields a macrotask, and asserts zero new updates arrived.

Why no new ACP protocol work

Research against the ACP SDK and Zed's AcpThread implementation:

  • AgentSideConnection.sessionUpdate() is documented as a notification endpoint for "real-time updates about session progress" (acp.d.ts:33-45) and the SDK imposes no runtime constraint requiring an active prompt().
  • Zed's AcpThread dispatches SessionUpdate events asynchronously through its channel-based event stream even outside active turns.
  • What ACP cannot do: agents cannot initiate a prompt turn. The protocol is strictly client-driven. Closing the "agent reacts while idle" loop therefore has to live on the client side (e.g. in a sidecar process that sits between Zed and gemini-cli, observes the wire, filters on _meta['gemini-cli/stream_output'], buffers events in a FIFO queue while the agent is busy, and flushes them as a consolidated session/prompt when idle).

This PR is the agent-side half — the client-side sidecar lives outside gemini-cli (e.g. in pmenic/ConsultaSkill).

Related Issues

Closes part of #25803 (together with #25825 this is the full plumbing contribution from gemini-cli).

How to Validate

End-to-end verified via Zed 0.233.5 on Windows (2026-04-23):

  1. Launched a PowerShell watcher emitting TICK:1..30 every 2 seconds (~60 seconds total) with stream_output: true in turn 1.
  2. Model replied launched and ended turn 1 (per explicit prompt instruction "do not read output or call any other tools").
  3. User sent a second prompt ("Tell me a haiku") while the process was still emitting.
  4. Wiretap log (scripts/acp-wiretap.js tapping the stdio between Zed and the local bundle) showed:
    • session/prompt id=2 at line 23 (watcher launch)
    • 30× tool_call_update(status: in_progress) with content[0].content.text = TICK:N and _meta: { "gemini-cli/stream_output": true, "pid": 23748 }
    • session/prompt id=3 at line 62 (haiku) — TICK:3..TICK:9 were emitted AFTER this prompt arrived, referencing the ORIGINAL toolCallId
    • session/prompt id=4 at line 92 (haiku re-sent) — TICK:10..TICK:30 continued emitting during this third turn
    • Every event carried the _meta marker.

Plus all unit tests green:

npx vitest run packages/core/src/utils/lineBuffer.test.ts
npx vitest run packages/core/src/tools/shell.test.ts
npx vitest run packages/cli/src/acp/acpClient.test.ts
npx vitest run packages/core/src/tools/shellBackgroundTools.integration.test.ts

Pre-Merge Checklist

  • Updated relevant documentation — docs/tools/shell.md
  • Added/updated tests — 1 unit test adjusted for new PR 2 semantics + 1 new regression test (acpClient) + 1 new test (updateOutput _meta forwarding)
  • Noted breaking changes — none; stream_output is opt-in, existing behavior unchanged when absent or when is_background: false
  • Validated on required platforms:
    • MacOS (not run locally)
    • Windows — npm run + end-to-end Zed 30-TICK cross-turn test
    • Linux (trusted to CI)

Changed files

  • docs/tools/shell.md (modified, +73/-0)
  • packages/cli/src/acp/acpClient.test.ts (modified, +308/-0)
  • packages/cli/src/acp/acpClient.ts (modified, +101/-9)
  • packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap (modified, +8/-0)
  • packages/core/src/tools/definitions/base-declarations.ts (modified, +1/-0)
  • packages/core/src/tools/definitions/dynamic-declaration-helpers.ts (modified, +6/-0)
  • packages/core/src/tools/shell.test.ts (modified, +321/-0)
  • packages/core/src/tools/shell.ts (modified, +103/-1)
  • packages/core/src/tools/shellBackgroundTools.integration.test.ts (modified, +88/-0)
  • packages/core/src/tools/tools.ts (modified, +23/-1)
  • packages/core/src/utils/lineBuffer.test.ts (added, +166/-0)
  • packages/core/src/utils/lineBuffer.ts (added, +133/-0)
  • scripts/demo-stream-output.ts (added, +133/-0)

Code Example

run_shell_command({
  command: "bash watcher.sh /inbox/ | grep --line-buffered '^NEW:'",
  is_background: true,
  stream_output: true  // each stdout line becomes a conversation event
})
RAW_BUFFERClick to expand / collapse

Problem

When Gemini CLI runs a background process (is_background: true), the AI is blind to its output until the user sends a new prompt. There is no way for the AI to react to background events in real-time.

This creates a "Blind Gap": if a background watcher detects a file change, a build fails, or an inter-agent message arrives, the AI remains unaware until the user happens to interact. The only workaround is blocking the CLI with a synchronous command (freezing user interaction) or relying on the user to mediate.

Real-World Impact

We built ConsultaSkill — a system where Claude Code and Gemini CLI collaborate on the same project through file-based messages. A watcher script monitors an inbox and outputs NEW:filename when a message arrives.

  • Claude Code has a Monitor tool that pushes each stdout line as a conversation event. The AI reacts instantly — zero polling, zero user intervention.
  • Gemini CLI must either block (wait-for-message.sh synchronous) or miss messages until the user triggers read_background_output.

Over 18 test sessions, Gemini required user intervention for every incoming message. Claude processed them automatically.

Proposed Solution

Add a stream_output flag to run_shell_command:

run_shell_command({
  command: "bash watcher.sh /inbox/ | grep --line-buffered '^NEW:'",
  is_background: true,
  stream_output: true  // each stdout line becomes a conversation event
})

When stream_output: true:

  • Each stdout line is injected as an asynchronous event in the conversation
  • The AI can react without waiting for user input
  • Filtering is handled in the shell pipe (standard Unix patterns)
  • The process runs until it exits, times out, or is explicitly stopped

This is the minimal change: no new tool, no new protocol — just a flag on an existing tool.

Use Cases Beyond Inter-Agent Communication

  • CI/CD: gh run watch — react when a build fails
  • Logs: tail -f app.log | grep ERROR — alert on errors in real-time
  • Deploys: watch deployment progress, react on completion
  • File watching: detect new files, modifications
  • Long tasks: training runs, migrations, data imports

Related Issues

  • #5941 — Background task processing (this solves "can't react to completion")
  • #9070 — Hooking system (complementary, different granularity)
  • #15338 — Daemon mode (would implicitly enable this)
  • #14596 — AfterAgent hook fires every turn, not per-event

Architecture Note

The existing ACP sessionUpdate notification could carry process_output events from a Stream Forwarder attached to the background process stdout. The key change is supporting an Asynchronous Turn in the Orchestration Engine — a turn triggered by an external event, not by user input.

Reference

Claude Code's Monitor tool implements this pattern since 2025: each stdout line is a conversation notification, with 200ms batching, persistent mode, and timeout support.


Discovered while building ConsultaSkill — an inter-agent communication protocol between Claude Code and Gemini CLI.

extent analysis

TL;DR

Adding a stream_output flag to run_shell_command allows Gemini CLI to react to background events in real-time, eliminating the "Blind Gap".

Guidance

  • Set stream_output: true when running background processes to enable real-time event handling.
  • Use the stream_output flag with run_shell_command to inject stdout lines as asynchronous events in the conversation.
  • Filter events using standard Unix patterns in the shell pipe to minimize changes to the existing codebase.
  • Test the stream_output flag with various use cases, such as CI/CD, log monitoring, and file watching, to ensure compatibility and effectiveness.

Example

run_shell_command({
  command: "bash watcher.sh /inbox/ | grep --line-buffered '^NEW:'",
  is_background: true,
  stream_output: true
})

Notes

The proposed solution builds upon the existing run_shell_command function, minimizing the need for new tools or protocols. However, the implementation details of the stream_output flag and its integration with the Orchestration Engine are not provided.

Recommendation

Apply the workaround by adding the stream_output flag to run_shell_command to enable real-time event handling, as this change is minimal and does not require significant modifications to the existing codebase.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

gemini-cli - ✅(Solved) Fix Feature Request: stream_output flag for run_shell_command (push background stdout to conversation) [2 pull requests, 1 participants]