Recall silently clipped every result to 500 chars mid-word with no marker,
and memory_edit update replaces content wholesale by id. There was no way
for an agent to inspect the full row before overwriting it: Mnemopi.get
existed but nothing surfaced it, and the advertised URI only served the
file-backed memory summary. The natural recall/inspect/update loop had no
inspect step.
Three fixes across mnemopi and coding-agent:
- recall now appends an ellipsis marker when it clips content and reports
truncated=true plus full_length. The cap is exposed as
RecallOptions.contentPreviewChars (default 500, 0 disables). The factLine
200-char clip used by the enhanced-context sandwich gets the same marker.
- Under the mnemopi backend the read-tool URI scheme now routes an id host
to Mnemopi.get() across every session's scoped banks, returning the row
as text/markdown with a YAML-frontmatter header (bank, store, source,
timestamp, importance, veracity). The root namespace remains for the
file-backed summary. Miss errors now name the backend explicitly.
- Updated the recall and memory_edit tool prompts to document the
truncation marker and require reading the full memory before any
wholesale content update.
Fixes#4443
- Added test case to verify that missing provider limits in sibling accounts are correctly marked as not reported.
- Verified that the usage breakdown correctly distinguishes between reported and unreported sibling provider limits.
Four tightly-scoped hot-path fixes covering the highest-impact items in the
reporter's CPU profile (13.1 s profiled / 30 s window):
1. `event-controller.ts:handleEvent` no longer fires a blanket
`statusLine.invalidate() + ui.requestRender()` before every session event.
The pre-render was a leftover from #4145 when `updateEditorTopBorder()`
still eagerly rebuilt the border; the lazy provider added in #4145 made
it redundant. It fired on every `message_update`/`tool_execution_update`
during streaming — the pre-render's frame ran while the handler was
awaiting, then the handler's own `requestRender` scheduled a second
identical frame. Every handler that mutates visible state already calls
`requestRender()`.
2. `shimmer.ts:shimmerSegments` iterates the segment string in place instead
of building a code-point array with `Array.from(seg.text)` every animation
frame. Runs of same-tier chars are emitted via a single `slice` per run
rather than accumulating into `runBuf`. Surrogate pairs stay atomic — the
code-point index still advances by 1 per emoji. Microbench over 30k
frames: 45 ms → 18 ms (2.53x), allocation rate down from ~N-per-frame to
a handful per frame. New tests cover mixed BMP+surrogate and all-emoji
inputs. `Array.from` was the #1 self-time hotspot in the reporter's
profile at 10.2%.
3. `Markdown.setText` gains an equality guard mirroring `Text.setText`
(returns `false` when `text === #text`). Providers re-emit identical text
on ticks with no delta (throttled frames, reconciled tool-execution
updates); each of those now short-circuits instead of dropping
`#cachedLines` and forcing a full lex + wrap on the accumulated paragraph
(the reporter's #3 hotspot at 8.4%). New test asserts render-reference
stability + return-value semantics.
4. `SPINNER_RENDER_INTERVAL_MS` aligned with `SPINNER_GLYPH_ADVANCE_MS`
(both 80 ms). The previous 33 ms cadence emitted ~2.4 paints per glyph
step; the differential-output dedup only skips the write, not the
compose walk. Visually identical (glyph advance was already 12.5fps),
halves paints during tool execution.
Skipped (out of scope for a bug fix, deserve dedicated PRs):
- Freezing streaming prefix on single `\n` boundaries — correctness-bound
to `\n\n` block separators (CommonMark loose-list continuation).
- Compose-phase idle gate + adaptive-backpressure moving-average — need a
component-level dirty flag; the 200 ms cap in `#scheduleRender` was set
for a reason (#4145 tail-latency guard).
Tests updated: two IRC-expiry tests in event-controller-message-start.test.ts
that were asserting the pre-render's second `requestRender` call now expect
one.
Fixes#4353
Merged PR #4330 and fixed the remaining offered-boundary regression by stopping offer promotion at intervening live blocks.
Verified with targeted transcript/native scrollback regressions: 40 pass.
- Added `normalizeSingleStringField` to dynamically map misplaced string inputs to required schema fields for single-argument tools.
- Integrated argument normalization into `validateToolArguments` to handle model-specific variations in JSON payloads during validation passes.
- Updated `coding-agent` streaming and rendering components to recognize `_input` as a legacy alias for `input` across various UI paths and logic flows.
- Refactored `hashlineEditParamsSchema` to strictly enforce the `input` field while maintaining support for legacy aliases via runtime coercion rather than schema definition.
- Corrected unit tests to reflect that `_input` is rejected by the strict schema but handled gracefully by the validation layer.
SessionManager.#runFencedAtomicRewrite's finally now only clears #atomicRewriteFenceEpoch when it still matches the unwinding task's epoch. When flushSync supersedes an in-flight rewrite (bumping #diskEpoch and resetting #diskTail), a fresh atomic task scheduled at the new epoch can take ownership of the fence before the stale rewrite finally settles; the previous unconditional clear stranded the newer rewrite's bookkeeping so subsequent sync appends took the hot writer path and were then detached by the newer publish.
Regression: SequencedRewriteStorage pauses the first N writeTextAtomic calls on per-call gates. Test schedules a stale rewrite, forces flushSync to bump the epoch via a fenced append, schedules a newer rewrite that parks at pauses[1], releases the stale gate (stale unwinds and guard-rejects), then appends a custom entry — asserts writerOpens does not grow (fence preserved) and the fenced entry lands in the newer publish's body. Without the fix, writerOpens grows from 1 to 2.
Fixes#4338
- Added `#hasPendingAsyncWake` to detect running or pending background jobs owned by the agent.
- Deferred todo reminders and `session_stop` hook passes until all agent-owned background async jobs complete.
- Ensured scheduling pauses caused by async jobs do not trigger terminal session stops or premature todo nags.
Cursor's provider only pushed toolCall content blocks for MCP and todo
in processInteractionUpdate.toolCallStarted. Native tools (bash, read,
write, grep, ls, delete, lsp) execute via the exec channel and produced
no toolCall blocks, so persisted assistant messages contained only text.
On replay, renderSessionContext could not pair the subsequent toolResult
messages with any toolCall block and fell through to addMessageToChat
(a no-op for toolResult), causing header-less \`\\u23ce\` output beneath the
last assistant text.
- packages/ai/src/providers/cursor.ts: add synthesizeCursorExecToolCall
and inject it at the top of each native exec case in
handleExecServerMessage, using the coding-agent bridge's mapped tool
name and args so live event and rebuild render identically. Normalize
args.toolCallId before invoking the handler so provider block id and
bridge result id always match.
- packages/agent/src/agent.ts: drop the text-length split in
#emitCursorSplitAssistantMessage. With toolCall blocks now at their
correct positions in content, emit the assistant message as-is
followed by buffered toolResults; the split's preambleText-per-text
copy also silently duplicated text on multi-block turns.
- packages/ai/test/cursor-streaming-args.test.ts + new
packages/coding-agent/test/issue-4348-repro.test.ts: guard block
ordering, event sequence, and rebuild pairing behavior.
Fixes#4348
Replaced the boolean #atomicRewriteActive flag with #atomicRewriteFenceEpoch: number | null. The fence branch in #appendToSessionFile now applies only while the pending atomic rewrite's epoch still matches #diskEpoch. Once flushSync -> #rewriteSynchronously bumps the epoch, the in-flight writeTextAtomic is guaranteed to abandon via its commitGuard, so subsequent sync appends can (and must) take the hot path against the freshly-published body instead of being stranded in memory when close() returns without another rewrite.
New regression: pauses writeTextAtomic mid-flight, appends a fenced custom entry, calls flushSync (which captures it into the durable body), then appends a message + custom entry after the epoch bump. Reads the current JSONL BEFORE releasing the paused atomic and asserts both post-flushSync entries are already on disk; then releases the atomic (commitGuard rejects) and closes the session and asserts nothing is lost.
Fixes#4338
- Simplified match logic to rely exclusively on content hash equality.
- Removed strict validation that rejected colliding snapshot tags.
- Updated recovery behavior to resolve collisions to the most-recently recorded snapshot.
- Refactored tests to expect successful preview and patching despite tag ambiguity.
SessionManager.#persistTitleChangeEntry's catch fallback previously did a single-shot atomic rewrite: any prompt/tool appended while it awaited was fenced with #atomicRewriteDirty=true but never re-serialized. Extracted the fenced-rewrite do-while loop into #runFencedAtomicRewrite and used it from both #rewriteAtomically and #persistTitleChangeEntry, so fenced entries during either path are captured before the task resolves.
Added SessionStorage.drain(): for FileSessionStorage and MemorySessionStorage it is a no-op; IndexedSessionStorage already had one and now conforms to the interface. SessionManager.flush() and close() await it so a graceful shutdown does not exit while a fire-and-forget writeTextSync publish (queued by flushSync on an indexed backend) is still on the wire — reducing the residual publish-window race for Redis/SQL where the backend cannot be aborted mid-flight.
Regression covers the title fallback loop: TitleFallbackPausingStorage forces updateSessionTitle to throw, pauses the fallback's writeTextAtomic, appends a message and a custom entry during the pause, and asserts (a) both fenced entries land on the current JSONL, (b) the final title is applied, and (c) writeTextAtomicCalls >= 2 proving the loop iterated.
Fixes#4338
IndexedSessionStorage.writeTextAtomic no longer delegates directly to writeText, which yielded on #awaitPath between the guard check and the backend publish. The new impl consults the guard three times — up front, again after #awaitPath resolves, and finally inside the enqueued task immediately before #backend.writeFull — so a flushSync that bumps #diskEpoch while the atomic rewrite is suspended cannot land stale content on Redis/SQL backends. When the enqueue-time guard rejects, the optimistic index update is restored only when nothing has advanced it past our mtime, so a concurrent writer's state is preserved.
Added a PausableWriteFullBackend regression: the first writeTextAtomic parks inside backend.writeFull holding the per-path tail; the second queues with a guard that flips after the first is released. The backend records only the first content, confirming the guard is honored at publish time.
Fixes#4338
FileSessionStorage.#replaceSessionFileAfterEpermSync now unlinks the staged temp file when commitGuard returns false in both fallback branches: the ENOENT-vanished-target path and the post-move-aside path (where the moved-aside backup is also restored). Honors the writeTextAtomic contract that a guard-rejected stage is discarded.
Regressions cover all three guard-reject exits: the direct rename pre-check, the ENOENT branch inside the EPERM fallback, and the move-aside branch that also restores the backup. Each asserts no orphan .tmp remains in the session dir.
Fixes#4338
SessionManager.#rewriteAtomically now enables #atomicRewriteActive before #closeWriterHandle() and keeps it set until the rewrite task exits, so a sync append landing in the close-yield window is fenced and cannot open a fresh writer that the pending writeTextAtomic would then detach from the current JSONL path. Same pattern applied to the #persistTitleChangeEntry atomic fallback.
Added a regression that pauses the fake storage's writer.close() gate, appends a message and a custom entry during the pause, and asserts (1) no new writer opens (writerOpens counter unchanged) and (2) the fenced entries land on the current JSONL path after the rewrite completes.
Fixes#4338
SessionStorage.writeTextAtomic now accepts a commitGuard the backend calls synchronously immediately before publishing the staged body. FileSessionStorage performs the guard check and rename in the same tick via fs.renameSync (both on the direct path and the EPERM move-aside fallback), so a concurrent #rewriteSynchronously (flushSync -> Ctrl+C / session exit) that bumps the disk epoch cannot be overwritten by the stale body serialized before it ran. MemorySessionStorage and IndexedSessionStorage honor the same guard.
SessionManager.#rewriteAtomically threads a guard that returns false when the disk epoch changes, and re-checks the epoch after every writeTextAtomic before touching #fileIsCurrent / #rewriteRequired. #persistTitleChangeEntry's atomic fallback wires the same guard.
Added a regression that pauses the fake storage's writeTextAtomic mid-flight, appends a session_exit custom entry (which the fence records in memory), calls flushSync, releases the paused rewrite, and asserts the exit record is still on the JSONL path and the atomic publish was rejected by the guard.
Fixes#4338
Fenced synchronous session appends while an atomic full-file replacement is active so Windows EPERM fallback cannot detach the append writer from the current JSONL path.
Added a deterministic storage fake regression covering superseded compaction rewrites, title changes, session-exit diagnostics, resume, and post-rewrite tool/assistant tail persistence.
Fixes#4338
Addresses codex review on #4335: the previous wrap dropped to cmd.exe on Windows, which broke bash tool semantics for $VAR, $(...), source, and POSIX quoting even when the local executor would have resolved Git Bash / bash.exe. The wrap now takes the resolved ShellConfig (shell binary + login/-c args + optional prefix) from settings.getShellConfig() and reuses it for the ACP terminal/create shape, so the ACP path matches the local path on both platforms. Tests updated to stub getShellConfig and assert the resolved-shell shape deterministically.
The bash tool routes commands through the ACP client's terminal/create when the client advertises the terminal capability. It was passing the full shell line as the ACP command field with no args, which relies on the client interpreting command through a shell. Per the ACP protocol docs, command is the executable and args is its argv tail; a spec-conformant client spawns them directly (no implicit shell), so any bash line with a space, pipe, &&, redirect, or $(...) failed with ENOENT and the agent silently degraded to read-only tools.
The bash tool now wraps the shell line before the createTerminal call: { command: /bin/sh, args: [-c, line] } on POSIX and { command: cmd.exe, args: [/d, /s, /c, line] } on Windows. /d/s/c matches Node's spawn({ shell: true }) convention (/s preserves the whole shell line as one argv element on the receiving end). process.platform on the agent side proxies the client's platform, which matches the near-universal ACP shape of an editor spawning omp as a co-hosted subprocess.
Fixes#4333
Bun keeps the parent event loop alive when an unref'd child has a piped
stderr stream. The first #4324 fix started with stderr: "pipe", so a
long-lived idle TTS/STT/tiny/mnemopi worker could keep short CLI commands
alive even after proc.unref().
Switch worker stderr capture to a temp-file fd target instead of a Bun
ReadableStream pipe. The parent does not start any JS read while the
worker is alive; after onExit it reads the bounded tail from the file,
logs captured lines, appends the tail to the surfaced worker Error, then
closes and removes the temp capture.
Add a regression that spawns a non-test wrapper process with an idle
unref'd worker and asserts the wrapper exits immediately. Existing stderr
capture, truncation, and intentional SIGKILL behavior remain covered.
Fixes#4324
Separated audited offerable transcript rows from durable snapshot rows so lower finalized content below a live block can be repaired instead of duplicated when the live block grows.
Added transcript and virtual-terminal regressions for the lower finalized tail case.
Fixes#4326
Inference worker subprocesses (TTS, STT, tiny-model, mnemopi embeddings)
were spawned with stderr: "ignore", so a native crash inside the child
was completely discarded. The parent only ever logged the bare exit code
(e.g. Kokoro TTS's recurring "tts subprocess exited with code 7"),
leaving the recurring crash loop undiagnosable.
createWorkerSubprocess now pipes stderr and drains it in the parent:
- Each decoded stderr line is forwarded to logger.debug under
"<exitLabel> stderr" so operators get live visibility on chatty native
runtimes without touching the chat scrollback.
- A bounded 16 KiB ring keeps the tail of stderr so the eventual exit
Error carries the actual crash reason (ONNX Runtime traceback, glibc
assertion, etc.) instead of "code 7" alone. The prefix is preserved so
existing log grepping keeps working.
- The exit event and the stderr pipe are independent, so a synchronous
read in onExit would race the drain. SpawnedSubprocess grew a
stderrDrained: Promise<void>, and onExit chains the error surface off
it so callers see the whole tail. Tests can await stderrDrained
deterministically instead of racing wall-clock timers.
- Intentional terminate() SIGKILLs still stay silent — signal-exit
gating on intentionalExit is unchanged.
Fixes#4324
- Changed the `path` property from an array of strings to a single semicolon-delimited string across tool definitions and tests.
- Updated validation error messages to reflect the new `path` input format.
- Adjusted all relevant test cases to provide path targets as semicolon-separated strings.
- Tracked placeholder/partial-result paints via render() override so an update landing before the shape reaches the terminal skips resetDisplay().\n- Added negative-case unit tests proving no reset fires when the intermediate shape was never painted.\n\nFixes #4314
- Added virtual-terminal coverage for SSH placeholder and partial-result repaint seams.\n- Asserted stale placeholder and pending rows disappear from the terminal buffer while current SSH output remains.\n\nFixes #4314
- Added renderer hooks for first-result placeholder replacement and partial-result settle repaints.\n- Enabled the hooks for SSH and covered the streamed-placeholder and settle seams.\n\nFixes #4314
Added providers.webSearchGeminiModel and GEMINI_SEARCH_MODEL so Gemini web_search requests use a selected grounding model while keeping gemini-2.5-flash as the fallback.
Covered OAuth, Developer API, and missing modelVersion fallback paths in Gemini web search tests.
Fixes#4312
Allowed npm:<package> install specs to validate against the resolved package name while still forwarding the original spec to Bun.
Added regression coverage for installing npm:pi-figma-remote-auth through PluginManager.
Fixes#4310
Converted ArkType validation failures into ConfigError results and surfaced models.yml validation failures in CLI model listing and noninteractive startup.
Fixes#4305
MCPOAuthFlow silently swallowed a rejected dynamic client-registration
response and then threw the opaque "OAuth provider requires client_id"
error from the fallback authorize probe. Users adding Figma's MCP server
hit this because Figma's DCR endpoint 403s every unlisted client (per
the Figma MCP catalog gate) and the surfaced string gave them nothing
actionable to do.
Capture the DCR endpoint + HTTP status (or the thrown transport error)
on the flow instance and, when the authorize probe confirms client_id
is required, throw an error that names the endpoint, the outcome, and
the manual oauth.clientId/oauth.clientSecret workaround. The no-DCR-
endpoint branch gets its own message so users know the server never
advertised registration in the first place.
Fixes#4307