From ebb0000d9a52c366184c9fdba9743df9b4493467 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:12:21 -0700 Subject: [PATCH] fix(server): a Cursor command you stop shows as interrupted When a turn is stopped mid-tool, Cursor never sends tool-call-completed for the running command. finalizeTurn closed those tools as "completed", so the cut-short command looked like it finished. Unfinished tools now take the turn's terminal status (interrupted, failed, or cancelled), as Pi and Codex already report them. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../orchestration-v2/Adapters/CursorAdapterV2.ts | 14 ++++++++++++-- .../turn_interrupt_mid_tool/cursor_output.ts | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts index df0f4043cb09..20a6b5280b4c 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts @@ -1165,12 +1165,16 @@ export function makeCursorAdapterV2( const emitToolArtifacts = Effect.fnUntraced(function* (input: { readonly active: ActiveCursorToolCall; readonly completed: boolean; + /** Terminal status for a tool the turn ended before Cursor completed it. */ + readonly unfinishedStatus?: "interrupted" | "failed" | "cancelled"; }) { const { active } = input; const toolCall = active.toolCall; const now = yield* DateTime.now; const failed = input.completed && cursorToolFailed(toolCall); - const status = input.completed ? (failed ? "failed" : "completed") : "running"; + const status = !input.completed + ? "running" + : (input.unfinishedStatus ?? (failed ? "failed" : "completed")); const nodeId = idAllocator.derive.nodeFromProviderItem({ driver: CURSOR_PROVIDER, nativeItemId: active.callId, @@ -1968,8 +1972,14 @@ export function makeCursorAdapterV2( } input.context.finalized = true; const completedAt = yield* DateTime.now; + // Tools still here never got a tool-call-completed. A stopped or + // failed turn cut them short, so they end with the turn's status. for (const tool of input.context.tools.values()) { - yield* emitToolArtifacts({ active: tool, completed: true }); + yield* emitToolArtifacts({ + active: tool, + completed: true, + ...(input.status === "completed" ? {} : { unfinishedStatus: input.status }), + }); } input.context.tools.clear(); // This interaction stops delivering updates at finalization. Tasks diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/cursor_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/cursor_output.ts index becc6006a970..ad4d01018d5d 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/cursor_output.ts +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/cursor_output.ts @@ -63,6 +63,7 @@ export function assertTurnInterruptMidToolCursorOutput( assert.isDefined(interruptRequest); assert.isDefined(interruptResult); assert.include(commandItem.input, "node -e"); + assert.equal(commandItem.status, "interrupted"); assert.equal(interruptRequest.status, "completed"); assert.equal(interruptResult.status, "interrupted"); assert.equal(interruptResult.parentItemId, interruptRequest.id);