Skip to content

Commit 11045ff

Browse files
committed
fix(cursor): preserve declined steer selection and attachments
1 parent 265976c commit 11045ff

8 files changed

Lines changed: 43 additions & 9 deletions

File tree

‎apps/server/src/orchestration-v2/EffectOutbox.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const OrchestrationEffectRequestV2 = Schema.Union([
6464
providerThreadId: ProviderThreadId,
6565
providerTurnId: ProviderTurnId,
6666
messageId: MessageId,
67+
modelSelection: Schema.optional(ModelSelection),
6768
}),
6869
Schema.Struct({
6970
type: Schema.Literal("provider-turn.restart"),

‎apps/server/src/orchestration-v2/EffectWorker.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,11 @@ export const layerExecutor: Layer.Layer<
314314
text: message.text,
315315
...(message.context ? { context: message.context } : {}),
316316
attachments: message.attachments,
317-
...(message.delegatedCompletion === undefined
318-
? {}
319-
: { modelSelection: run.modelSelection }),
317+
...(error.steerDeclined && effect.request.modelSelection !== undefined
318+
? { modelSelection: effect.request.modelSelection }
319+
: message.delegatedCompletion === undefined
320+
? {}
321+
: { modelSelection: run.modelSelection }),
320322
dispatchMode: {
321323
type: queued ? "queue_after_active" : "start_immediately",
322324
},

‎apps/server/src/orchestration-v2/Orchestrator.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5639,6 +5639,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
56395639
providerThreadId: providerThread.id,
56405640
providerTurnId: providerTurn.id,
56415641
messageId: input.messageId,
5642+
modelSelection: input.modelSelection,
56425643
},
56435644
} satisfies PendingOrchestrationEffectV2,
56445645
]);

‎apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,7 +1153,7 @@ it.effect("starts a steer that missed the turn on the saved next-turn selection"
11531153
),
11541154
);
11551155

1156-
it.effect.each(["complete", "stop-before-decline", "stop-after-decline"] as const)(
1156+
it.effect.each(["complete", "stop-before-decline", "stop-after-decline", "queued-model"] as const)(
11571157
"persists a declined steer without blocking the worker: %s",
11581158
(timing) =>
11591159
Effect.scoped(
@@ -1194,6 +1194,21 @@ it.effect.each(["complete", "stop-before-decline", "stop-after-decline"] as cons
11941194
})
11951195
.pipe(Effect.asVoid, Effect.orDie);
11961196
if (timing === "stop-before-decline") beforeDecline = stop;
1197+
const steerSelection = { ...runSelection, model: "steer-model" };
1198+
if (timing === "queued-model") {
1199+
yield* orchestrator.dispatch({
1200+
type: "message.dispatch",
1201+
commandId: CommandId.make("queued-model"),
1202+
threadId,
1203+
messageId: MessageId.make("message:queued-model"),
1204+
text: "already queued",
1205+
attachments: [],
1206+
modelSelection: composerSelection,
1207+
dispatchMode: { type: "queue_after_active" },
1208+
createdBy: "user",
1209+
creationSource: "web",
1210+
});
1211+
}
11971212
const messageId = MessageId.make("message:declined");
11981213
yield* orchestrator.dispatch({
11991214
type: "message.dispatch",
@@ -1202,13 +1217,16 @@ it.effect.each(["complete", "stop-before-decline", "stop-after-decline"] as cons
12021217
messageId,
12031218
text: "follow up",
12041219
attachments: [],
1220+
...(timing === "queued-model" ? { modelSelection: steerSelection } : {}),
12051221
dispatchMode: { type: "steer_active", targetRunId: first.runId },
12061222
createdBy: "user",
12071223
creationSource: "web",
12081224
});
12091225
yield* worker.drain();
12101226
const pending = yield* orchestrator.getThreadProjection(threadId);
1211-
const queued = pending.runs.find((run) => run.id !== first.runId);
1227+
const queued = pending.runs.find(
1228+
(run) => run.userMessageId === messageId && run.id !== first.runId,
1229+
);
12121230
const effects = yield* (yield* EffectOutbox.EffectOutboxV2).listByCommandId(
12131231
CommandId.make("steer"),
12141232
);
@@ -1223,6 +1241,10 @@ it.effect.each(["complete", "stop-before-decline", "stop-after-decline"] as cons
12231241
}
12241242
assert.equal(queued?.status, "queued");
12251243
assert.equal(queued?.userMessageId, messageId);
1244+
if (timing === "queued-model") {
1245+
assert.deepEqual(queued?.modelSelection, steerSelection);
1246+
return;
1247+
}
12261248
if (timing === "stop-after-decline") {
12271249
yield* stop;
12281250
yield* worker.drain();

‎docs/user/cursor.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ The adapter supports:
5555
final result.
5656

5757
The public SDK does not currently expose native agent fork, conversation rollback, or interactive
58-
approval callbacks. A steer injects into the live run with `Run.steer` and leaves that run in place.
59-
When the SDK declines the injection, the same message is sent as the next turn after this run
60-
finishes. A provider or runtime change that has to apply immediately still interrupts and restarts the run.
58+
approval callbacks. A text-only steer injects into the live run with `Run.steer` and leaves that run
59+
in place. When the SDK declines the injection, the same message is queued for the next turn after
60+
this run finishes. If you press Stop, that follow-up stays held until you resume the queue. Steers
61+
with attachments and provider or runtime changes that must apply immediately interrupt and restart
62+
the run.
6163
Same-provider Cursor forks use the orchestrator's portable full-thread context handoff into a fresh
6264
Cursor agent. The SDK has in-process custom callback tools, but the V2 adapter intentionally uses the
6365
authenticated, thread-scoped MCP server instead.

‎packages/provider-cursor/src/server/CursorAgentSdk.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ export const makeCursorAgentSdkRunner = Effect.fn("makeCursorAgentSdkRunner")(fu
584584
}),
585585
),
586586
),
587-
steer: (text) =>
587+
steer: Effect.fn("CursorAgentSdkRun.steer")((text: string) =>
588588
log({
589589
direction: "outgoing",
590590
stage: "decoded",
@@ -612,6 +612,7 @@ export const makeCursorAgentSdkRunner = Effect.fn("makeCursorAgentSdkRunner")(fu
612612
}),
613613
),
614614
),
615+
),
615616
cancel: log({
616617
direction: "outgoing",
617618
stage: "decoded",

‎packages/provider-cursor/src/server/adapter.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2097,6 +2097,10 @@ describe("CursorAdapterV2", () => {
20972097
modelSelection,
20982098
runtimePolicy,
20992099
});
2100+
assert.equal(
2101+
runtime.providerSession.capabilities.turns.activeSteeringAcceptsAttachments,
2102+
false,
2103+
);
21002104
const providerThread = yield* runtime.ensureThread({
21012105
threadId,
21022106
modelSelection,

‎packages/provider-cursor/src/server/adapter.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export const CursorProviderCapabilitiesV2 = {
100100
// A steer can move a foreground subagent to the background. Mailbox wakes
101101
// stay queued so a notification does not cut into the live turn.
102102
activeSteeringInterruptsTools: true,
103+
activeSteeringAcceptsAttachments: false,
103104
supportsSteeringByInterruptRestart: true,
104105
supportsQueuedMessages: true,
105106
terminalStatusQuality: "strong",

0 commit comments

Comments
 (0)