fix(server): let Claude steer interrupt a running Bash command#23892
0 / 3 viewed3 files changedLine totals unavailable
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts Β· Earlier revision 133b3edd Β· right 9953β9957
Original diff
@@ -9917,24 +9917,46 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
fileSystem,
skills: yield* userInvocableSkills(currentTurn.input.runtimePolicy.cwd),
});
+ // The turn can end while the moves or the interrupt are answered. A
+ // steer offered then would start a new turn the orchestrator never
+ // asked for, so the steer fails and the orchestrator starts the
+ // follow-up turn.
+ const failIfTurnEnded = Effect.gen(function* () {
+ if (
+ (yield* Ref.get(activeTurn))?.providerTurnId !== turnInput.providerTurnId ||
+ (yield* Ref.get(queryContext)) !== existing
+ ) {
+ return yield* new ProviderAdapter.ProviderAdapterProtocolError({
+ driver: CLAUDE_PROVIDER,
+ detail: `Claude provider turn ${turnInput.providerTurnId} ended before the steer was sent.`,
+ });
+ }
+ });
yield* backgroundForegroundSubagents(existing, backgroundDeadline);
- // The turn can end while the moves are answered. A steer offered
- // then would start a new turn the orchestrator never asked for, so
- // the steer fails and the orchestrator starts the follow-up turn.
- if (
- (yield* Ref.get(activeTurn))?.providerTurnId !== turnInput.providerTurnId ||
- (yield* Ref.get(queryContext)) !== existing
- ) {
- return yield* new ProviderAdapter.ProviderAdapterProtocolError({
- driver: CLAUDE_PROVIDER,
- detail: `Claude provider turn ${turnInput.providerTurnId} ended before the steer was sent.`,
- });
- }
+ yield* failIfTurnEnded;
yield* Ref.update(steeredTurns, (current) => {
const next = new Set(current);
next.add(turnInput.providerTurnId);
return next;
});
+ // Priority "now" does not stop a foreground command (#15720), so a
+ // running Bash command is interrupted first, as Esc would. Its
+ // aborted_tools result ends only this segment of the steered turn.
+ // Interrupting also aborts pending permission callbacks, so a
+ // question or approval the user has not answered keeps the command.
+ if (
+ [...currentTurn.toolCalls.values()].some((call) => call.toolName === "Bash") &&
+ (yield* Ref.get(pendingRuntimeRequests)).size === 0
+ ) {
+ // The CLI may have aborted before the request failed, and that
+ // result is swallowed, so the steer is still sent to continue.
+ yield* existing.query.interrupt.pipe(
+ Effect.catch((cause) =>
+ Effect.logWarning("orchestration-v2.claude-steer-interrupt-failed", { cause }),
+ ),
+ );apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts Β· right 9953β9957