Bun.sleep(timeoutMs).then(...) leaves an uncancellable timer registered
with the event loop, so every successful handler race in the runner
leaked one — a completed tool_call/tool_result handler could delay
non-interactive CLI exit by up to the 30s default cap. Verified with a
subprocess exit-time probe: buggy pattern exits in ~5000ms for a 5s
timeout, setTimeout+clearTimeout pattern exits in ~17ms.
Extract a raceHandlerWithTimeout helper backed by setTimeout with a
finally-scoped clearTimeout, and route both #runHandlerWithTimeout
(pre-existing latent leak) and emitToolCall (introduced in the same PR)
through it. No behavior change on the timeout branch.
Addresses review on #3951 from chatgpt-codex-connector[bot].
StdioTransport.request() awaited stdin.write() and stdin.flush() before returning the internal deferred promise. When the child stopped draining stdin (wedged process, or full OS pipe buffer with no reader), Bun's FileSink returned a pending Promise that never settled — the async function got stuck above 'return promise', past the timeout timer and the abort handler. cleanup() + reject() still ran on the inner deferred, but the outer async-function promise never adopted it, so the caller's await hung forever and the deferred rejection surfaced as an unhandled promise rejection.
Send the frame without awaiting: sync EPIPE throws (Windows) still reject the request immediately; async EPIPE rejections (POSIX processTicksAndRejections) are wired to the same reject() via a guarded failFromSend handler that no-ops after cleanup(). The returned promise now settles from the response, the timer, the abort signal, or the read loop's transport-close broadcast.
Regression test spawns 'sleep 60' (POSIX only), sends a 1MB tools/call payload past the pipe buffer, and asserts the deferred rejects with the timeout error before the outer window elapses and produces no orphaned unhandled rejections.
Fixes#3945
The in-process fd builtin passed no-op heartbeats to pi_walker for
both its gitignore-respecting fallback path (`collect_with_heartbeat`
in `search`) and its fast path (`for_each_entry_with_heartbeat` in
`try_search_fast`), so cancellation of a large or slow directory walk
was deferred until traversal completed. The shell wrapper flips the
shared `AtomicBool` cancel flag when the runtime cancellation token
fires and then awaits the blocking task; with no heartbeat hookup the
walker had no way to observe the flag mid-walk and kept collecting the
whole tree before the wrapper could return exit 130.
Introduce `cancel_heartbeat(&AtomicBool)` — the walker-level heartbeat
that returns `io::ErrorKind::Interrupted` when the flag is set — and
plug it into both walker calls. Both call sites recognize the resulting
`WalkError::Interrupted` alongside `cancelled` and break silently
instead of surfacing an `fd:` diagnostic on stderr; the shell wrapper
owns the user-visible exit code.
Regression cover: a walker-level test pre-sets the cancel flag and
asserts `collect_with_heartbeat(cancel_heartbeat(&flag))` surfaces
`WalkError::Interrupted` instead of collecting the tree; two
higher-level `search` tests exercise the silent break for both the
fallback and fast paths; a fourth pins the non-cancelled contract so
the added heartbeat can't stall normal searches. Neuter the helper to
a no-op and the walker-level test fails with the pre-fix `WalkOutcome`
showing every entry scanned — the exact bug the issue reports.
Fixes#3949
emitToolCall awaited each extension handler directly (runner.ts:704-706),
bypassing the #runHandlerWithTimeout wrapper every other subscribed event
routes through. A tool_call handler that never resolves parked
ExtensionToolWrapper.execute indefinitely, freezing tool dispatch even
though the symmetric emitToolResult path has always been timeout-protected.
Race each tool_call handler against Bun.sleep(extensionHandlerTimeoutMs)
inline (the shared wrapper swallows errors, and this callsite is
fail-closed). On timeout: emit an ExtensionError with event: 'tool_call',
log a warning, and return { block: true, reason: 'Extension <path>
timed out after <ms>ms' } — symmetric with the existing per-handler error
branch. Fail-closed is the correct policy for a pre-execution gate: an
unresponsive extension MUST NOT be silent consent to run the tool.
Fixes#3948
- Added `hasUsableNativeToolCall` helper to verify that a streaming tool call has non-empty, trimmed name and id values.
- Retain projection initialization and updates on subsequent deltas if the provider emits native tool identifiers late.
- Guard tool call synchronization and late salvage logic to prevent empty or invalid placeholders from corrupting streaming state.
gen:bundle previously ran gen:docs and gen:docs:reset around the build. Under prepack that reset ran before bun pm pack assembled the tarball, so the published src/ copy of docs-index.generated.txt was empty and consumers importing via subpath (e.g. @oh-my-pi/pi-coding-agent/internal-urls/docs-index) fell through to the missing repo docs/ tree.
Restored gen:docs in prepack so postpack owns the reset (matching the original lifecycle), and made gen:bundle assert the embed is populated and fresh instead of touching it, so a direct gen:bundle outside prepack fails with an actionable message.
Fixes#3934
Kept the STT subprocess referenced while download and stream requests are pending so setup cannot exit before the worker answers.
Propagated worker download errors to setup callers and verified completed downloads leave the expected cache files.
Fixes#3939
Added a contributor-facing native crate map (docs/native-crates.md) covering pi-natives, pi-shell, pi-ast, pi-iso, pi-walker, pi_uu_grep, pi-uutils-ctx, and vendored brush crates, and linked it from natives-architecture.md and user-facing-packages.md.
Added a docs-index tool coverage test asserting every BUILTIN_TOOL_NAMES entry and injected custom tool (generate_image, tts) has a docs/tools/<name>.md page served by omp://.
Inlined tiny fail/buildPayloadText/checkDocsIndexFreshness helpers in generate-docs-index.ts per the project rule against single-expression named functions.
Fixes#3934
Retained only the requested AST search page window in native ast_grep/ast_match and the coding-agent multi-target wrapper while preserving exact totals.
Fixes#3935
Added root omp docs for memory_edit, learn, manage_skill, generate_image, and tts, plus package-level coverage for user-facing README-only CLIs.
Added a docs-index freshness check to package check and made gen:bundle generate and reset the docs embed itself.
Fixes#3934
When a batched task spawn is cancelled while still queued behind task.maxConcurrency the semaphore now rejects acquire(), but the previous patch let the abort throw past the aborted handler so progress.status and onSettled never fired and buildAsyncDetails kept reporting the batch as running. The wrapper now records whether the slot was held, funnels both acquire-time and post-acquire aborts through the same aborted branch (releasing only when held), and a batch regression test pins the contract.
Fixes#3930
The grep and rg in-process builtins passed no-op heartbeats to pi_walker
when recursing into a directory, so the uutils scope's cancel flag was
ignored mid-walk. The shell wrapper sets that flag on abort/timeout and
then awaits the blocking task — with no heartbeat hookup, a cancelled
recursive grep/rg waited for the whole tree to be scanned before the
shell could return exit 130.
Add pi_uutils_ctx::is_cancelled() and have grep's search_dir, rg's
search_dir, and rg's collect_filtered_files supply heartbeats that
return io::ErrorKind::Interrupted when the flag is set. The walker maps
that to WalkError::Interrupted, which the utilities now silently treat
as a harness cancellation (no spurious 'native directory scan
interrupted' on the command's stderr — the shell wrapper owns the
user-visible exit code).
Regression tests pre-set the cancel flag and assert grep -r /
rg / rg --files exit without scanning the matching file in the tree.
Fixes#3933
- Added `#streamTurnNonce` to prevent aborted streaming turns from corrupting content indexes of subsequent messages.
- Implemented temporary stream-key generation using content position and turn nonces for previewing tool calls without native IDs.
- Added migration logic to key pending tool previews by their real ID and rekey `ToolArgsRevealController` once the real ID is parsed.
- Replaced native `AbortSignal.timeout` calls with self-clearing timeout helper functions across model discovery.
- Added `withTimeoutSignal`, `withCatalogDiscoveryTimeout`, and `withOpenAICompatibleDiscoveryTimeout` helpers to manage cancellable fetch timeouts.
- Supported `timeoutMs` options throughout the Ollama, Llama.cpp, LiteLLM, vLLM, LM Studio, and OpenAI-compatible discovery processes.
- Documented the fix addressing Bun garbage collection segfaults caused by uncancellable timeout signals.
- Introduced a two-pass file processing architecture with `ReadPolicy` and `FileOutcome` state tracking.
- Deferred oversized files to a second pass where only their leading segment is read and searched.
- Integrated the two-pass processing logic into both sequential and native parallel grep execution paths.
- Updated agent tool definitions and user-visible messages to reflect partial coverage of large files instead of skipping them.
- Anchors the incomplete-todo reminder block inside the scrollback transcript instead of a floating live container.
- Eliminates duplicate reminder copies piling up in terminal scrollback during terminal reflows.
- Removes the dedicated `todoReminderContainer` and simplifies state synchronization on todo reload.
- Updates tests to verify sequential reminders commit as separate blocks and are left intact when tools succeed.
- Replaced custom fast-walk and fs-cache implementations in pi-natives with a new dedicated pi-walker library.
- Integrated the thread-safe, parallel pi-walker library across pi-natives, pi-shell, pi-uu-grep, and uu-find.
- Rewrote file search, fuzzy finding, and glob-matching logic to leverage pi-walker configurations and visitor traits.
- Optimized shell process tracking in pi-shell by replacing global descendant-diff logic with an isolated, per-run SpawnRegistry.
- Added parallel rayon-based walking and optimized fast paths for directory scanning and entry classification.
Added setup.cfg and pyrightconfig.json to PYTHON_ROOT_MARKERS so pyright, basedpyright, and pylsp project shapes also probe Windows .venv/Scripts before PATH fallback.
Fixes#3916
Added Windows virtualenv Scripts directories to local LSP command resolution so project-local Ruff launchers are discovered before PATH fallback.
Fixes#3916
- Added a `SpawnObserver` trait to intercept freshly spawned external command PIDs and process group IDs.
- Exposed `set_spawn_observer` and `spawn_observer` on `ExecutionParameters` to allow tracking process lifecycles.
- Triggered the observer on successful external command execution unless the process is detached or reparented.
- Added a helper to build the Git process environment that explicitly clears common ambient Git environment variables.
- Applied the new environment builder to both async and synchronous Git commands to prevent environment bleeding from parent processes.
- Migrated the `/resume` session selector from an inline component to a fullscreen overlay.
- Enabled alternate screen buffer borrowing and mouse tracking support for the picker.
- Ensured proper cleanup of the fullscreen overlay during normal termination or shutdown.
- Configured the selector layout to pin keybinding hints and the footer to the bottom of the screen.
- Introduced a model canonicalization helper to strip redundant model compatibility fields that match defaults.
- Regenerated the models catalog JSON to eliminate over eight hundred lines of redundant compatibility specifications.
- Updated the variant collapse logic to rebuild models using the projected compatibility configurations.
- Added a missing type annotation to a test environment variable to resolve a compilation warning.
- Added configurations for `anthropic/claude-sonnet-5` under openrouter, vercel-ai-gateway, and zenmux providers.
- Reduced model pricing and cost structure rates for `anthropic/claude-sonnet-5`.
- Removed `trustExplicitThinkingOnly` compatibility flag from several Claude and Gemini model entries.
- Removed legacy `disableStrictTools` property from model definitions and updated tests.
- Fixed model builder variant collapse logic to properly map `compatConfig` to `compat`.
- Sanitized environment variables in git-clone test helpers to avoid host-leakage in test runs.
Extended the mid-prompt /skill:<name> parser exclusions to also defer
to the bash tool (!cmd / !!cmd) and the python tool ($ cmd / $$ cmd
followed by ASCII whitespace), so drafts like '!echo /skill:reviewer'
are no longer consumed as skill invocations before the local-execution
branches of the interactive submit path get to dispatch them.
${HOME}-style shell expansions and prose-leading $ characters (which
pythonCommandPrefixLength already declines) keep matching mid-prompt
skills as before.
Fixes#3913
Restricted mid-prompt /skill:<name> parsing to non-slash drafts so
builtin/custom slash-command arguments such as /compact /skill:foo are
not intercepted before the command dispatcher runs.
Added parser and RPC dispatch regression coverage for the precedence
case while keeping leading /skill:<name> (including leading whitespace)
working.
Fixes#3913
The mid-prompt slash skill autocomplete added in #3654 replaced the
entire editor draft with /skill:<name> on accept so the dispatcher
(which only matched leading /skill:) would still fire. That wiped
every keystroke the user had typed before reaching for the skill.
Insert the /skill:<name> token at the cursor in the TUI editor —
replacing only the partial /sk slash token, leaving prose before and
after intact — and extend the skill-command parser so a /skill:<name>
token surrounded by whitespace is recognized as an invocation too,
with the surrounding prose threaded through to the skill as args.
The parser change is shared across all three dispatch sites
(interactive TUI, ACP, RPC) via a new parseSkillInvocation helper
in extensibility/skills, so the three Map<string,string> /
session.skills lookups stay aligned on the same parse.
Fixes#3913
- Removed instructions prohibiting mocks and testing defaults in favor of using a tester agent when available.
- Streamlined delivery contract rules by removing the explicit instruction against suppressing tests.
- Updated verification claim guidelines to emphasize smoke testing.