Compared the resolved advisor runtime inputs before rebuilding on explicit enable so unchanged /advisor on calls keep the live advisor agent and history.
Added regression coverage for the unchanged-config path preserving advisor context.
Fixes#4302
Rebuilt live advisor runtimes when the explicit enable path runs so changed modelRoles.advisor values are re-resolved without restarting the session.
Added a regression test covering a live advisor moving from one role model to another through setAdvisorEnabled(true).
Fixes#4302
Cancel the ACP terminal timeout timer on completion: the hoisted Bun.sleep(timeoutMs) left a live ref'd timer for the full command timeout after every fast command, accumulating timers and delaying process shutdown in SDK/headless use. Switched to a cleared setTimeout, and rewrote the timeout regression test against real time instead of mocking Bun.sleep (the mock coupled the test to timer implementation and starved the event loop).
Replace scattered persisted-message cache invalidation (11 call sites across branch-mutation flows) with use-time validation: the memoized key set is anchored to (session file, leaf id) and rebuilds itself whenever the anchor moved. Every branch mutation changes the leaf by definition, so staleness is structurally unrepresentable and no mutation path has to remember to invalidate. Incremental append extends the cache only when it was fresh for the branch the append lands on.
Validate the full-session retention cache with a rolling hash over the entire retained prefix instead of only the boundary message: a branch rewrite that leaves the boundary message unchanged (e.g. in-place edit of earlier messages) now also triggers a full rebuild. Regression test covers the boundary-unchanged prefix rewrite.
Self-healing boundary guard for the full-session retention cache: fingerprint the last retained message; on branch shrink (rewind) or prefix rewrite (branch switch/compaction), rebuild the full transcript instead of retaining stale content or silently retaining nothing. Regression tests cover append, shrink, and rewrite.
Codex reviewer flagged that the ranged-read fallback recommended by
the patcher's over-cap reveal path (`path:N-M`) itself applies the
512-column cap and still records the displayed line numbers via
`recordSeenLinesFromBody`. On a minified wide anchor line, `read
file:N` shows only the clipped prefix but the line lands in the
tag's seenLines — a subsequent edit anchored at N then slips past
the seen-line guard.
- `packages/coding-agent/src/edit/file-snapshot-store.ts`:
`recordSeenLinesFromBody` grows an optional `excludedLines` set;
parsed line numbers matching it are filtered before recording.
- `packages/coding-agent/src/tools/read.ts`:
`#readLocalFileMultiRange` and the single-range disk path build a
`clippedLines` set alongside `columnTruncated` for both direct-range
lines and `buildLineEntriesWithBlockContext` context lines, then
pass it into `recordSeenLinesFromBody`.
- `packages/coding-agent/src/tools/grep.ts`:
same wiring for the match line via `match.truncated`, plus a
conservative length+`...`-marker heuristic for context lines
(native `crates/pi-natives/src/grep.rs` `truncate_line` doesn't
propagate a per-line flag on `contextBefore`/`contextAfter`; a
proper native-side flag is a follow-up).
- `packages/coding-agent/test/edit/seen-line-guard.test.ts`:
new case reads a 4KB single line and asserts the clipped line
number stays out of `seenLines` and the edit against it still
rejects with the seen-line guard.
TTL-based negative cache (30s) for failed !command config resolutions instead of process-lifetime: transient secret-provider failures recover without restart while still bounding the execSync retry storm to one probe per window.
- Replace the module-level writeMessage with a private DapClient.#writeMessage
that races sink.flush() against a 30 s cap and this.proc.exited, disposes the
client, and rethrows on either failure. A wedged adapter stdin previously
blocked writeMessage forever.
- sendRequest fires the write in the background and attaches a passive
promise.catch guard so the request timer's rejection cannot become an
unhandled promise before the caller's await subscribes. This also lets the
caller's timeoutMs win over a hung flush instead of waiting the full 30 s
write cap.
- #spawnSocketUnix and #spawnSocketClientAddr wrap the readiness / connect
race in try/catch that kills proc before rethrowing. Add a private
socketReadyTimeoutMs hook on DapSpawnOptions so tests do not have to wait
the full 10 s socket cap.
Regression tests cover all three failure modes: a wedged stdin flush must
reject sendRequest at the caller timeout with no unhandledRejection, and the
Unix + TCP client-addr spawn paths must terminate the detached adapter
process when the socket never appears.
Fixes#4233
The status-line renderer's #lookupPr method called `gh pr view` through
Bun's raw `$` shell with no signal, no timeout, and no non-interactive
environment. A stalled `gh` process (keychain prompt, network hang, auth
deadlock) wedged the await forever; because #prLookupInFlight was set
before the call and never reset, subsequent renders skipped the lookup
and the child leaked indefinitely.
Route the lookup through the existing `git.github.run` helper with
`AbortSignal.timeout(git.GIT_COMMAND_TIMEOUT_MS)` so the child inherits
GH_NON_INTERACTIVE_ENV (disabling terminal and keychain prompts) and
receives SIGTERM on the standard 5-minute deadline. Non-zero exit still
falls through to the null cache, preserving the failure-tolerant
behavior.
Fixes#4234
readTextFromClipboard called execSync for pbpaste, termux-clipboard-get,
wl-paste, and xclip; readMacFileUrlsFromClipboard did the same for
osascript; copyToClipboard for termux-clipboard-set. execSync parks the
event loop until the child exits or the 2000ms timeout fires, so a hung
clipboard daemon froze the TUI render loop for the whole budget on every
paste and copy chord (input-controller handleImagePaste and
handleClipboardTextRawPaste).
A new spawnCapture helper wraps Bun.spawn with the same 2000ms guard,
stdout-to-string decoding, and non-zero-exit/timeout throw semantics the
outer try/catch already assumed. Every synchronous clipboard shell-out
now yields to the event loop while the child runs. Regression test
under readTextFromClipboard runs a slow fake pbpaste and asserts a
concurrent setInterval keeps ticking; the pre-fix code delivered zero
ticks.
Fixes#4235
Added timeout-backed AbortSignals to update, Hindsight, and Smithery fetch calls so stalled endpoints abort instead of hanging indefinitely.
Added regression coverage for the timeout signals on the exposed command/client paths.
Fixes#4229
PluginManager.install (bun install + bun update), PluginManager.uninstall,
PluginManager.#fixMissingPlugin, the legacy installer.ts install/uninstall
helpers, and generate-legacy-pi-bundled-registry.ts's formatInPlace all
called Bun.spawn with stdout/stderr piped and awaited proc.exited before
touching either stream. Once a child's output exceeded the ~64 KiB OS
pipe buffer, the child would block on write(2) while the parent blocked
on exit — a classic pipe-buffer deadlock. Even where Bun's current runtime
happens to buffer eagerly, the pattern silently leaked unbounded bytes.
Each site now starts new Response(proc.stdout).text() and stderr readers
immediately after Bun.spawn and awaits them alongside proc.exited via
Promise.all. Existing error semantics are preserved: install throws with
stderr, uninstall keeps its generic error, and formatInPlace still includes
Biome's stderr in the failure message.
Adds a regression test (plugin-install-git.test.ts) that models the
OS-pipe deadlock by holding proc.exited until both mock streams are
drained — install must read them before awaiting exit, else the test hits
its 2s Promise.race timeout.
Fixes#4230
runSshSync and runSshCaptureSync in connection-manager.ts invoked ssh
through the Bun shell with .quiet().nothrow() but no timeout and no
abort signal. They sit on the ensureHostInfo -> probeHostInfo /
ensureConnection path that runs *before* SshTool.execute applies the
user-supplied command timeout, so an unreachable host or wedged
control-master hung the tool forever.
Switch both helpers to ptree.exec with a 30s timeout,
allowNonZero: true, allowAbort: true, and stderr: "full". The
timeout is a parameter (default 30_000) so tests can override with a
short bound; both helpers still return their existing failure-result
shape (exitCode / stdout / stderr), so ensureConnection can surface a
'Failed to start SSH master ...' error instead of blocking.
Add a regression test that stages a fake ssh binary trapping SIGTERM
and sleeping 300s, then confirms both helpers return within a bounded
window with a non-zero exit code.
Fixes#4232
Plan mode records idle IRC deliveries into context without waking a turn,
so an 'irc send await:true' sender was stranded until its wait timeout:
delivery reported injected but no reply could ever be generated. Extend the
existing ephemeral side-channel auto-reply (previously only for mid-turn
recipients with async execution disabled) to the idle plan-mode case — the
second situation where a real reply turn cannot happen in time. No primary
turn is woken; plan-mode convergence stays user-driven.
The deliverIrcMessage eligibility change itself rode in the previous commit
(same-file hunks); this commit carries the bus doc and the regression test:
an awaited idle IRC message in plan mode resolves the sender's bus waiter
via the side-channel reply while the primary loop stays asleep.
The settle-time reminder queues a hard 'required' tool choice paired with a
scheduled continuation. If that continuation never runs (user prompt bumps
the generation, dispose, compaction/handoff) or plan mode is exited first,
the queued directive leaked onto the next unrelated turn as a forced tool
call. Remove it by label on continuation skip, on user-initiated prompts,
and when plan mode is disabled.
docs-rs.ts:handleDocsRs downloads up to 50 MB of compressed rustdoc JSON (MAX_BYTES) and then decompresses it with gunzipSync without a size bound, so a zip bomb can expand 10:1+ and OOM/crash the process. Pass { maxOutputLength: 256 * 1024 * 1024 } to gunzipSync at line 402 to cap decompressed output; if the limit is exceeded it throws and falls through to the existing catch (signal check / return null), preserving the failure contract. Coding-agent typecheck (bun run check:types) and Biome check on the changed file pass; no dedicated test exists for this path.
Closes#4249
CollabSocket.send previously called ws.send() whenever the socket was OPEN without checking bufferedAmount, allowing the WebSocket send buffer to grow unbounded under a slow relay or guest. The fix adds 64 KiB high / 32 KiB low backpressure thresholds: new frames are queued when bufferedAmount is above the high watermark, and a 25 ms drain timer retries flushing queued frames once the buffer drops below the low watermark, preserving send order via the existing #sendChain. Overflow still drops frames once #pendingSends reaches MAX_PENDING_SENDS, and the drain timer is cleared on close, reconnect, and fatal failure. Verified with `bun test packages/coding-agent/test/collab` (64 pass, 0 fail).
Closes#4248
Main removed the floating todoReminderContainer in 112317bc8 (todo
reminders are now anchored inside the scrollback transcript and reset
by renderInitialMessages({clearTerminalHistory: true})), so the added
this.todoReminderContainer.clear() references a property that no longer
exists post-merge, failing typecheck and throwing on every /btw branch.
Revert the interactive-mode hunk and its test, and reword the changelog
entry to cover only the goal-mode todo context fixes.
buildSessionOptions in packages/coding-agent/src/main.ts awaited three independent resolvePromptInput calls for SYSTEM.md, APPEND_SYSTEM.md, and TITLE_SYSTEM.md sequentially, adding unnecessary I/O latency on the startup path. Replace the sequential awaits with Promise.all so the three file reads run concurrently while preserving the same variable assignments and error propagation. Verified with bun run check:types and the coding-agent-heavy test suite.
Closes#4247
The github renderer materializes plain Text per display rebuild, so its
animatedPendingPreview opt-in requested 30fps repaints with a frozen
glyph for the whole run_watch wait; drop the flag. Custom tools with
only one of renderCall/renderResult never route to the animated generic
fallback, so gate the unregistered-renderer spinner on both being
absent. Regression tests for both no-tick contracts.
With retainMode: 'full-session' (the default), HindsightSessionState.retainSession previously called prepareRetentionTranscript on the entire message window every N turns, making total formatting cost O(n²) over a long session. The change caches the formatted transcript and the index of the last retained message, appending only newly added messages and advancing the cache after client.retain succeeds. The cache is reset when the session id changes, conversation tracking resets, or the mode leaves full-session. Verified byte-identical output against full-window formatting, plus bun run check:types and bun test packages/coding-agent/test/hindsight-backend.test.ts.
Closes#4246
The consume-once source map kept entries for graph modules the initial
import never loaded (modules only reached via lazy dynamic imports).
Their first import - possibly long after load, and after an on-disk
edit - was served the boot-time snapshot instead of current file
content, and the unconsumed sources stayed in the plugin closure for
the process lifetime.
Clear the map once the entry import settles: everything Bun loaded at
startup was already consumed (keeping the read-once win), and anything
left must be read at its actual import time, matching pre-dedup
behavior for lazy modules. The new regression test passes on the
pre-dedup baseline and fails on the unfixed dedup.
TtsrManager.checkDelta concatenated the full stream buffer on every text_delta and thinking_delta before scope filtering in #matchBuffer, making long streams O(n²) even when all registered rules are tool-scoped. Added #canMatchText and #canMatchThinking flags, set them only after successful rule registration from the computed scope, and returned early from checkDelta for text/thinking sources that no rule can match. Verified by re-reading the modified ranges; no test or typecheck suites were run per campaign constraints.
Closes#4245
In `wrapBunWorker` (`packages/coding-agent/src/eval/js/context-manager.ts`), `error` and `messageerror` listeners were registered during normal operation, but the `close` listener was only added inside `close()`. If user code called `process.exit(0)` or the Bun worker otherwise exited cleanly, the `close` event fired with no handler, so `runOnce` never rejected and callers hung until the cell timeout.
Add a normal-operation `close` listener in `wrapBunWorker.onError` that forwards `new Error("JS eval worker exited")` through the existing error handler path, and remove it in the returned unsubscribe callback.
Verified with `bun run check:types` and `bun test test/tools/eval-*.test.ts test/core/eval-workflow-helpers.integration.test.ts` (32 pass, 0 fail).
Closes#4244
isAllCapsWord matched any multi-letter token without a lowercase letter,
so CJK tokens registered as ALL-CAPS words: two adjacent ones marked the
whole source shouty and silently disabled acronym restoration for every
non-Latin-script message (e.g. '修复 CNPG 集群故障' kept 'Cnpg'). Require an
actual uppercase letter; cased-script shout detection is unchanged.
The PR's changelog, doc comment, and prompt examples all name ETL as a
restored acronym, but the review-response narrowing (vowel heuristic +
allowlist) silently dropped it: ETL bears a vowel and was not listed.
Add it to COMMON_TITLE_ACRONYMS and pin it in the allowlist test.
AgentSession.#sessionMessageAlreadyPersisted previously called SessionManager.getBranch() on every message_end event, rebuilding the root-to-leaf path and JSON-comparing each entry; for long sessions this is O(n) per message and O(n²) over a growing transcript. Add #persistedMessageKeys, a memoized Set<string> of persistence keys for the current branch, so the common missing-key check uses Set.has and only falls back to a branch walk on rare key collisions. Invalidate the cache whenever the leaf path changes (newSession, createBranchedSession, resetLeaf, branch, and branchWithSummary) and incrementally warm it after each real append. Verified with git diff --check, a scoped bun build of agent-session.ts, and bun test packages/coding-agent/test/agent-session-persisted-keys-cache.test.ts (2/2 passing).
Closes#4243
`withAbortTimeout` rejected after `MCP_CALL_TIMEOUT_MS` but left `waitForConnection` / `callTool` running because the timeout never propagated an abort signal to those underlying promises. The fix creates a per-call `AbortController` in `createMCPProxyTools.execute`, combines its signal with the caller signal via `AbortSignal.any`, and passes both the combined signal and the controller to `withAbortTimeout`, which aborts the controller on timeout or caller abort. Verified by `bun build packages/coding-agent/src/task/executor.ts --target=bun --format=esm` and `bun test packages/coding-agent/test/task-executor-mcp-timeout.test.ts`.
Closes#4242
ACP terminal RPCs in `BashTool.execute` were awaited without abort or timeout guards, so a hung `createTerminal`, `kill`, or `currentOutput` call could keep the tool alive past its configured timeout. The ACP polling loop also recreated `exitPromise`/`abortedP` `.then()` closures on every 250 ms tick, leaking memory during long-running commands. This change moves abort/timeout setup before `createTerminal`, races it against the timeout/abort signals, hoists the exit/abort racers outside the loop, tracks `lastPolledOutput`, and bounds kill and final-output waits with fallbacks; late-resolving `createTerminal` handles are killed and released. Verified with `bun run check:types` and `bun test test/bash-acp-terminal.test.ts` in `packages/coding-agent` (6 pass, 0 fail).
Closes#4241
`BashInteractiveOverlayComponent.appendOutput` pushed every PTY chunk into an unbounded `#writeQueue` while xterm drained asynchronously, so fast output could grow memory without bound during the tool timeout. Added `MAX_LIVE_WRITE_QUEUE_CHUNKS = 512` and `#trimWriteQueue()` to drop oldest pending chunks at/after the first pending index after each append. `bun run check:types` passed for the package, and a focused JS sanity check verified the trim keeps the queue under the cap for idle and active-write cases without mutating active write state.
Closes#4240
`registerPersistedSubagentsFromDir` in `packages/coding-agent/src/modes/components/agent-hub.ts` used `fs.readdirSync` recursively to walk persisted subagent directories, blocking the event loop with unbounded synchronous I/O when the Agent Hub opened. Converted `registerPersistedSubagents` and `registerPersistedSubagentsFromDir` to async functions using `fs.promises.readdir`, and changed `AgentHubOverlayComponent`'s constructor to start registration fire-and-forget, exposing `persistedSubagentsReady` so callers can observe completion. Failures are logged with `logger.warn`, rows are refreshed on success, and a final render is requested in `finally`. Verified with `bun --check packages/coding-agent/src/modes/components/agent-hub.ts` and `packages/coding-agent/test/agent-hub-activate.test.ts`.
Closes#4239
Every keystroke or arrow-key navigation in the copy selector re-rendered the preview by syntax-highlighting the entire `target.preview`, replacing tabs, and wrapping it to width. `CopySelectorComponent` now keeps instance-private memo fields for the last target and its generated source; `invalidate()` clears them, and rendering reuses the cached source while the selected target is unchanged. Verified with `bun test packages/coding-agent/test/modes/components/copy-selector.test.ts` (5 pass, 0 fail).
Closes#4238