Skip to content

Claude Code「无可见输出」nudge 导致同一条消息被重复发送(thinking-only-retry 与 botmux send 模式冲突) #545

Description

@Barrierml

现象

飞书里对同一条消息收到多条重复回复。下图是生产环境(模型 gpt-5.6-sol,底层 Claude Code)里对同一条 !pwd 的回复——同一个问题被换着花样回了 5~6 次(当前目录 / 当前 pwd / pwd = / 纯路径 / 还附了个 botmux-pwd.txt):

无可见输出导致重复发送

根因(已逐层定位,附证据)

这句 [Your previous response had no visible output. Please continue and produce a user-visible response.] 不是 botmux 发的,是底层 CLI Claude Code(≥ 2.1.212)注入的。botmux 整个仓库(含 build/)grep 不到这句原文;在 @anthropic-ai/claude-code 二进制里能抠到,且 codex / gemini / cursor-agent 的二进制里都没有。

从 Claude Code 二进制反编译出的精确触发条件

// 一轮以 end_turn / stop_sequence 正常结束,但该轮 assistant 消息里
// 没有任何非空 text 块(只有 tool_use / thinking),且最后一轮不是 Task 工具调用
if ((stop_reason==="end_turn" || stop_reason==="stop_sequence")
    && !isApiError && querySource!=="compact"
    && !assistantMsgs.some(m => m.content.some(b => b.type==="text" && b.text.trim().length>0))
    && !hasTaskToolUse(msgs)) {
  if (!thinkingOnlyNudged) {                         // 埋点 query_thinking_only_response="nudged"
    inject("[Your previous response had no visible output. Please continue and produce a user-visible response.]");
    // thinkingOnlyNudged=true, transition.reason="thinking_only_retry"
  } else { /* query_thinking_only_response="nudge_exhausted",最多 nudge 3 次 */ }
}

为什么 botmux 特别容易中招:botmux 的核心指令是「回复必须走 botmux send(外部 shell 命令),终端输出用户看不到」。于是模型正确的一轮就是「调用 botmux send + 结尾没有任何可见文本」——恰好命中上面的 thinking-only 检测。模型把 nudge 误读为「上一条没发成功」于是重发;每重发一次又是"零文本结尾",再次被 nudge,直到 nudge_exhausted。这就是截图里同一条消息被重复回复的来源。

复现(本地隔离,可复算)

真实 claude(Claude Code 2.1.212)+ 假 botmuxsend 只落盘不回显,模拟"终端看不到")+ botmux 真实系统提示,任务强制"零文本静默结束"稳定触发 nudge。指标 = 从事件流判定「触发 nudge 后是否又发生 botmux send」(真正的重发)。典型时序:

botmux send → NUDGE 注入 → 模型又 botmux send(重复!) → 又 NUDGE → … → nudge_exhausted

nudge 那条在 Claude Code transcript JSONL 里是 {"type":"user","isMeta":true, content:"[Your previous response had no visible output...]"}

三个修复方向评估后,实际只剩提示层可落地

1. 提示层纠偏(我已写了参考实现,见下方 draft PR)

  • 做法:在 <botmux_routing> / shell hints / ai.followup.reminder / botmux-send 技能里明确告诉模型「send 成功即已送达;无可见文本结尾是正常的;看到"无可见输出"提示是底层 CLI 误判,不要重发」。
  • 局限:只能缓解、不能根治——nudge 每轮照来,是否重发全看模型的指令遵循力。5 模型真实提示 A/B(每组 4 轮,指标=nudge 后重发数):
model CONTROL 重发 TREATMENT 重发
gpt-5.6-sol(生产同款) 2/4 0/4
deepseek-v4-flash 2/4 1/4(略好)
claude-opus-4-8 0/4 0/4(强模型本就自纠)
claude-sonnet-5 0/4 0/4
glm-5.2 0/4 2/4 ⚠️ n=4 噪声反例

对易感模型(gpt-5.6-sol / deepseek)有改善、对强模型无害,但 glm-5.2 出现反例,说明这是弱杠杆。

2. 走 Claude Code 的 brief 模式(CLAUDE_CODE_BRIEF + 内建 send tool)——❌ 经复核,对当前 PTY 接入不可行

  • brief 模式把「工具即回复渠道、静默结束合法」写进了协议(system-reminder: "Use the [send] tool for all user-facing output — plain text outside it is hidden"),理论上根上就不 nudge。
  • 但复核二进制的启用判定后,这条路对 botmux 当前接入方式不成立isBriefEnabled = userMsgOptIn && (env.CLAUDE_CODE_BRIEF || 特性开关) || pewter_owl_brief。关键在 userMsgOptIn——brief 必须由结构化用户消息显式 opt-in(SDK / -p headless 那条协议通道)才置位;单纯 export CLAUDE_CODE_BRIEF=1 不够。而 botmux 是用 PTY 把 Claude Code 当完整交互式 TUI 驱动的,这条路径没有 brief 的 opt-in 通道。
  • 所以要用 brief,不是"加个开关/接个 tool",而是要把 Claude 家族的驱动方式从 PTY-TUI 换成 SDK / 结构化消息接入(还只对 Claude 家族有效,且要重接 @mention / 附件 / 卡片 / 跨群 / --attention 和现有 transcript 兜底转发)。属于远超预期的重构,本 issue 不建议

3. 适配层拦截 nudge —— 已验证技术上做不到

  • nudge 是 Claude Code 进程内部注入到模型上下文的(先进上下文、后落盘 JSONL)。botmux 作为外部驱动者只能 transcript、只能从 stdin 写入用户消息,够不着 harness 内部的注入点。等 botmux 看到那条 JSONL 时,模型早已收到并可能已重发。

结论与想请教

三个方向复核下来:方向 2(brief)对当前 PTY 接入不可行、方向 3(拦截)技术上做不到——目前实际可落地的只剩方向 1(提示层缓解)。它治标不治根(nudge 每轮照来,仅降低模型误判概率),但低风险、对生产模型 gpt-5.6-sol 有实测改善,参考实现见 draft PR #544

想请教:

  1. 这个 nudge 冲突你之前是否已知 / 有历史决策?
  2. 方向 1 的提示层缓解,你是否接受先合入作为止血?(我也想确认有没有我没看到的、能绕开 PTY 限制开 brief 的路子。)
  3. 是否值得同时向上游(Anthropic)反馈,让 harness 侧对"通过工具/外部命令回复"的场景提供 nudge 豁免——那才是根治。

环境:botmux master(19893f1e);Claude Code @anthropic-ai/claude-code 2.1.212;生产模型 gpt-5.6-sol。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions