Commit Graph

13 Commits

Author SHA1 Message Date
can1357 df0bb6e31a feat: implemented provider wire codecs and optimized telemetry and parsing
- Added internal protobuf wire codecs, message builders, and protocol definitions for Cursor and Devin providers.
- Deferred loading of OTel SDK and OTLP exporters and added bounded caches to optimize startup and lookup performance.
- Added SQLite-backed parse caching for legacy extension source analysis and streaming file chunk parsing for changelogs.
- Added support for rendering context usage overflow above 100% in the status line component.
2026-08-20 07:54:06 +02:00
Diogo Soares Rodrigues b6e01c8a3c feat(ai): handle Cursor's modern exec wire protocol
Current Cursor CLI builds emit exec frames this client did not model. A
frame whose oneof number is absent from `agent.proto` decodes with
`message.case` unset, so the dispatcher found no handler, ran no tool and
sent no result — the server was left waiting on an execution that never
happened.

Every recognised frame now gets a typed answer:

- The seven Pi tools (45-51) run their local equivalents. They are a
  separate wire family from the legacy args, not aliases: `pi_grep`'s
  `ignore_case` is the inverse of the local `case` flag, `pi_find`
  searches filenames (so it routes to `glob`, not `grep`), and
  `pi_edit`'s replacements are renamed to snake_case pairs.
- Hooks, subagents, prechecks, MCP state, smart-mode, canvas,
  conversation search and agent-store answer with the error, not-found or
  empty-but-valid variant that is true of this client.
- Unnameable frames raise `ExecClientControlMessage.throw`
  (`unknown_exec_variant`); recognised frames with no truthful answer —
  `git_diff_request`, whose `GetDiffResponse` has no error variant —
  raise `exec_variant_unsupported`.

Four frames previously answered `create(XSchema, {})`. In proto3 that is
not an empty result: the oneof is unset and the server reads it as "the
tool ran and produced nothing", indistinguishable from success. They now
send real variants.

`connect_scm` lost its repository (the target rides in a oneof, so the
flat property was always undefined) and settled on a fixed failure at the
announcement, before the server's `success`/`error`/`rejected` verdict
arrived on the completion frame.

The stream decoder tracked a single "current" tool-call block and settled
it on any `toolCallCompleted`, ignoring the envelope `call_id`: an
unrelated completion paired the wrong block, and `start A, start B`
orphaned A so nothing ever paired it — which strips the whole interaction
from every rebuilt transcript. Blocks are now retained per envelope id.

`lsp` is advertised as MCP again; the native `diagnostics` frame covers
one of ~10 actions.

(cherry picked from commit 4d269724a3a448886d13b4323ac02aadbfe38de3)
2026-07-30 01:41:55 +02:00
can1357 daf2680298 fix(cursor): bound the dispatch drain by the abort signal and settle todo calls when the host sync callback throws
Two defects flagged by review on the final head, both in the new drain/settle
machinery:

- drainInFlightDispatches looped on Promise.all unconditionally. Exec handlers
  have no cancellation contract (the bridge calls tool.execute with no signal),
  so a hung or long-running tool held the terminal error hostage after Ctrl+C.
  The drain now returns once the signal aborts; late results were discarded
  after agent_end regardless.

- A host todoSync callback that throws (session persistence on disk failure)
  skipped both the paired result and toolcall_end, stranding the live card and
  leaving the resolved block to be stripped from rebuilt transcripts. The
  callback is now caught and the call settles as a failure carrying the thrown
  message.

Both regression tests fail without their fix (the first by hanging the stream).
2026-07-26 15:41:27 +02:00
Diogo Soares Rodrigues c11a315c7b fix(cursor): read MCP tool calls from the protobuf oneof
ToolCall.tool is a oneof: a wire-decoded message exposes the selected
variant as { case, value } and never as a flat mcpToolCall property.
Both MCP reads — the streamed start that creates the block and the
completion that merges the decoded arg map — used the flat property, so
they saw undefined on every real message. Hand-shaped fixtures kept
passing, exactly as they did for the native todo calls this PR started
with; selectMcpCall mirrors selectTodoCalls, flat fallback included.

The streamed block also named itself `name || toolName` while
decodeMcpCall (and therefore the paired result) uses `toolName || name`.
Aligned, so the block and its result cannot disagree.

Test round-trips through toBinary/fromBinary with name != toolName, and
covers both reads: it fails if either goes back to the flat property or
the precedence flips.
2026-07-26 09:29:13 -03:00
Diogo Soares Rodrigues eaf7a3d605 fix(cursor): pair a result for every server-resolved call
Three orphan paths, same failure mode: the assistant block is marked
kCursorExecResolved before the work runs, so agent-loop.ts emits no
placeholder for it, and any path that produces no toolResult leaves the
call unpaired — buildSessionContext then strips the whole interaction
from every rebuilt transcript.

1. resolveExecHandler returned no toolResult on three exits (no handler
   installed, handler produced nothing, handler threw). Each now pairs a
   result carrying the same text the server sees in execResult, routed
   through onToolResult like a real one. `pairing` is a required
   parameter so a new callsite cannot silently recreate the orphan.

2. Agent only installed its result-buffer sink when cursorExecHandlers
   or cursorOnToolResult was set. Both are optional, so a bare SDK host
   dropped the provider result on the floor. Installed unconditionally;
   a non-Cursor provider never calls it.

3. A todo completion frame with no tool_call (the field is optional)
   skipped settlement entirely. It now settles as "nothing to mirror".

