diff --git a/crates/pi-natives/src/shell.rs b/crates/pi-natives/src/shell.rs index ae0e34de0..3a9e175d2 100644 --- a/crates/pi-natives/src/shell.rs +++ b/crates/pi-natives/src/shell.rs @@ -38,7 +38,11 @@ use clap::Parser; use napi::{ bindgen_prelude::*, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - tokio::{self, sync::Mutex as TokioMutex, time}, + tokio::{ + self, + sync::{Mutex as TokioMutex, mpsc}, + time, + }, }; use napi_derive::napi; use tokio::io::AsyncReadExt as _; @@ -550,10 +554,11 @@ async fn run_shell_command( } let reader_cancel = CancellationToken::new(); + let (activity_tx, mut activity_rx) = mpsc::channel::<()>(1); let mut reader_handle = tokio::spawn({ let reader_cancel = reader_cancel.clone(); async move { - read_output(reader_file, on_chunk, reader_cancel).await; + read_output(reader_file, on_chunk, reader_cancel, activity_tx).await; Result::<()>::Ok(()) } }); @@ -585,12 +590,34 @@ async fn run_shell_command( drop(params); // The foreground command can complete while background jobs keep the - // stdout/stderr pipe open. Don't hang forever waiting for EOF; drain briefly, - // then cancel. - if time::timeout(Duration::from_millis(200), &mut reader_handle) - .await - .is_err() - { + // stdout/stderr pipe open. Don't hang forever waiting for EOF; drain output + // for a short period, then cancel. + const POST_EXIT_IDLE: Duration = Duration::from_millis(250); + const POST_EXIT_MAX: Duration = Duration::from_secs(2); + + let mut reader_finished = false; + let mut idle_timer = Box::pin(time::sleep(POST_EXIT_IDLE)); + let mut max_timer = Box::pin(time::sleep(POST_EXIT_MAX)); + + loop { + tokio::select! { + res = &mut reader_handle => { + let _ = res; + reader_finished = true; + break; + } + msg = activity_rx.recv() => { + if msg.is_none() { + break; + } + idle_timer.as_mut().reset(time::Instant::now() + POST_EXIT_IDLE); + } + () = &mut idle_timer => break, + () = &mut max_timer => break, + } + } + + if !reader_finished { reader_cancel.cancel(); let _ = reader_handle.await; } @@ -730,6 +757,7 @@ async fn read_output( reader: fs::File, on_chunk: Option>, cancel_token: CancellationToken, + activity: mpsc::Sender<()>, ) { const REPLACEMENT: &str = "\u{FFFD}"; const BUF: usize = 4096; @@ -751,6 +779,9 @@ async fn read_output( Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, Err(_) => break, }; + if n > 0 { + let _ = activity.try_send(()); + } it += n; // Consume as much of `pending` as is decodable *right now*. diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4e6264e09..a5a9640e4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] + ### Added - Added current date to system prompt context in YYYY-MM-DD format for date-aware agent reasoning @@ -9,6 +10,7 @@ ### Changed +- Changed skipped file messages to include file size information for better visibility into why files were excluded - Changed file processing to skip reading files exceeding 5MB (text) or 25MB (images) and include them as path-only references instead - Changed @mention auto-reading to skip files exceeding 5MB (text) or 25MB (images) to prevent out-of-memory issues with large files - Clarified that subagents automatically inherit full system prompt including AGENTS.md, context files, and skills — do not repeat project rules or conventions in task context @@ -19,8 +21,9 @@ ### Fixed - Fixed bash tool hanging when commands spawn background jobs by properly detecting foreground process completion -- Fixed crash when auto-reading @mentions for very large files by skipping content injection and referencing the path only - Fixed bash tool occasionally hanging after command completion when background jobs keep stdout/stderr open +- Fixed crash when auto-reading @mentions for very large files by skipping content injection with an explicit "skipped" note +- Improved bash tool output draining after foreground completion to reduce tail output truncation ## [11.8.0] - 2026-02-10 ### Added diff --git a/packages/coding-agent/src/cli/file-processor.ts b/packages/coding-agent/src/cli/file-processor.ts index eacd9b330..62d7e861b 100644 --- a/packages/coding-agent/src/cli/file-processor.ts +++ b/packages/coding-agent/src/cli/file-processor.ts @@ -7,13 +7,13 @@ import type { ImageContent } from "@oh-my-pi/pi-ai"; import { isEnoent } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { resolveReadPath } from "../tools/path-utils"; -import { DEFAULT_MAX_BYTES, formatSize } from "../tools/truncate"; +import { formatSize } from "../tools/truncate"; import { formatDimensionNote, resizeImage } from "../utils/image-resize"; import { detectSupportedImageMimeTypeFromFile } from "../utils/mime"; // Keep CLI startup responsive and avoid OOM when users pass huge files. // If a file exceeds these limits, we include it as a path-only block. -const MAX_CLI_TEXT_BYTES = DEFAULT_MAX_BYTES * 100; // 5MB +const MAX_CLI_TEXT_BYTES = 5 * 1024 * 1024; // 5MB const MAX_CLI_IMAGE_BYTES = 25 * 1024 * 1024; // 25MB export interface ProcessedFiles { @@ -53,7 +53,7 @@ export async function processFileArguments(fileArgs: string[], options?: Process console.error( chalk.yellow(`Warning: Skipping file contents (too large: ${formatSize(stat.size)}): ${absolutePath}`), ); - text += `\n`; + text += `(skipped: too large, ${formatSize(stat.size)})\n`; continue; } diff --git a/packages/coding-agent/src/utils/file-mentions.ts b/packages/coding-agent/src/utils/file-mentions.ts index 106c4866d..88cf507d7 100644 --- a/packages/coding-agent/src/utils/file-mentions.ts +++ b/packages/coding-agent/src/utils/file-mentions.ts @@ -24,7 +24,7 @@ const DEFAULT_DIR_LIMIT = 500; // Avoid OOM when users @mention very large files. Above these limits we skip // auto-reading and only include the path in the message. -const MAX_AUTO_READ_TEXT_BYTES = DEFAULT_MAX_BYTES * 100; // 5MB +const MAX_AUTO_READ_TEXT_BYTES = 5 * 1024 * 1024; // 5MB const MAX_AUTO_READ_IMAGE_BYTES = 25 * 1024 * 1024; // 25MB function isMentionBoundary(text: string, index: number): boolean { @@ -191,7 +191,7 @@ export async function generateFileMentionMessages( if (stat.size > MAX_AUTO_READ_IMAGE_BYTES) { files.push({ path: filePath, - content: "", + content: `(skipped auto-read: too large, ${formatSize(stat.size)})`, byteSize: stat.size, skippedReason: "tooLarge", }); @@ -227,7 +227,7 @@ export async function generateFileMentionMessages( if (stat.size > MAX_AUTO_READ_TEXT_BYTES) { files.push({ path: filePath, - content: "", + content: `(skipped auto-read: too large, ${formatSize(stat.size)})`, byteSize: stat.size, skippedReason: "tooLarge", });