From 0bb0335b909387b981f6b0d3170de67897c51ff7 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Tue, 20 Oct 2026 20:36:38 -0500 Subject: [PATCH 1/3] fix(cursor): keep failed plans from becoming actionable A createPlan call that Cursor finished with an error already marked its plan node and turn item failed, but the plan artifact still went active. The thread then showed "Plan Ready" and the implement-plan prompt, and the server accepted the failed plan as an implementation source. Only a plan Cursor accepted becomes active now; a failed one stays a draft, so its markdown and failed row remain visible. Co-Authored-By: Claude Opus 5.5 --- .../src/server/adapter.test.ts | 34 ++++++++++++++++++- .../provider-cursor/src/server/adapter.ts | 2 +- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/provider-cursor/src/server/adapter.test.ts b/packages/provider-cursor/src/server/adapter.test.ts index a297af469cf4..f6c44690f580 100644 --- a/packages/provider-cursor/src/server/adapter.test.ts +++ b/packages/provider-cursor/src/server/adapter.test.ts @@ -512,7 +512,7 @@ describe("CursorAdapterV2", () => { ), ); - it.effect("projects Cursor directory trees and lint diagnostics as file search results", () => + it.effect("projects Cursor directory trees, lint diagnostics and plans", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -717,6 +717,26 @@ describe("CursorAdapterV2", () => { result: { status: "error", error: "lint failed" }, }, }, + { + type: "tool-call-completed", + modelCallId: "native-model-call", + callId: "plan-created", + toolCall: { + type: "createPlan", + args: { plan: "Ship the fix." }, + result: { status: "success", value: {} }, + }, + }, + { + type: "tool-call-completed", + modelCallId: "native-model-call", + callId: "plan-rejected", + toolCall: { + type: "createPlan", + args: { plan: "Rejected plan." }, + result: { status: "error", error: "plan failed" }, + }, + }, ]; const adapter = yield* makeCursorAdapterV2({ instanceId, @@ -907,6 +927,18 @@ describe("CursorAdapterV2", () => { }, ], ); + // Only a plan Cursor accepted can be implemented. + assert.deepEqual( + events.flatMap((event) => + event.type === "plan.updated" && event.plan.kind === "proposed_plan" + ? [{ markdown: event.plan.markdown, status: event.plan.status }] + : [], + ), + [ + { markdown: "Ship the fix.", status: "active" }, + { markdown: "Rejected plan.", status: "draft" }, + ], + ); }).pipe( Effect.scoped, Effect.provide( diff --git a/packages/provider-cursor/src/server/adapter.ts b/packages/provider-cursor/src/server/adapter.ts index 9ff5ba75f466..3cb4f8a41e95 100644 --- a/packages/provider-cursor/src/server/adapter.ts +++ b/packages/provider-cursor/src/server/adapter.ts @@ -1401,7 +1401,7 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( runId: input.context.input.runId, nodeId, kind: "proposed_plan", - status: input.completed ? "active" : "draft", + status: status === "completed" ? "active" : "draft", markdown: input.markdown, }; yield* emitProviderEvent({ From 5ea0a4983e252e139210e8417318e6db2330df1b Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Wed, 21 Oct 2026 08:28:48 -0500 Subject: [PATCH 2/3] fix(cursor): retire failed proposals and cover their lifecycle Recover the focused plan lifecycle test and superseded status from lane commits 6b478d3025 and 09fe3f2ea0 verified during the outage. Replace the mixed diagnostics test addition with the dedicated protocol regression. Co-Authored-By: GPT-6.1 Sol --- .../src/server/adapter.test.ts | 190 +++++++++++++++--- .../provider-cursor/src/server/adapter.ts | 2 +- 2 files changed, 158 insertions(+), 34 deletions(-) diff --git a/packages/provider-cursor/src/server/adapter.test.ts b/packages/provider-cursor/src/server/adapter.test.ts index f6c44690f580..3efc7b267fb3 100644 --- a/packages/provider-cursor/src/server/adapter.test.ts +++ b/packages/provider-cursor/src/server/adapter.test.ts @@ -40,6 +40,162 @@ import { ConfigurationError } from "./sdk.ts"; const decodeCursorSettings = Schema.decodeEffect(CursorSettings); describe("CursorAdapterV2", () => { + it.effect.each([true, false])( + "keeps a completed Cursor plan actionable only when it succeeds (failed: %s)", + (failed) => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const workspace = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "cursor-v2-plan-", + }); + const instanceId = ProviderInstanceId.make("cursor"); + const threadId = ThreadId.make("cursor-plan-thread"); + const modelSelection = { instanceId, model: "composer-2.5" }; + const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: workspace, + }); + const adapter = yield* makeCursorAdapterV2({ + instanceId, + settings: yield* decodeCursorSettings({}), + environment: { HOME: workspace }, + }).pipe( + Effect.provideService(CursorAgentSdk.CursorAgentSdkRunner, { + assertComplete: Effect.void, + open: () => + Effect.succeed({ + agentId: "native-cursor-plan", + listMessages: Effect.succeed([]), + close: Effect.void, + send: (input) => + Effect.gen(function* () { + const toolCall = { + type: "createPlan" as const, + args: { plan: "# Plan\n\nKeep the failed proposal for inspection." }, + }; + yield* input.onDelta!({ + type: "tool-call-started", + modelCallId: "model-call", + callId: "plan-call", + toolCall, + }).pipe(Effect.orDie); + yield* input.onDelta!({ + type: "tool-call-completed", + modelCallId: "model-call", + callId: "plan-call", + toolCall: { + ...toolCall, + result: failed + ? { status: "error", error: "Plan could not be saved" } + : { status: "success", value: {} }, + }, + }).pipe(Effect.orDie); + return { + agentId: "native-cursor-plan", + runId: "native-cursor-run", + wait: Effect.succeed({ + id: "native-cursor-run", + requestId: "native-request", + status: "finished" as const, + model: { id: "composer-2.5" }, + durationMs: 1, + }), + cancel: Effect.void, + }; + }), + }), + }), + Effect.provide(TestProviderHost.layer({ cwd: workspace })), + ); + const runtime = yield* adapter.openSession({ + threadId, + providerSessionId: ProviderSessionId.make("cursor-plan-session"), + modelSelection, + runtimePolicy, + }); + const providerThread = yield* runtime.ensureThread({ + threadId, + modelSelection, + runtimePolicy, + }); + const now = yield* DateTime.now; + yield* runtime.startTurn({ + threadId, + providerThread, + modelSelection, + runtimePolicy, + runId: RunId.make("cursor-plan-run"), + runOrdinal: 1, + providerTurnOrdinal: 1, + attemptId: RunAttemptId.make("cursor-plan-attempt"), + rootNodeId: NodeId.make("cursor-plan-root"), + appThread: { + id: threadId, + projectId: ProjectId.make("cursor-plan-project"), + createdBy: "user", + creationSource: "web", + title: "Cursor plan", + providerInstanceId: instanceId, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: providerThread.id, + lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId }, + forkedFrom: null, + createdAt: now, + updatedAt: now, + archivedAt: null, + settledOverride: null, + settledAt: null, + lastVisitedAt: null, + deletedAt: null, + }, + message: { + messageId: MessageId.make("cursor-plan-message"), + createdBy: "user", + creationSource: "web", + text: "Propose a plan.", + attachments: [], + }, + }); + const events = yield* runtime.events.pipe( + Stream.takeUntil((event) => event.type === "turn.terminal"), + Stream.runCollect, + ); + const plans = events.filter((event) => event.type === "plan.updated"); + assert.equal(plans[0]?.plan.status, "draft"); + assert.equal(plans.at(-1)?.plan.id, plans[0]?.plan.id); + assert.equal(plans.at(-1)?.plan.status, failed ? "superseded" : "active"); + const plan = plans.at(-1)?.plan; + assert.equal( + plan?.kind === "proposed_plan" ? plan.markdown : null, + "# Plan\n\nKeep the failed proposal for inspection.", + ); + const item = events.findLast( + (event) => event.type === "turn_item.updated" && event.turnItem.type === "proposed_plan", + ); + assert.equal( + item?.type === "turn_item.updated" ? item.turnItem.status : null, + failed ? "failed" : "completed", + ); + const node = events.findLast( + (event) => event.type === "node.updated" && event.node.kind === "plan", + ); + assert.equal( + node?.type === "node.updated" ? node.node.status : null, + failed ? "failed" : "completed", + ); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll(NodeServices.layer, IdAllocator.layer, McpProviderSessions.layer), + ), + ), + ); + it.effect.each([ { status: "finished", model: undefined, lateModel: undefined }, { status: "cancelled", model: "claude-opus-4-6", lateModel: undefined }, @@ -512,7 +668,7 @@ describe("CursorAdapterV2", () => { ), ); - it.effect("projects Cursor directory trees, lint diagnostics and plans", () => + it.effect("projects Cursor directory trees and lint diagnostics as file search results", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -717,26 +873,6 @@ describe("CursorAdapterV2", () => { result: { status: "error", error: "lint failed" }, }, }, - { - type: "tool-call-completed", - modelCallId: "native-model-call", - callId: "plan-created", - toolCall: { - type: "createPlan", - args: { plan: "Ship the fix." }, - result: { status: "success", value: {} }, - }, - }, - { - type: "tool-call-completed", - modelCallId: "native-model-call", - callId: "plan-rejected", - toolCall: { - type: "createPlan", - args: { plan: "Rejected plan." }, - result: { status: "error", error: "plan failed" }, - }, - }, ]; const adapter = yield* makeCursorAdapterV2({ instanceId, @@ -927,18 +1063,6 @@ describe("CursorAdapterV2", () => { }, ], ); - // Only a plan Cursor accepted can be implemented. - assert.deepEqual( - events.flatMap((event) => - event.type === "plan.updated" && event.plan.kind === "proposed_plan" - ? [{ markdown: event.plan.markdown, status: event.plan.status }] - : [], - ), - [ - { markdown: "Ship the fix.", status: "active" }, - { markdown: "Rejected plan.", status: "draft" }, - ], - ); }).pipe( Effect.scoped, Effect.provide( diff --git a/packages/provider-cursor/src/server/adapter.ts b/packages/provider-cursor/src/server/adapter.ts index 3cb4f8a41e95..8558b2c7f2f4 100644 --- a/packages/provider-cursor/src/server/adapter.ts +++ b/packages/provider-cursor/src/server/adapter.ts @@ -1401,7 +1401,7 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( runId: input.context.input.runId, nodeId, kind: "proposed_plan", - status: status === "completed" ? "active" : "draft", + status: input.failed ? "superseded" : input.completed ? "active" : "draft", markdown: input.markdown, }; yield* emitProviderEvent({ From b91e125fc0929d22a9c5be038efac345e0cad789 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 1 Nov 2026 03:11:30 +0000 Subject: [PATCH 3/3] fix(cursor): update plan regression for current adapter options --- packages/provider-cursor/src/server/adapter.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/provider-cursor/src/server/adapter.test.ts b/packages/provider-cursor/src/server/adapter.test.ts index 69195fb1f151..d878f216ca84 100644 --- a/packages/provider-cursor/src/server/adapter.test.ts +++ b/packages/provider-cursor/src/server/adapter.test.ts @@ -54,7 +54,6 @@ describe("CursorAdapterV2", () => { }); const adapter = yield* makeCursorAdapterV2({ instanceId, - settings: yield* decodeCursorSettings({}), environment: { HOME: workspace }, }).pipe( Effect.provideService(CursorAgentSdk.CursorAgentSdkRunner, {