Also fixes an empty update_todos with a nonzero total_count being
mirrored as an authoritative clear: the length guard added earlier
skipped the mismatch check for empty responses, so a partial or
size-limited merge response deleted every local task at once.
2026-07-26 09:29:13 -03:00
Diogo Soares Rodrigues ab17ef5657 fix(cursor): refuse partial update snapshots and blank todo contents
Two more unrepresentable-snapshot cases, same doctrine as the existing
collision/dependency refusals.

- The total_count mismatch guard only applied to read_todos. A partial
  or size-limited update_todos merge response is just as incomplete, and
  mirroring it deleted every task the server omitted. The empty case
  still splits: an empty read is ambiguous (proto3 defaults total_count
  to 0) and refused, while an empty update remains the authoritative
  clear path.

- content is a proto3 string, so a missing value arrives as "". The
  local list is keyed by content and resolveTaskOrError rejects a falsy
  one before lookup, leaving the row unreachable to every task-targeted
  done/drop/rm.

Each guard has a positive control so the refusal is attributable.
2026-07-26 09:29:13 -03:00
Diogo Soares Rodrigues cc210094ef fix(cursor): refuse todo dependency graphs and sanitize failure text
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.
2026-07-26 09:29:13 -03:00
Diogo Soares Rodrigues 0ab0aa6331 fix(cursor): refuse empty read_todos and say when a snapshot is not mirrored
An empty `read_todos` with `total_count=0` (proto3 unset or genuinely
empty) was accepted and mirrored as an authoritative wipe. Refuse empty
reads; clearing the list stays on `update_todos`.

Benign refusal text is now "Todo snapshot not mirrored" instead of
"No todo changes", which falsely described a server-accepted update that
only the local mirror declined.
2026-07-26 09:29:13 -03:00
Diogo Soares Rodrigues a42264beeb docs(cursor): scope the collision claim to task-targeted ops
The refusal rationale overstated the blast radius: `getTaskTargets`
falls back to a phase's tasks, then to every task, and only consults
`findTaskByContent` when an op names a `task`. So a content collision
strands the second row for task-targeted `done`/`drop`/`rm` only --
phase-wide and untargeted ops still reach both.

The guard is unchanged; only the source comment, test comment, and
changelog entry were overstating why it exists.
2026-07-26 09:29:13 -03:00
Diogo Soares Rodrigues 56857e9900 fix(cursor): refuse todo snapshots that collide on content
Cursor's wire model identifies todos by `id` and can represent two rows
sharing the same content. The local list is keyed by content alone
(`findTaskByContent`) and `todo` rejects a duplicate outright, so
importing such a snapshot would leave every later `done`/`drop`/`rm`
resolving to the first row and the second permanently unaddressable.

Preserving identity would mean threading an id through the tool, the
renderer, and the persisted phases; the local model has no such field.
Refusing is consistent with the two refusals already there (filtered and
short reads): local state untouched, the call still settles as a no-op.
2026-07-26 09:29:13 -03:00
Diogo Soares Rodrigues 0639246d27 fix(cursor): settle refused and failed native todo calls
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.
2026-07-26 09:29:13 -03:00
Diogo Soares Rodrigues 29e64ce608 fix(cursor): resolve and persist server-owned todo blocks
Review follow-up on two defects in the native todo bridge.

`tool_execution_end` was emitted under a freshly generated UUID, but the
interactive transcript files the visible block under the streamed
`callId` and only clears it when the ids match. The card therefore stayed
pending and animating for the rest of the session. The settled call id is
now passed to `todoSync`, making the parameter required so no caller can
silently reintroduce a mismatch.

Nothing produced a `toolResult` for these blocks either: `todoSync` only
appended a custom entry and emitted a transient event. Since
`buildSessionContext` strips any `toolCall` with no matching result, the
interaction vanished from every rebuilt transcript -- reload, branch
switch, or Ctrl+L -- leaving a "tool call elided" placeholder. A paired
result now travels the same `onToolResult` channel the other
server-resolved Cursor calls already use, including when the snapshot is
refused: the call happened, it just changed no local state.
2026-07-26 09:29:13 -03:00
Diogo Soares Rodrigues 7214951ead fix(cursor): sync native todo list from server-resolved tool calls
Cursor resolves its native `update_todos`/`read_todos` tools server-side,
so the todo list never followed the model's intent locally.

Two defects, both silent:

- `agent.v1.ToolCall` is a protobuf oneof. A decoded message exposes the
  selected variant as `tool: { case, value }` and has no flattened
  `updateTodosToolCall` property, so the bridge recognized no native todo
  call at all on the wire path.
- The synthesized `todo` block was emitted as locally runnable carrying a
  `{todos}` payload the local tool's schema rejects, turning every update
  into a validation error and driving a spurious continuation turn.

Todo calls are now read through the oneof, both native blocks are stamped
resolved, and local state is mirrored only from the server's confirmed
success snapshot. Partial `read_todos` responses -- narrowed by
`status_filter`/`id_filter`, or short of the server's own `total_count` --
are subsets, not the list, and are refused rather than deleting the tasks
they omit. `TODO_STATUS_CANCELLED` maps to `abandoned` instead of
reverting the task to `pending`.

The exec bridge mirrors each snapshot into session state, refreshes the
interactive panel via a synthetic `tool_execution_end`, and persists to
the session branch so the list survives reloads, rewinds, compaction, and
session switches. Existing phase grouping is preserved.

Regression tests drive the bridge with wire-encoded protobuf, which is
the only shape production ever sees; all six fail without this change.
2026-07-26 09:29:13 -03:00