Skip to content

fix(claude-code): interrupt a written steer's native turn when its Turn is cancelled - #690

Merged
BytePioneer-AI merged 2 commits into
BytePioneer-AI:mainfrom
YanYunCY:fix/claude-cancel-written-steer
Nov 1, 2026
Merged

BytePioneer-AI merged 2 commits into
BytePioneer-AI:mainfrom
YanYunCY:fix/claude-cancel-written-steer

Conversation

@YanYunCY

Copy link
Copy Markdown
Contributor

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 泄漏出去:

  • 用户很快发下一条消息时:下一个 Desktop Turn 收到的是被停止的 steer 的回复,之后每一轮都在回答上一条消息(错位);
  • 用户稍晚发送时:steer 以 autonomous Turn 出现(UI 上是一个莫名的「Thinking」回合),Desktop 的 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 skipped
  • npx 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 skipped
  • npx 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 5 tool_use,否则回 ECHO: <text>),隔离 HOME / CLAUDE_CONFIG_DIR,通过 Desktop JSON-RPC 驱动:

turn/start "tool5" → 等 commandExecution 开始 → turn/steer "ljdasjlkdsa"(delivery: activeTurn)
→ 100ms 后 turn/interrupt → 等 turn/completed(interrupted) → 等 200ms 或 1600ms → turn/start "dadhaks" → turn/start "third"
间隔 修复前 修复后
200ms "dadhaks" 回合收到 ECHO: ljdasjlkdsa,"third" 回合收到 ECHO: dadhaks(持续错位) "dadhaks" → ECHO: dadhaks,"third" → ECHO: third
1600ms 出现 autonomous 回合回答 ljdasjlkdsa;turn/start "dadhaks" 返回 -32072 External Thread already has an active Turn 无 autonomous 回合;turn/start "dadhaks" 正常并收到 ECHO: dadhaks

另外验证:不带 steer、在 0s(冷/热 transport)直接停止再发新消息,修复前就正常(该路径本 PR 未改动),说明问题只出在「已写入 steer + 停止」。

未验证:真实 macOS Codex Desktop 26.1023 UI(队列条 / Thinking 渲染)、真实 Anthropic API 时延下的表现。CLI 也支持 interrupt 的 cancel_queued: true(capability interrupt_cancel_queued_v1)/ cancel_async_message,但 SDK 0.3.273 的类型未声明,本 PR 未采用。

…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
@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Caution

Review failed

The pull request is closed.

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 6afde08 and c72b32d.

Files selected for processing: 2
  • packages/adapters/claude-code/src/claude-code-adapter.ts
  • packages/adapters/claude-code/test/claude-code-adapter.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Recent review details
Context from checks skipped due to timeout: 5
  • GitHub Check: CodeRabbit
  • GitHub Check: Check Linux ARM64
  • GitHub Check: Check windows-latest
  • GitHub Check: Check ubuntu-22.04
  • GitHub Check: Check macos-14
Additional context used
Code guidelines: 1
AGENTS.md — auto-discovered
Path-based instructions: 1
Source excerpt: `codexhost` runs external Agent Harnesses as independent Threads inside Codex Desktop, preserving the official shell and native Codex path.

CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/adapters/claude-code/src/claude-code-adapter.ts
  • packages/adapters/claude-code/test/claude-code-adapter.test.ts
Learnings: 1
Common learnings
Learnt from: CR
Repo: BytePioneer-AI/codex-host PR: 0
File: .agents/skills/codexhost-pre-push-checks/SKILL.md:0-0
Timestamp: 2026-10-31T06:53:01.023Z
Learning: Applicability reference:
AGENTS.md: # Repository Guidelines
## Repository Skills
| Task | Skill |
| --- | --- |
| Selecting local checks before pushing or claiming checks pass | [codexhost-pre-push-checks](.agents/skills/codexhost-pre-push-checks/SKILL.md) |

Source excerpt:
# codexhost 推送前检查

## 按改动面选择

规则:每个行为改动至少有一个“回归时会失败”的测试或检查;没有现成的就补一个或说明缺口。不要因为即将提交就重复跑刚通过的检查。
Additional comments: 2
packages/adapters/claude-code/src/claude-code-adapter.ts: 1

1605-1608: LGTM!

packages/adapters/claude-code/test/claude-code-adapter.test.ts: 1

1355-1432: LGTM!

Summary

Summary by CodeRabbit

  • Bug Fixes
    • 修复取消处理:即使 Turn 尚未进入保持状态,取消也会等待并处理已接受但尚未启动的 steer。
    • 取消延续任务后,过期响应不会再影响当前 Turn 或被误发布为自主 Turn;后续 Turn 可正常启动和完成。
  • Tests
    • 补充了相关取消场景的测试。

当存在待接管的 steer 时,取消请求现在会进入后台工作取消流程。新增测试覆盖原生延续被取消、原 Turn 完成,以及同一 Session 后续 Turn 成功运行的过程。

Changes: 待处理 steer 取消

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

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: Passed 4
Check name Status Explanation
Title check Passed 标题“fix(claude-code): interrupt a written steer's native turn when its Turn is cancelled”与变更内容直接相关。变更使 Claude Code 适配器在存在已写入但未被 drain 的 steer 时,走既有取消路径,并在该 steer 的原生 turn 开始时中断它。标题准确描述了主要修复点。标题长度为 84 个…
Description check Passed 描述与变更集直接相关。它说明了问题根因(ClaudeHarnessSession.#cancel 仅对 held Turn 处理未 drain 的 steer)、修复方式(有未 drain steer 时无论 Turn 是否 held 都走既有取消路径)、新增回归测试、验证步骤和端到端复现结果,并关联 Fixes #688。描述还列出了未验证的范围和取舍(约一个 continuation 安静期的…
Linked Issues check Passed [#688] 直接关联 Issue 只有现象描述:连续输入后转圈,停止后新输入堆积在队列中。Issue 未给出具体验收标准(Expected behavior、Reproduction 均为“如题”)。因此本评估以该现象和 PR 声明的根因作为要求基准。

实现证据:在 ClaudeHarnessSession.#cancel(`packages/adapters/claude-code/sr…
Out of Scope Changes check Passed 变更范围仅包含两部分:
1. claude-code-adapter.ts 中 #cancel 的一个条件调整(约 4 行,含一条注释)。该调整直接对应 PR 目标:带已写入未 drain steer 的取消必须进入已有取消路径。
2. claude-code-adapter.test.ts 中一个新增回归测试(+79 行)。该测试直接验证上述行为,属于支持性测试,与目标直接相关。

…
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 &amp;&amp; (... || steersAwaitingNative.size &gt; 0) 改为 active.steersAwaitingNative.size &gt; 0 || (active.held &amp;&amp; (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

变更范围仅包含两部分:

  1. claude-code-adapter.ts 中 #cancel 的一个条件调整(约 4 行,含一条注释)。该调整直接对应 PR 目标:带已写入未 drain steer 的取消必须进入已有取消路径。
  2. 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.

Share

@BytePioneer-AI
BytePioneer-AI merged commit dc7bef3 into BytePioneer-AI:main Nov 1, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants