The lock-race and EPERM regression tests compared injection predicates
against the uncanonicalized temp config path, but the atomic writer
resolves it via realpath; on macOS /var -> /private/var made the mocks
never fire and both tests failed. Compared against the realpath instead.
- Treated missing files separately from malformed or unreadable configs.
- Backed up malformed YAML and wrote settings atomically without dropping pending changes.
- Reported success only after saving, with regression tests for global and project configs.
Upstream Pi's deleteKittyImage/deleteAllKittyImages return unwrapped control
sequences; legacy callers such as pi-sprite wrap tmux passthrough themselves.
Aliasing OMP's auto-wrapping encodeKittyDeleteImage (and hand-wrapping
deleteAllKittyImages) double-wrapped under tmux, so the outer terminal dropped
the delete command. Match the upstream bare-sequence contract and pin it in
the regression test.
Bun's global bin entry on Windows is a regular-file .exe shim, not a
symlink, so the standalone-binary override would have rerouted a
legitimate bun-managed install to in-place binary replacement and
clobbered the shim. Gate the override on POSIX, where package-manager
bin entries are always symlinks; add a regression test.
The settings panel persists "" when a credential is cleared and renders
that as unset; config list now uses the same semantics instead of
masking the empty string as a configured credential.
The #6694 deferral must not apply when the scope comes from the --models
flag: createAgentSession re-resolves the default role against
settings.enabledModels only and never sees parsed.models, so leaving
options.model unset let a saved out-of-scope default silently escape an
explicit CLI scope. Pin scopedModels[0] for CLI scopes as before.
When the default role IS deferred (settings-derived scope), also skip
seeding options.thinkingLevel from scopedModels[0]'s explicit suffix —
explicit options win in createAgentSession and would override the
re-resolved role's own thinking selector.
Retained complete native web-search call/result pairs through leaked-thinking projection at their original source anchors.
Kept opaque server-tool blocks atomic during persistence, stripped them on reparent, and covered custom continuations plus compaction retention.
Fixes#6703
Two boundary defects on the mirrored-todo path.
1. The Agent error drain snapshotted #cursorToolResultBuffer without
awaiting entry.pending, unlike #emitCursorSplitAssistantMessage. An
async cursorOnToolResult still running when the provider errored
patched an entry the catch path had already detached, so the
pre-transform payload was persisted. A provider error is exactly when
a transform is most likely to be in flight.
2. The todo renderer interpolated mirrored provider text straight into
terminal output. A Cursor snapshot carries model-authored task
content, phase names and summary text verbatim, so a label holding
ANSI/C0 sequences rewrote the terminal on every render and replay.
sanitizeText alone is not enough - it preserves tabs, which punch
holes in bordered output - so every display path now funnels through
one forDisplay() helper: task labels, blocker notes, phase headers,
the zero-task fallback, and the streaming renderCall preview. Raw
values are untouched; content and phase name are the identity keys
the local list is looked up by and what gets persisted.
When Cursor packs toolCallStarted and toolCallCompleted into one HTTP/2
chunk, the bridge tool_execution_end (synchronous callback, fired
mid-parse) reaches the interactive controller before the streamed
toolcall_start (queued on AssistantMessageEventStream, delivered a
microtask later). The controller found no pendingTools entry, dropped
the completion, and the card created afterwards animated forever.
Two halves, each necessary:
- Hold an early todo completion in #orphanedToolCompletions and replay
it when the streamed block creates its component.
- Guard card creation from cumulative message_update frames with the
turn-scoped #toolTimelineComponents map. Without this, the update
after the replay re-lists the same toolCall block, finds pendingTools
empty again, and spawns a second, permanently pending card. This is
also why emitting a synthetic tool_execution_start from the bridge
(previous attempt, reverted) could not work.
Both maps are cleared together at the existing transcript-anchor reset
sites. The normal ordering (start first) is covered by a control test.
Two review findings on the native todo sync.
- TodoItem.dependencies is a graph the local model cannot store: rows
are keyed by content, carry no id, and hold no edges. An imported
dependent row files as plain pending and nextActionableTask then
offers work the server considers blocked. Refuse snapshots with an
edge pointing at an unfinished row; edges whose blockers already
finished constrain nothing and still mirror.
- The todo failure warning interpolated the provider error verbatim.
Collapse and truncate it at the render boundary.
Also documents two known, unfixed defects: an async cursorOnToolResult
transformer resolving after the buffer drain, and the todo card
lifecycle race. Emitting a synthetic tool_execution_start for the
latter was measured and rejected -- the completion deletes the entry it
creates, so the late streamed block adds a second card.
Only a successful snapshot settled a native todo block. A `read_todos`
narrowed by a filter and a server `UpdateTodosError` both went
unanswered: no `tool_execution_end`, so the card animated forever, and
no `toolResult`, so `buildSessionContext` stripped the block on rebuild.
Every completed native todo call now settles. The refusal path carries
no `details.phases` -- `event-controller` feeds that straight into
`setTodos`, so echoing the current list back would let a call that
changed nothing overwrite live panel state. A server error is carried
through as a failed result instead of collapsing into the benign no-op.
Each regression is covered by a test verified to fail without its fix.