feat(cursor-adapter): visibility parity, cancel-race fix, stall watchdog #978
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!978
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "code-lead/950"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #950.
tool_call/status/taskSDKMessages to domaintool_progress/tool_summary/systemevents with per-tool arg + result summarisers; liftRunResult.git.branches[].prUrlintoresultText; rendercursor_init/cursor_status_*/cursor_tool_error/cursor_stalledand previously-droppeduserecho turns throughevent-log.for await (run.stream())with an abort-aware race so cancellation exits in <100 ms even when the cursor cloud stream is silent; best-effortr.cancel()regardless ofsupports("cancel"), behind a 10 s hard timeout; syntheticresult { subtype: "cancelled" }keepstask_historyhonest.service_config.watchdogs.cursor_stall_ms) emitssystem { subtype: "cursor_stalled" }so operators can tell a hung cloud session from a quiet model.Test plan
just qaclean (3311 pass, 28 new incursor-sdk-adapter.test.ts).cancelWithTimeoutresolves within the 10 s budget whencancel()hangs forever.cursor_stalledafter the configured threshold (tested with a 30 ms threshold).All AC met, CI green, 28 tests comprehensive. Two issues:
behavior
cursor-sdk-adapter.tsnextOrAborterror handler (~line 540): anyiter.next()rejection resolves as{ aborted: true }, so a cursor SDK error (network drop, auth failure mid-stream) records as"cancelled"in task_history. The re-throw viaqueueMicrotask(() => { throw err })is an uncaught synchronous throw in a microtask — in Bun this surfaces as anuncaughtException, not an unhandled-promise-rejection, and depending on the process's error handler may terminate the server. Fix: add a{ error: unknown }variant to the return union, yield asystem { subtype: "cursor_stream_error", details: { error } }event before breaking, then fall through to the cancelled result path so task_history stays consistent. ThequeueMicrotaskrethrow should be dropped entirely.behavior
cursor-sdk-adapter.tsstreamRunWithStallAndAbortfinally block (~line 590): when the loop breaks on abort,stream(theAsyncGeneratorfromrun.stream()) is never explicitly closed — thefinallyonly callsdisarmStallTimer(). The outercancelWithTimeout(run)handles the run-level cancel, but the generator may hold open an HTTP/2 or WebSocket frame buffer independently. Fix: addawait stream.return(undefined).catch(() => {})(orif (Symbol.asyncDispose in stream) await (stream as AsyncDisposable)[Symbol.asyncDispose]()) to thefinallyblock so the generator's internal resources are freed regardless of exit path.Both points addressed in
9b87f89:nextOrAborterror handling — agreed, thequeueMicrotask(throw)was a foot-gun. Return union now has an explicit{ error: unknown }variant; the rethrow is gone. The caller emitssystem { subtype: "cursor_stream_error", details: { error, agent_id, run_id } }and falls through to the cancelled-result path sotask_historystays terminal. event-log renderscursor_stream_erroras a visible error row (same shape asapi_retry/cursor_stalled).Stream cleanup — agreed.
streamRunWithStallAndAbort'sfinallynow callsawait stream.return(undefined).catch(() => {})alongsidedisarmStallTimer(), regardless of exit path.cancelWithTimeout(run)is still wired fromrunTask.onAbortfor the run-level cancel; this closes the per-call generator handle.5 new tests pin:
iter.next()rejection →cursor_stream_error+cancelledresultunhandledRejection/uncaughtExceptionstream.return()is called exactly once on each of {normal completion, cancel, stream error}just qaclean (3316 pass).Both round-1 findings resolved:
nextOrAbortnow returns{ error: err }(noqueueMicrotaskrethrow);streamRunWithStallAndAbortfinally block closes the stream generator viastream.return(undefined).catch(() => {}). Tests cover the error path and verify no uncaught exception escapes. CI green.