- Expanded web search and fetching providers with robust parsing, authentication storage integration, and response validation.
- Added support for new configurations including SearXNG safesearch, Cloudflare AI Gateway endpoints, and dynamic Firecrawl base URLs.
- Implemented comprehensive test suites covering error handling, content filtering, and provider-specific response behaviors.
Re-applies the race fix from 7b21716754 that 563f00fe31 dropped as
'unrelated': InteractiveMode.init() defers the prewarm behind setImmediate,
and init()'s own awaits are microtasks that can resolve without yielding to
the immediate queue, so asserting right after await init() races under CI's
chunked load. Flush one immediate tick before asserting.
When `{{toolRefs.grep}}` resolves to "grep", the rendered line read
"Regex search or locating targets → grep, not grep, rg, or awk" — a
self-contradiction. Match the surrounding convention ("not shell
grep") as proposed in #8032.
The persisted pre-vibe snapshot was applied on every reconciliation that
re-entered vibe mode, including cold resumes and switches in from a non-vibe
session. Those paths build their toolset from the current CLI flags and
settings, so replacing it with a historical snapshot silently drops tools the
session was started with: resuming a session that entered vibe under
--tools read with --tools read,bash restored only read on exit.
Gate the override on the one case the snapshot exists for: the vibe -> vibe
switch, where #clearTransientModeState kept the already-reduced live set.
dispose({ timeoutMs }) legitimately detaches the consolidate pass when
the shutdown budget expires mid-flight (#3641), so asserting the shared
bank flush synchronously after dispose raced the detached pass on slow
CI runners (0 calls observed on run 31287979000). Signal the flush call
through a deferred and await it after releasing the lock; the bounded
<500ms return assertion is unchanged.
`/goal <objective>`, `/plan <prompt>` and `/vibe <prompt>` promote the
composer draft into the first turn, but built their submission from the
draft *text* only:
this.onInputCallback(this.startPendingSubmission({ text: objective }));
The editor-submit path in `InputController` passes
`editor.pendingImages`/`pendingImageLinks` alongside the text; these four
call sites did not. A draft holding pasted screenshots therefore reached
the model with its positional `[Image #N, WxH]` markers intact and every
image payload missing, so the agent saw markers pointing at nothing and
`read "Image #1"` resolved against an empty list.
The payload was not only dropped, it also outlived the draft: the mode
commands cleared the composer with `editor.setText("")`, which leaves
`pendingImages` attached. The orphans then rode along with whatever the
user typed next, one index off, which is how a later message can attach a
screenshot the user never re-pasted.
Measured on 259 image-bearing user messages across 10 local session logs
(v17.2.x): 36 of 37 messages submitted as a goal objective lost every
image, against 190 of 198 preserved on the ordinary submit path.
Fix:
- `#takeDraftImages()` detaches the composer's pending images and links,
and all four mode-command submissions spread it into
`startPendingSubmission` (`cancelPendingSubmission` already restores
them when a submission is cancelled).
- `/goal`, `/guided-goal`, `/plan` and `/vibe` clear the draft with
`editor.clearDraft()` instead of `editor.setText("")`, so images can
never outlive the text they were pasted into (the streaming branch of
`/goal` never submits, so its draft must die whole).
Tests: two regression cases in `goal-mode-integration.test.ts` assert the
objective submission carries the image and empties the composer; both
fail on the previous behaviour with `images: undefined`. The `/plan`,
`/goal` and `/guided-goal` slash stubs now model `clearDraft`.
- seal() now runs before the final dispose close() and bumps the disk
epoch: work an event handler enqueues while dispose awaits the closing
tail is superseded, and an already-running fenced or authoritative
rewrite fails its commit guard at the rename fence instead of
publishing over a file a revival reopened.
- The authoritative repair path resets the disk tail itself, escaping the
close() serialization, and would atomically publish the emptied entry
list; it now no-ops once sealed and commit guards also check the seal.
- Execution-time gates cover the queued title persist and fenced rewrite
callbacks; setSessionName/appendCustomEntry attempted by a handler that
outlives dispose are dropped and covered by the seal regression, and a
failed atomic batch across the seal can no longer truncate the file.
- Replaced count-only assertions with the exact ordered message set:
revival sees only the seed, the reread holds seed + post-revive, and
the sealed manager's late persist text appears nowhere in the file.
- AgentLifecycleManager.park() resolves as soon as dispose() returns, so
ensureLive() may reopen the same JSONL through a new manager while a
timed-out event handler still holds the old one; a late append reopened
a second writer on that file and the deferred finalize then closed it.
- A post-release rewrite was worse: it persisted the emptied entry list,
truncating the transcript on disk.
- releaseRetainedEntries() now seals the manager: appends, title changes,
and rewrites become dropped no-ops and the append writer is closed, so
the deferred dispose pass is in-memory only and can never touch the file.
- File-backed regression: dispose on the drain deadline, revive the JSONL
immediately, unpark the late handler, and prove the file is byte-stable
and the revival writer owns it exclusively.
- The dispose drain deadline does not cancel in-flight event handlers: one
parked in a slow extension hook resumed after close/release, reopened the
append writer for its late persist, and repopulated the released state.
- Track whether the drain settled; on deadline, redo the final close +
release once the pipeline genuinely settles (hook runtime is bounded by
the extension runner).
- Expose drainTimeoutMs on AgentSessionDisposeOptions for bounded teardown
paths and deterministic coverage of the deadline branch.
- PR #8004 dispose() now releases the session manager in-memory transcript;
snapcompact-budget rebuilt a session over the closed manager and the exit
diagnostics read released entries.
- Snapcompact reopens the persisted file for its replacement session; exit
diagnostics move to disk-backed managers and assert the exit marker from a
reopened manager, proving actual durability.
- PR #8004 made dispose() release the session manager in-memory transcript;
three handoff tests reused the closed manager for a replacement session.
- Reopen the persisted session file via SessionManager.open, matching
production revival paths.
- Added account-scoped policy error detection to correctly identify Codex cyber-policy rejections.
- Updated credential storage and retry logic to route denied accounts through sibling rotation instead of bypassing it.
- Ensured coding-agent sessions exhaust all sibling accounts before falling back on cyber denials.
- Added comprehensive test coverage for credential rotation and retry behavior on policy errors.
- Add minimum character threshold to bypass minimization for short outputs.
- Add preserve-if-empty configuration and pipeline support for filters.
- Update test fixtures and integration tests to meet length thresholds.
- Add a tree-sitter syntax probe and parser veto to prevent syntax-unaware boundary repairs.
- Reject span pastes from empty named registers instead of deleting ranges.
- Reject duplicate top-level snapshot row line numbers during parsing.
- Export new syntax module symbols and add associated tests and changelog updates.
setAgentDir() clears OMP_PROFILE/PI_PROFILE and snapshots the agent dir,
so restoring only PI_CODING_AGENT_DIR/HOME leaks the default profile to
later tests when the suite is started under a caller-selected profile.
Capture and restore all three env vars plus HOME, then rebuild the dir
resolver via __resetDirsFromEnvForTests() (disabled-extensions pattern).
Addresses review comment on PR #8018.
listClaudePluginRoots tests point HOME at a temp dir but left the agent
dir at the real profile (~/.omp/agent). User-scope discovery (native MCP
config via getAgentDir()/mcp.json) then read the machine's real
mcp.json, leaking its servers into loadCapability results and failing
'deduplicates a plugin alias of a directly configured MCP connection'
whenever the real agent dir contains an mcp.json.
Point the agent dir at a temp dir in beforeEach and restore the
previous value (or unset PI_CODING_AGENT_DIR) in afterEach, matching
the established pattern in mcp-profile/autolearn-discovery tests.
- Removed the Loader backpressure cap so slow ConPTY paints retain the documented proportional duty cycle.
- Made WSL terminal-title working state static to avoid a second periodic OSC write loop.
Fixes#8012
agent-core dispatches the session's event subscriber fire-and-forget (agent.ts #emit), so a message_end/agent_end handler can still be awaiting extension/subscriber/maintenance work — and its sessionManager/agent.state append — after agent.waitForIdle() resolves. The earlier settle waited only on the core run, so a late handler could append the finished message/entries back into the disposed session and re-pin the transcript.
Track every #handleAgentEvent dispatch in #inFlightEventHandlers and drain it (alongside agent.waitForIdle) inside the bounded settle before reset/clear/release. Added a regression test using a real extension whose message_end hook blocks before persistence, asserting dispose does not release memory until the in-flight handler settles.
dispose() only *signalled* the agent loop via abort(); it never awaited the run, so a mid-turn dispose (Ctrl-C/timeout/hard-killed subagent) could let the loop unwind after the release ran — its response/SSE interceptors re-recording wire frames into rawSseDebugBuffer and its terminal message re-appending to agent.state.messages, repopulating the disposed session with exactly the retained state the release drops.
Detach the response/SSE interceptors and await a bounded agent.waitForIdle() before the reset/clear so it lands on a quiescent session. Added a deterministic regression test that gates the active turn and asserts dispose blocks on it before clearing.