Repository navigation
fix(claude-code): interrupt a written steer's native turn when its Turn is cancelled - #690
Conversation
…rn is cancelled Claude Code keeps a steer written with `priority: "next"` across an interrupt: after the interrupted Root's result it still runs the steer as its next native turn. A running (non-held) Turn with such an undrained steer took the plain abort path and completed at the Root result, so that native turn leaked: the next Desktop Turn received the stopped steer's reply (and every later Turn answered the previous message), or the steer surfaced as an autonomous Turn that made Desktop's next turn/start fail with "already has an active Turn". Take the cancellation path whenever a written steer awaits its native turn, held or not, so the Turn stays open, interrupts that native turn when it starts, and only then completes as cancelled. Fixes BytePioneer-AI#688
|
Caution Review failedThe pull request is closed. No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
处理待接管 steer 的取消 packages/adapters/claude-code/src/claude-code-adapter.ts, packages/adapters/claude-code/test/claude-code-adapter.test.ts |
适配器在待处理 steer 存在时调用后台工作取消流程。测试验证原 Root 以 aborted_tools 结束后,原 Turn 保持打开并阻止新 Turn;延续启动后被取消,过期文本不改变取消结果。原 Turn 完成后,同一 Session 可成功运行新 Turn,且不会发出 turn.autonomous.started。 |
Estimated code review effort: 2 (Simple) | ~10 minutes
Change: Bug fix · Severity of issue fixed: Medium
Possibly related issues
BytePioneer-AI/codex-host的 issue 688 报告连续输入后请求持续排队。本 PR 测试覆盖取消已接受的 steer 后,原 Turn 完成且新 Turn 可启动的场景。
Possibly related PRs
- BytePioneer-AI/codex-host#617:引入待处理 steer 跟踪及相关 Turn 生命周期;本 PR 调整该生命周期中的取消条件。
- BytePioneer-AI/codex-host#488:提供后台任务及 Root continuation 的取消流程;本 PR 在待处理 steer 场景中调用该流程。
- BytePioneer-AI/codex-host#572:建立
turn.steer的写入和接受语义;本 PR 覆盖 steer 已接受但尚未接管时的取消行为。
Suggested labels: 范围:适配器, 适配器:Claude Code
Suggested reviewers: bwndlct, bytepioneer-ai
Merge Risk: 
Minimal · previous review c72b3
-
Cancellation keeps the Turn open while written steers are pending. No material merge-blocking risk was established.
Pre-merge checks: 
4
-
Full details: Title check
-
Explanation
标题“fix(claude-code): interrupt a written steer's native turn when its Turn is cancelled”与变更内容直接相关。变更使 Claude Code 适配器在存在已写入但未被 drain 的 steer 时,走既有取消路径,并在该 steer 的原生 turn 开始时中断它。标题准确描述了主要修复点。标题长度为 84 个字符,略长,但仍可读。
Full details: Description check
-
Explanation
描述与变更集直接相关。它说明了问题根因(ClaudeHarnessSession.#cancel 仅对 held Turn 处理未 drain 的 steer)、修复方式(有未 drain steer 时无论 Turn 是否 held 都走既有取消路径)、新增回归测试、验证步骤和端到端复现结果,并关联 Fixes
#688。描述还列出了未验证的范围和取舍(约一个 continuation 安静期的额外等待)。
Full details: Linked Issues check
-
Explanation
[
#688] 直接关联 Issue 只有现象描述:连续输入后转圈,停止后新输入堆积在队列中。Issue 未给出具体验收标准(Expected behavior、Reproduction 均为“如题”)。因此本评估以该现象和 PR 声明的根因作为要求基准。实现证据:在
ClaudeHarnessSession.#cancel(packages/adapters/claude-code/src/claude-code-adapter.ts,约第 1602 行)中,条件由active.held && (... || steersAwaitingNative.size > 0)改为active.steersAwaitingNative.size > 0 || (active.held && (rootSegmentActive || awaitingContinuation))。因此,只要存在已写入但未被 Claude drain 的 steer,未 held 的 Turn 也会走已有的取消路径(#cancelBackgroundWork),不再直接 abort。这阻止了 steer 的原生 turn 泄漏到下一个 Turn,与 Issue 描述的“输入堆积、新输入无法进行”路径相符。回归测试:新增
keeps a cancelled Turn open to interrupt the native turn of its written steer。该测试验证:Root 以aborted_tools结束后 Turn 保持打开;新 Turn 返回sessionBusy;steer 原生 turn 开始后被中断;Turn 结束为 cancelled;turn-2 只收到自己的回复;不产生turn.autonomous.started。端到端:PR 描述给出真实 AppServerHost 与 Claude Code CLI 2.1.316 的复现对比(200ms 与 1600ms 间隔),修复后错位和
-32072拒绝均消失。未覆盖:未在 macOS Codex Desktop 26.1023 UI 中验证;未使用真实 Anthropic API 延迟。Issue 未提供原始复现步骤,因此“PR 修复的路径即为 Issue 报告的路径”是基于现象一致性的推断,不是 Issue 原始日志的直接证明。上述内容不影响编码要求的判定。
Full details: Out of Scope Changes check
-
Explanation
变更范围仅包含两部分:
claude-code-adapter.ts中#cancel的一个条件调整(约 4 行,含一条注释)。该调整直接对应 PR 目标:带已写入未 drain steer 的取消必须进入已有取消路径。claude-code-adapter.test.ts中一个新增回归测试(+79 行)。该测试直接验证上述行为,属于支持性测试,与目标直接相关。
未发现与 PR 目标无关的修改。无 steer 的普通停止路径未改变,符合 PR 描述中的范围说明。PR 未修改导出类型声明。
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Summary
Claude Code 外部 Thread 中,运行中追加的输入会以原生 steer(
priority: "next")写给 Claude Code(#572 引入)。如果随后立刻点击停止,Claude Code 2.1.316 会先以error_during_execution结束被中断的 Root,然后仍把这条已写入的 steer 作为下一个原生 turn 执行(command_lifecycle started+ 真实 API 请求)。问题在
ClaudeHarnessSession.#cancel(packages/adapters/claude-code/src/claude-code-adapter.ts):只有 held Turn 在有未 drain 的 steer 时才走「等待并中断 steer 原生 turn」的取消路径;正在运行(非 held)的 Turn 走普通transport.abort(),在 Root result 到达时就结束了。于是这条 steer 的原生 turn 泄漏出去:turn/start被拒绝为-32072 External Thread already has an active Turn,输入只能堆在队列里。这与 #688 的现象一致(连续输入 → 停止 → 新输入一直转圈 / 队列堆积)。
修复:只要还有已写入但未被 Claude drain 的 steer(
steersAwaitingNative.size > 0),无论 Turn 是否 held,都走已有的取消路径。现有机制会在该 steer 的原生 turn 开始时中断它(#takeOverSteer→segment.started→#interruptCancelledSegment),安静期后才把 Turn 结束为 cancelled;steer 若从未开始,则由已有的 release / 取消升级(关闭进程)兜底。没有 steer 的普通停止路径不变。代价:带未 drain steer 的停止会多等约一个 continuation 安静期(默认 2s),与 held Turn 现有行为一致。
Related issues
Fixes #688
Test plan
新增回归测试
keeps a cancelled Turn open to interrupt the native turn of its written steer(packages/adapters/claude-code/test/claude-code-adapter.test.ts),按真实 CLI 的事件顺序建模:steer 被接受 → 取消 → Root result → steer 的原生 turn 开始并被中断 → Turn 才结束;期间新 Turn 返回sessionBusy,之后 turn-2 只收到自己的回复,且没有turn.autonomous.started。npx vitest run packages/adapters/claude-code/test/claude-code-adapter.test.ts -t "keeps a cancelled Turn open"AssertionError: expected { ok: true, … } to match object { ok: false, … },即被取消的 Turn 在 Root result 时就结束,turn-2 抢占了仍欠着 steer 原生 turn 的流)npx vitest run packages/adapters/claude-code:27 files passed / 1 skipped,471 tests passed / 3 skippednpx vitest run packages/host-runtime/test/app-server-host.claude.real.test.ts packages/host-runtime/test/external-thread-claude-continuation.test.ts packages/host-runtime/test/app-server-host.native-steering.test.ts packages/host-runtime/test/external-turn-steering.test.ts:4 files passed,49 passed / 4 skippednpx eslint packages/adapters/claude-code/src/claude-code-adapter.ts packages/adapters/claude-code/test/claude-code-adapter.test.ts:通过npx prettier --check(同上两个文件):通过npx tsc -b:通过端到端复现(Linux arm64,无真实 Anthropic 凭据):真实
AppServerHost+ 真实ClaudeCodeAdapter+ 真实 Claude Code CLI 2.1.316(npm i @anthropic-ai/claude-code@2.1.316),ANTHROPIC_BASE_URL指向本地假 Messages 端点(SSE;以tool5开头的提示返回Bash sleep 5tool_use,否则回ECHO: <text>),隔离HOME/CLAUDE_CONFIG_DIR,通过 Desktop JSON-RPC 驱动:ECHO: ljdasjlkdsa,"third" 回合收到ECHO: dadhaks(持续错位)ECHO: dadhaks,"third" →ECHO: thirdljdasjlkdsa;turn/start "dadhaks"返回-32072 External Thread already has an active Turnturn/start "dadhaks"正常并收到ECHO: dadhaks另外验证:不带 steer、在 0s(冷/热 transport)直接停止再发新消息,修复前就正常(该路径本 PR 未改动),说明问题只出在「已写入 steer + 停止」。
未验证:真实 macOS Codex Desktop 26.1023 UI(队列条 / Thinking 渲染)、真实 Anthropic API 时延下的表现。CLI 也支持
interrupt的cancel_queued: true(capabilityinterrupt_cancel_queued_v1)/cancel_async_message,但 SDK 0.3.273 的类型未声明,本 PR 未采用。