arboard's Windows reader feeds Qt-style CF_DIBV5 payloads (BI_RGB plus
alpha mask, rewritten to BI_BITFIELDS by its header tweak) to a
header-less BMP decode that mis-places the pixel offset for V4/V5
bitfield headers, so PixPin/Snipaste screenshots failed with
ConversionFailure. read_image_from_clipboard now falls back to reading
the raw CF_DIB clipboard bytes and decoding them through the BMP file
path with an explicit bfOffBits, keeping native Windows image paste off
the PowerShell bridge.
Fixes#3426
The non-PTY bash streaming bridge queued every decoded chunk into
flume::unbounded and fired ThreadsafeFunction callbacks NonBlocking
with no budget, so a producer outrunning the JS event loop grew the
native queue (and the napi queue behind it) without bound — measured
33.5 MB queued for a 32 MiB stream with a stalled consumer, and
multi-GB RSS on longer runs. The downstream OutputSink caps sit after
the N-API boundary and cannot bound either queue.
Bound the pipeline end to end without dropping data:
- pi-natives: bridge_chunks now creates flume::bounded(64) and the
drain task (extracted as pump_chunks) awaits on_chunk.call_async per
coalesced <=64 KiB batch, so at most one batch sits in the napi
queue and the JS event loop's real consumption rate backpressures
the whole pipeline. If the JS side is gone, the pump exits and
drops the receiver so senders fail fast.
- pi-shell: emit_chunk sends with send_async().await — a full bridge
queue parks the pipe reader, which parks the child on its
stdout/stderr pipe (ordinary pipe backpressure) instead of
buffering; a disconnected receiver fails immediately so child pipes
always keep draining.
Unlike a drop-after-cap design, every byte still reaches JS: the
rolling tail view, lossless [raw output: artifact://…] capture, and
totalBytes accounting keep working for outputs past the display cap.
E2E (darwin-arm64 addon): 32 MiB through a JS callback stalling 1 ms
per call — lossless, 472 coalesced callbacks, peak RSS +21.8 MiB.
Fixes#4078
Stop pi-walker from applying .gitignore/.ignore files from unrelated ancestors above an explicit non-repository search root. Preserve repo-root ignore inheritance when scanning a subdirectory inside a repository.
Fixes#4706
Addresses the third review on #4606: `record` guarded pruning with
`spawned.len() >= PRUNE_THRESHOLD`. Once the recorded vec stabilized
above the threshold with entries the sweep could not remove — a run
whose live children exceed the threshold, e.g. `for i in {1..1000}; do
sleep 60 & done` — every subsequent `record` re-entered `prune_exited`
while holding the registry lock. Each sweep is O(N) (a status probe per
entry, plus a Toolhelp descendant walk on Windows for exited roots), so
the per-spawn cost climbed to O(n²) even though the doc-comment
promised amortized O(1).
The registry now tracks a `next_sweep_at` watermark alongside the
recorded vec (both under one mutex — the two fields are always
mutated together). A sweep fires only when `spawned.len()` crosses
that watermark; every sweep rescheds the next fire `PRUNE_THRESHOLD`
further records away from the current post-sweep size. Sweep frequency
is now capped at one per `PRUNE_THRESHOLD` records regardless of live
set size, restoring true amortized O(1) per spawn.
`build_targets` resets the watermark after its own sweep so the
record-time schedule stays consistent with the post-cancel live set.
Added `spawn_registry_watermark_bounds_sweep_frequency`: fills the vec
past threshold with permanently-live entries, records another 20, and
asserts the vec grew by exactly 20 (no sweep modified it) and the
watermark did not advance. Existing tests updated for the collapsed
`state` mutex.
Fixes#4605
Addresses the second review on #4606: `prune_exited` retained an entry
while its process was running OR its process group was alive. On Windows
`process_group_alive` is always `false` (no process groups), so the
predicate collapsed to "root running" — when a brush-spawned root exited
after starting a child that stayed alive (a `pwsh`/shell command that
launches a helper and exits, an MCP stdio wrapper handing off to a
long-running server), pruning immediately dropped the pinned handle.
Dropping that handle closes the last thing keeping the root pid slot
reserved. Windows can then recycle the pid onto an unrelated process,
reintroducing the exact race #4605 closes. It also strands the leftover
child: the next cancellation wave has no root to walk descendants from,
so `signal_tree` never reaches it.
The retain predicate is now:
- root process still running → keep (all platforms);
- Windows-only: root exited but the pinned handle still probes at least
one live descendant via Toolhelp → keep, because the handle is what
guarantees the walk targets the original subtree (recycled pids would
not be reachable while the handle holds the slot);
- pgid still alive → keep (Unix only; Windows falls through).
Unix stays unchanged because a child reparented onto init keeps its
pgid, so `process_group_alive` already catches "root gone, descendants
alive" without a per-entry tree walk.
Fixes#4605
Addresses the review on #4606: pinning a stable `Process` per spawn is
correct against pid reuse, but a long-running shell command that spawns
many short-lived external processes (a bash loop invoking a binary per
iteration) would otherwise retain one owned OS handle per historical
spawn — a pidfd on Linux, a process `HANDLE` on Windows — until the run
ends. Under enough iterations that hits the per-process FD/handle limit
and starts breaking `Process::from_pid` (or any other file operation) for
the rest of the run.
`SpawnRegistry` now sweeps entries whose pinned process and process group
are both gone. The sweep runs opportunistically inside `record` once the
recorded vec crosses a small threshold (`PRUNE_THRESHOLD = 64`) and
unconditionally at the top of `build_targets`, so the retained handle
count is bounded by the current live tree rather than the historical
spawn count. Amortized cost per spawn stays O(1); a sweep is O(N) probes
of `Process::status` (non-blocking pidfd `poll` on Linux, `WaitForSingle
Object(_, 0)` on Windows), running at most once per `PRUNE_THRESHOLD`
records.
The previous identity-pinning regression test was rewritten to defend
the actual invariant — the pinned handle carries into `build_targets`
while the child is alive, and the entry is dropped (never re-opened by
pid) once the child exits. A new regression asserts pruning keeps
retained entries under `PRUNE_THRESHOLD` after 2×threshold spawns of
short-lived children.
Fixes#4605
`SpawnRegistry` previously stored only the raw pid reported by brush's
`SpawnObserver` hook and deferred `Process::from_pid` to `build_targets`
at cancellation time. Between the moment a bash-spawned child exited and
the moment cancellation fired, Windows could recycle that freed pid onto
an unrelated process — typically another `pwsh.exe` or `powershell.exe`
in a different Cursor terminal tab, since PowerShell is the parent shell.
`Process::from_pid` at kill time then opened the impostor, and
`signal_tree` walked the current Toolhelp snapshot for `ppid == root`
matches and `TerminateProcess`'d whatever subtree happened to live under
that recycled pid. That is the reporter's Variant A symptom: OMP crashing
kills unrelated PowerShell sessions.
The `SpawnObserver` impl now pins a stable `Process` handle *at spawn
time*, before any pid recycling window can open:
- Windows: an open process handle keeps the pid slot reserved for the
handle's lifetime (Raymond Chen's documented invariant), so the pid
cannot be reassigned while the registry holds a reference.
- Linux: the pidfd carries identity independent of the numeric pid.
- macOS: the recorded `(pid, start_tvsec, start_tvusec)` triple detects
impersonation on every subsequent access.
`TerminationTargets::add_process` accepts a pre-pinned handle and skips
the `Process::from_pid` re-open entirely, and `build_targets` no longer
consults the raw pid at all — an entry the observer failed to pin (the
child exited before we could open a handle) becomes a no-op instead of
racing pid reuse.
Fixes#4605
Updated collab-web package metadata to the current published version and aligned swarm-extension with the matching pi-coding-agent major.
Documented the pi-uu-grep clap-only fields with dead_code allow reasons so the lint exceptions remain intentional.
Fixes#4549
- Exposed `Markdown.getLastRenderSettledRows` to track streaming progress.
- Simplified scrollback architecture by migrating logic to a unified `SeamLineList` boundary.
- Removed legacy safe-end methods and redundant `findCommittedPrefixResync` test suites.
- Fixed live tool and evaluation preview duplication issues during re-layouts.
- Introduced parallel streaming grep with windowed result processing to enhance search performance and memory management.
- Implemented stateful parallel file walking with buffer pooling and directory entry record caching to minimize memory allocations.
- Optimized ignore state derivation by using directory entry names instead of stat probes.
- Added comprehensive unit and performance tests covering parallel traversal correctness, early termination, and streaming behavior.
- Collapsed seven duplicated stderr-trim + exit-format sites across diff.rs, rcopy.rs, and overlayfs.rs into one pub(crate) helper.
- Exit code stays caller-formatted so signal-death renders unchanged.
- Also covers the fuse_mount site PR #4376 left with an eager to_string.
Spawned a stderr reader before writing git apply stdin so large diagnostics cannot fill the pipe and deadlock dirty-state seeding.
Added a regression test with a fake git process that fills stderr before consuming stdin.
Fixes#4231
- Downgraded `blocking_task_panic_scope` panics from silent to logged recoverable.
- Persists panic reports of caught worker task panics to the disk crash log while keeping stderr silent.
- Consolidated panic payload message extraction by reusing `crash_handler::panic_payload` in the task runner.
- Promoted the `SilenceHook` test helper to the shared testing module for use across multiple test suites.
- Extracted the panic message before disposing the payload; disposal now runs under its own catch_unwind with mem::forget fallback so a Drop-panicking panic_any payload can no longer unwind across the napi extern C boundary and abort the host.