- 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.
Two independent bugs found in review.
A stream that dies mid-turn takes the terminal-error path: `settleH2`
rejects when the transport closes without `turnEnded`, so the flush on
the success path never runs. `connect_scm` and native todo blocks are
stamped `kCursorExecResolved` at start, so `agent-loop.ts` synthesizes
no placeholder and only their completion frame pairs a result - the call
was left unpaired and its card animating, and `buildSessionContext`
strips a dangling call from every rebuilt transcript. The catch path now
closes open blocks and pairs those server-owned calls with an
interrupted result. Exec-settled MCP blocks are excluded: the dispatch
that ran them owns their result and `drainInFlightDispatches` awaits it,
so pairing here would duplicate against the same id.
Separately, the advisor bridge supplied no `getToolContext`.
`ExtensionToolWrapper` reads the approval mode, per-tool policies and
`autoApprove` only from that execute-time context, so every wrapped
advisor bridge tool resolved as `yolo` with empty policies - a
configured `ask` or `deny` on `edit`/`grep` did not apply to native
frames. Advisors now get the same `ToolContextStore` as the primary
bridge.
Both are covered against the real paths: the interrupted call through
the HTTP/2 fixture server (a helper-level test passes even with the
catch-path flush removed), and approval through real deny policies.
(cherry picked from commit 5ace682578af96708caf88db2d44b9077a1c0e74)
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).
Two P2 findings on the exec-handler path.
1. The success path waits for in-flight exec dispatches before pushing
done, but the catch path did not. When the HTTP/2 completion rejects
while a handler decoded from the last chunk is still running, the
Agent finalizes the synthesized call from the terminal error and
clears its Cursor result buffer; the handler then lands its real
result after agent_end and it is discarded at the next run - even
though the tool may already have performed side effects. The barrier
is now a shared helper and inFlightDispatches is hoisted out of the
try so both exits drain it.
2. resolveExecHandler always synthesized "Tool produced no transcript
result" with isError: false for the TResult-only handler form. A
rejected or error protocol result therefore reached Cursor as a
failure while the rebuilt transcript recorded the same call as
successful. Every exec result is a proto oneof whose only non-failure
variant is success, so describeExecResult() derives the state from
the variant and reuses its own error/reason text - the same string
the server received.
The gate asserted the stream was unsettled and only then resolved
handlerDone. A failing assertion skipped the resolve, so the consuming
`for await` waited on a handler nobody would unblock and the real
failure surfaced as a timeout. Moved to a finally.
The h2 data loop parses every frame in a chunk synchronously and starts
each handleServerMessage with `void`, so the socket keeps draining while
a handler runs. Nothing tracked those promises: after h2Completion the
provider went straight to `done`.
When an exec request, turnEnded and the stream close arrive in ONE chunk
- routine, since the server has no reason to split them - the transport
completes while the exec handler (and any onToolResult transformer) is
still resolving. The Agent drains its Cursor result buffer on the
terminal event, so the result is reserved after the snapshot and never
persisted, leaving the synthesized (already resolved) toolCall block
unpaired and stripped on replay.
Dispatches are tracked in a Set and awaited after h2Completion. Each
already swallows its own rejection, so this only waits.
The test drives a real h2 server whose final chunk carries readArgs +
turnEnded, and releases the handler only once the server flushed its
response and the handler is known to be running - no wall-clock delay.
Verified it fails 5/5 without the barrier and passes 5/5 with it. A
microtask drain in place of the yield does NOT discriminate: the
client end handler is IO, not a microtask.
- Kept turnEnded as application-level state without settling the HTTP/2 request.
- Surfaced late CONNECT, gRPC trailer, request, and abort failures.
- Rejected streams that end before turnEnded and covered terminal lifecycle cases.
Fixes#5634