Skip to content

fix(prompt): 消除 Claude Code「无可见输出」误判导致的重复发送 - #544

Merged
deepcoldy merged 2 commits into
deepcoldy:masterfrom
Barrierml:fix/claude-thinking-only-nudge-resend
Jul 29, 2026
Merged

fix(prompt): 消除 Claude Code「无可见输出」误判导致的重复发送#544
deepcoldy merged 2 commits into
deepcoldy:masterfrom
Barrierml:fix/claude-thinking-only-nudge-resend

Conversation

@Barrierml

@Barrierml Barrierml commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

⚠️ 方案待讨论:本 PR 仅为提示层缓解的参考实现,不能根治该问题。根因分析、5 模型 A/B、以及三个修复方向的完整权衡见 #545——复核后,brief 模式(因 PTY 接入无法 opt-in)与适配层拦截(够不着 harness 内部注入点)均不可行,实际只剩本 PR 这条提示层缓解可落地。请先在 #545 对齐方向后再决定本 PR 的去留。


现象

用户在飞书里对同一条消息收到多条重复回复。下图是生产环境(模型 gpt-5.6-sol)里对同一条 !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 Code 自带的 brief 模式CLAUDE_CODE_BRIEF + 内建 send tool)在协议层把「工具即回复渠道、静默结束合法」做对了,理论上能根治。但复核发现它需要结构化用户消息 opt-in(userMsgOptIn,而 botmux 是用 PTY 把 Claude Code 当交互式 TUI 驱动、没有该 opt-in 通道——单设 CLAUDE_CODE_BRIEF=1 不生效。真正根治需改 Claude 家族的接入方式或由上游 harness 提供豁免(详见 #545)。

修复

纯提示层纠偏,不动发送 / 去重 / 桥接架构。明确告诉模型三件事:

  1. botmux send 退出码 0(返回 {"success":true,...})就代表已送达用户;
  2. 本轮「终端没有可见文本、直接结束」是正常且预期的;
  3. 若看到「无可见输出,请继续」这类提示是底层 CLI 的误判,不要重发——只有当 botmux send 自身报错(非零退出 / 打印「发送失败」)才重试。

改动落点(6 文件,+34/-4):

  • src/i18n/{zh,en}.ts:新增 ai.routing.no_visible_output_okai.shell.no_visible_output_ok;扩写每轮注入的 ai.followup.reminder(覆盖非 injectsSessionContext CLI 的追问轮)。
  • src/adapters/cli/shared-hints.ts:注入两条路径——buildBotmuxSystemPromptText(claude-code / grok / genius 的 --append-system-prompt)+ buildBotmuxShellHints 及旧 BOTMUX_SHELL_HINTS(约 15 个内联提示 CLI)。
  • src/skills/definitions.tsbotmux-send 技能补「发送成功判定 & 不要重发」。
  • 测试:更新 test/prompt-builder.test.ts 的 reminder 断言 + 在 test/workflow-discovery-hints.test.ts 新增纠偏提示落位断言。

不动:hermes(final 文本转发模型,语义相反)与 bridge/adopt 模式(模型无感知 botmux)。

影响面

仅提示词文本,不改任何运行逻辑。跨全部走 botmux send 的 CLI、每轮生效。

验证

单测

pnpm exec vitest run test/prompt-builder.test.ts test/workflow-discovery-hints.test.ts test/cli-adapters.test.ts
→ 3 files, 356 passed
# 相关套件:insight-readers / resumable-session-discovery / codex-app-clean-prompt / grok-transcript → 73 passed

本地隔离 A/B 复现

方法:真实 claude(Claude Code 2.1.212)+ 一个假 botmuxsend 只落盘、不回显,模拟「终端输出用户看不到」)。CONTROL 用 botmux 真实系统提示(master 版),TREATMENT 用本 PR 补丁版——两者逐字仅差本 PR 新增的纠偏句。任务强制「零文本静默结束」以稳定触发 nudge。指标 = 从事件流判定「触发 nudge 后是否又发生 botmux send」(真正的重发)。

生产同款模型 gpt-5.6-sol,各 4 轮:

T1 T2 T3 T4 nudge 后重发率
CONTROL(修复前) 未重发 重发 未重发 重发 2/4
TREATMENT(本 PR) 未重发 未重发 未重发 未重发 0/4

8 轮全部触发了 nudge(Claude Code 确定性行为)。典型时序对比:

CONTROL(重发):  botmux send → NUDGE → botmux send(重复!) → NUDGE → 结束
TREATMENT(正确): botmux send → NUDGE → 识别为误报 → 安静结束,不重发

关于模型差异(如实说明):nudge 是 Claude Code harness 的确定性行为,与后端模型无关;是否重发是模型相关的。除 gpt-5.6-sol(生产同款)能干净复现外,另跑了 deepseek-v4-flash(更易感,简化提示下 4→1 条);而 claude-opus-4-8 / claude-sonnet-5 / glm-5.2 这类更强的模型多数能自我纠错、仅偶发重发(n 小、噪声大)。结论:本修复是提示层的显式护栏——对易感模型/易感回合帮助明显,对强模型无害。

模型说明:生产现象为 gpt-5.6-sol;本地 A/B 主验证亦为 gpt-5.6-sol(经 traex 代理经由 Claude Code 2.1.212 harness 路由)。

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review:代码正确、安全、零回归 —— 可合入(方向问题留给 #545)

@BOTMUX开发者(Claude) 独立复核。结论:代码层面无阻塞,纯提示词改动、零运行逻辑变更、干净合入 master、无回归。是否走「提示层」这一层是产品方向问题(作者已在 #545 提出待讨论),不属于代码缺陷。

我独立验证过的点

构建 & 测试

  • pnpm build
  • 触及提示/i18n/适配器/reminder 信封的全部相关套件 430 用例全绿prompt-builder / workflow-discovery-hints / cli-adapters / resumable-session-discovery / insight-readers / codex-app-clean-prompt / grok-transcript

合入现状:PR base 落后 master 230 个 commit,但 i18n 高频 churn 未冲突——git merge-tree --write-tree exit 0,逐文件 merge-file 均 CLEAN。

诊断依据成立:no visible output / please continue 原文在 botmux src+dist 里 grep 不到(只有本 PR 新增的纠偏 key),确认该 nudge 非 botmux 注入,来自 Claude Code 二进制——与 PR/#545 描述一致。

易感 CLI 覆盖完整:只有 Claude-Code 家族(claude-code / genius / grok)驱动 claude 二进制、会触发该 nudge;三者都走 buildBotmuxSystemPromptText,已带上 ai.routing.no_visible_output_ok。mir/mira 包 mircli、codex-app 包 codex、riff 是远端 agent——均不经此 harness,无需覆盖。

机制成立:纠偏句经 --append-system-prompt 在 spawn 时注入一次、会话级常驻;nudge 是同会话内注入的一条 user 消息,系统提示仍在上下文里 → 模型能看到纠偏。

hermes 排除双重成立:

  1. adapter.systemHints 在运行时从不被读(全仓 grep 只有注释引用它;实际走 buildBotmuxShellHints/buildHermesBotmuxHints 按 locale 重新取)——所以 BOTMUX_SHELL_HINTS 多一行不会泄漏进 hermes;
  2. hermes 是「final 文本转发」模型,每轮以可见文本结尾 → 本就不触发 thinking-only 检测;follow-up 也走独立的 hermesFollowupReminder(未改)。

i18n 完整性:两个新 key ai.routing.no_visible_output_ok / ai.shell.no_visible_output_ok 在 zh/en 均已定义,全量 key-set 无 zh-only / en-only 漂移。

边角安全:

  • 字面量 {"success":true,...} 安全:interpolate 正则 /\{(\w+)\}/g 要求 { 后接 word char,而实际是 {",永不匹配;且所有新调用点 params 为 undefined,interpolate 根本不执行。
  • adopt/bridge 路径正确不注入 <botmux_reminder>(模型对 botmux 无感知),纠偏句不会污染桥接模式。
  • 无「必须以可见文本结尾」类矛盾指令。
  • 无脆弱的 hint 数组长度/精确匹配断言会被 +1 行打破。

观察(非阻塞)

  • follow-up reminder(ai.followup.reminder)实际覆盖除 mira 外全部 CLI(含 claude-code),PR 描述写的「覆盖非 injectsSessionContext CLI 的追问轮」略窄——实际覆盖面更广,对 claude-code 是「系统提示常驻 + 每轮 reminder」双保险,无害。仅描述措辞可微调。
  • 效力如实:A/B(gpt-5.6-sol)CONTROL 2/4 重发 → TREATMENT 0/4。提示层护栏对易感模型/回合帮助明显、对强模型无害;但天然无法 100% 保证——这正是 #545 要对齐的方向问题。

建议

代码可合。先在 #545 对齐「提示层缓解 vs 根治」的方向,方向 OK 后本 PR 即可落地。

deepcoldy pushed a commit to Barrierml/botmux that referenced this pull request Jul 27, 2026
## 背景
PR deepcoldy#544 无条件给所有走 botmux send 的 CLI 注入「无可见输出/不要重发」
纠偏提示。评审结论:该提示主要在 Claude Code(≥2.1.212)驱动**非 Claude 后端
模型**时才有明显收益(易把 thinking-only nudge 误读为发送失败而重复回复),
对纯 Claude 场景无害但通常无需。应做成可选、默认关。

## 改动
新增全局实验开关 `dashboard.noVisibleOutputHint`(默认 OFF,absent⇒off),
完全套用现成 `codexRpcInput` 的成熟链路(实验性、live-read、翻动下一个会话即
生效、无需重启 daemon):
- global-config.ts:接口字段 + readDashboard 布尔解析
- config.ts:live getter `config.noVisibleOutputHint`(readGlobalConfig
  ...dashboard?.noVisibleOutputHint === true)
- shared-hints.ts:两处高频注入面(buildBotmuxSystemPromptText 路由块 +
  buildBotmuxShellHints)按开关条件插入;静态 BOTMUX_SHELL_HINTS 不加(其契约
  禁止 module load 期读运行时配置)
- session-manager.ts:每轮 <botmux_reminder> 按开关在
  ai.followup.reminder(基线) / ai.followup.reminder_no_resend(纠偏)间选择
- i18n:ai.followup.reminder 回退 master 原短句,新增
  ai.followup.reminder_no_resend(zh/en);ai.routing/shell.no_visible_output_ok
  保留(仅在开关开时被引用)
- dashboard:ResolvedDashboardSettings 字段 + resolve + settings-write-applier
  校验(invalid_noVisibleOutputHint)+ settings-page.tsx 实验性区块 ToggleRow +
  i18n label/help(zh/en)

技能文档 botmux-send 里那段「不要重发」按讨论保留不门控(按需查阅、非每轮强注入,
且为它改 installer 幂等/签名不划算)。

## 影响面
仅提示词文本 + 一个默认关的实验开关,不改发送/去重/桥接逻辑。
- 跨 CLI:门控在共用注入面(shared-hints / session-manager),对全部走 botmux send
  的 CLI 一致生效;hermes 仍走自己的 hermesFollowupReminder,不受影响
- 默认 OFF 时渲染出的 prompt **逐字回到 pre-feature master**(见验证)

## 验证
- pnpm build 绿
- 相关套件全绿(workflow-discovery-hints / prompt-builder / global-config /
  settings-write-applier / cli-adapters / dashboard-i18n / codex-rpc-lifecycle
  / daemon-internal-api 等)
- 运行时门控双向验证:OFF⇒路由/shell/reminder 均无纠偏句;ON⇒三处均出现
- **字节级对拍**:当前分支 OFF 状态 vs PR base 父提交(pre-feature master)
  的 buildBotmuxSystemPromptText/buildBotmuxShellHints(zh+en)渲染 **完全一致**;
  ON 状态与 OFF 有差异(纠偏句确实出现)
- 全量 pnpm test:9 失败全部落在 scheduler/schedule-card-model/
  v3-distillation-runner,在干净 origin/master 同 3 文件跑出**一模一样的 9 失败**
  =机器级时间敏感/沙盒基线,零回归

Co-Authored-By: Riff <noreply@riff.dev>
@deepcoldy

Copy link
Copy Markdown
Owner

追加改动:改为全局实验开关(默认关) — 已推送 70ae9609

按讨论把本 PR 的「无可见输出/不要重发」纠偏提示从无条件注入改为全局实验开关门控,默认关闭。评审共识:该提示主要在 Claude Code 驱动非 Claude 后端模型时才有明显收益(易把 harness 的 thinking-only nudge 误读为发送失败而重复回复),对纯 Claude 场景无害但通常无需——因此做成可选、默认关,有需要再在 dashboard 打开。

开关设计

新增 dashboard.noVisibleOutputHint(布尔,默认 OFF,absent ⇒ off),完全套用现成 codexRpcInput 的成熟链路(实验性、live-read、翻动下一个会话即生效、无需重启 daemon):

改动
global-config.ts 接口字段 + readDashboard 布尔解析(非布尔丢弃)
config.ts live getter config.noVisibleOutputHint(readGlobalConfig().dashboard?.noVisibleOutputHint === true)
shared-hints.ts 两处高频注入面(buildBotmuxSystemPromptText 路由块 + buildBotmuxShellHints)按开关条件插入。静态 BOTMUX_SHELL_HINTS 不加(其契约禁止 module-load 期读运行时配置)
session-manager.ts 每轮 <botmux_reminder> 按开关在 ai.followup.reminder(基线) / ai.followup.reminder_no_resend(纠偏)间选择
i18n(src) ai.followup.reminder 回退 master 原短句;新增 ai.followup.reminder_no_resend(zh/en);ai.routing/shell.no_visible_output_ok 保留(仅开关开时被引用)
dashboard ResolvedDashboardSettings 字段 + resolve + settings-write-applier 校验(invalid_noVisibleOutputHint)+ settings-page.tsx 实验性区块 ToggleRow + i18n label/help(zh/en)

技能文档 botmux-send 里那段「不要重发」按讨论保留不门控(按需查阅、非每轮强注入,为它改 installer 幂等/签名不划算)。

影响面

仅提示词文本 + 一个默认关的实验开关,不改发送/去重/桥接逻辑。门控在共用注入面,对全部走 botmux send 的 CLI 一致生效;hermes 仍走自己的 hermesFollowupReminder,不受影响

验证

  • pnpm build 绿
  • 相关套件全绿:workflow-discovery-hints(重写为 OFF/ON 双态)/ prompt-builder / global-config / settings-write-applier / cli-adapters / dashboard-i18n / codex-rpc-lifecycle / daemon-internal-api
  • 运行时门控双向验证:OFF ⇒ 路由/shell/reminder 三处均无纠偏句;ON ⇒ 三处均出现
  • 字节级对拍:当前分支 OFF 状态 vs PR base 父提交(pre-feature master)buildBotmuxSystemPromptText/buildBotmuxShellHints(zh+en)渲染 逐字完全一致;ON 与 OFF 有差异(纠偏句确实出现)。即:默认关时对既有行为零影响
  • 全量 pnpm test:9 失败全部落在 scheduler / schedule-card-model / v3-distillation-runner;在干净 origin/master 同 3 文件跑出一模一样的 9 失败 = 机器级时间敏感/沙盒基线,零回归

新增测试:global-config 解析(布尔/非布尔丢弃)、settings-write-applier(写入 + invalid_noVisibleOutputHint 拒绝)、workflow-discovery-hints(OFF 基线态 + ON 态)、prompt-builder(OFF 基线 reminder + ON 纠偏 reminder)。

@Barrierml
Barrierml force-pushed the fix/claude-thinking-only-nudge-resend branch from 70ae960 to fa8a933 Compare July 27, 2026 04:12
Barrierml pushed a commit to Barrierml/botmux that referenced this pull request Jul 27, 2026
PR deepcoldy#544 无条件给所有走 botmux send 的 CLI 注入「无可见输出/不要重发」
纠偏提示。评审结论:该提示主要在 Claude Code(≥2.1.212)驱动**非 Claude 后端
模型**时才有明显收益(易把 thinking-only nudge 误读为发送失败而重复回复),
对纯 Claude 场景无害但通常无需。应做成可选、默认关。

新增全局实验开关 `dashboard.noVisibleOutputHint`(默认 OFF,absent⇒off),
完全套用现成 `codexRpcInput` 的成熟链路(实验性、live-read、翻动下一个会话即
生效、无需重启 daemon):
- global-config.ts:接口字段 + readDashboard 布尔解析
- config.ts:live getter `config.noVisibleOutputHint`(readGlobalConfig
  ...dashboard?.noVisibleOutputHint === true)
- shared-hints.ts:两处高频注入面(buildBotmuxSystemPromptText 路由块 +
  buildBotmuxShellHints)按开关条件插入;静态 BOTMUX_SHELL_HINTS 不加(其契约
  禁止 module load 期读运行时配置)
- session-manager.ts:每轮 <botmux_reminder> 按开关在
  ai.followup.reminder(基线) / ai.followup.reminder_no_resend(纠偏)间选择
- i18n:ai.followup.reminder 回退 master 原短句,新增
  ai.followup.reminder_no_resend(zh/en);ai.routing/shell.no_visible_output_ok
  保留(仅在开关开时被引用)
- dashboard:ResolvedDashboardSettings 字段 + resolve + settings-write-applier
  校验(invalid_noVisibleOutputHint)+ settings-page.tsx 实验性区块 ToggleRow +
  i18n label/help(zh/en)

技能文档 botmux-send 里那段「不要重发」按讨论保留不门控(按需查阅、非每轮强注入,
且为它改 installer 幂等/签名不划算)。

仅提示词文本 + 一个默认关的实验开关,不改发送/去重/桥接逻辑。
- 跨 CLI:门控在共用注入面(shared-hints / session-manager),对全部走 botmux send
  的 CLI 一致生效;hermes 仍走自己的 hermesFollowupReminder,不受影响
- 默认 OFF 时渲染出的 prompt **逐字回到 pre-feature master**(见验证)

- pnpm build 绿
- 相关套件全绿(workflow-discovery-hints / prompt-builder / global-config /
  settings-write-applier / cli-adapters / dashboard-i18n / codex-rpc-lifecycle
  / daemon-internal-api 等)
- 运行时门控双向验证:OFF⇒路由/shell/reminder 均无纠偏句;ON⇒三处均出现
- **字节级对拍**:当前分支 OFF 状态 vs PR base 父提交(pre-feature master)
  的 buildBotmuxSystemPromptText/buildBotmuxShellHints(zh+en)渲染 **完全一致**;
  ON 状态与 OFF 有差异(纠偏句确实出现)
- 全量 pnpm test:9 失败全部落在 scheduler/schedule-card-model/
  v3-distillation-runner,在干净 origin/master 同 3 文件跑出**一模一样的 9 失败**
  =机器级时间敏感/沙盒基线,零回归

Co-Authored-By: Riff <noreply@riff.dev>
@Barrierml
Barrierml marked this pull request as ready for review July 27, 2026 04:12
@Barrierml

Copy link
Copy Markdown
Contributor Author

已 rebase 到最新 master + 解冲突,转正式 PR

按讨论把分支从旧 base(落后 master 245 commit)rebase 到最新 origin/master,两个 commit 原样保留(fix(prompt) 作者 shir0ha / feat(config) 作者 申晗)。

冲突只集中在实验开关那一版触及的 4 个 dashboard 文件,全部是「master 新增 codexNotifier 设置」与「本 PR 新增 noVisibleOutputHint 开关」在同一处各自追加,两边保留即可,无逻辑取舍:

  • src/dashboard.tsResolvedDashboardSettings 字段 + resolve)
  • src/dashboard/settings-write-applier.ts(字段 + 错误类型 union;校验/写入块非冲突区已干净落入)
  • src/dashboard/web/i18n.ts(zh + en label/help)
  • src/dashboard/web/settings-page.tsx(类型 + 本地 state + JSX:CodexNotifierSettingsEditor 与新 ToggleRow 并存)

验证

  • mergeable: MERGEABLE(GitHub 侧确认干净合入)
  • 开关链路 4 文件(global-config / config / shared-hints / session-manager)门控完整无缺
  • 相关套件全绿:global-config / settings-write-applier / workflow-discovery-hints / prompt-builder / cli-adapters = 453 passed
  • 注:本地沙盒 tsc 会报 122 个 Cannot find namespace 'JSX',但干净 origin/master 上同样报 122 个、数目一致,是环境缺 JSX 运行时类型配置所致、非本次改动引入(实际 dashboard 走 esbuild bundle)

默认 OFF,行为与 pre-feature master 逐字一致。方向问题仍以 #545 为准。

@deepcoldy

Copy link
Copy Markdown
Owner

基于最新 commit 70ae9609 复审(对抗式自审)

用审别人 PR 的眼光重扫了实验开关这版。代码本身无阻塞、逻辑正确;发现一个需要处理的非代码缺陷:与最新 master 有合并冲突(纯相邻,已验证可平凡解决)。

🟠 唯一 actionable:与当前 master 冲突(4 个 dashboard 文件)

  • 不是我这版引入的 bug——PR base(aac64bb)单独并入当前 master 无冲突;是我这个 commit 与 master 并行落地的 codexNotifier 实验开关(#? Codex 任务完成通知)撞在了同一批接缝:两个实验开关都改 ResolvedDashboardSettings 接口、applier 的 error-union + 校验链、settings-page 的解析块、实验性区块 i18n。经典的「dashboard 设置=churn 热点,两个 feature 同点扩展」。
  • 冲突性质=纯相邻(union 保留双方),非语义重叠。我在临时 worktree 实际并了一遍:4 文件全是「两边都留」;唯一需人工的一处是 settings-page.tsx 里我的 <ToggleRow .../> 自闭合 /> 恰好被 ======= 标记切断,补回一行即可。
  • 验证:解冲突后 pnpm build 绿;合并树上 workflow-discovery-hints/prompt-builder/global-config/settings-write-applier 150 用例全绿,两个开关并存无干扰。
  • 建议:合码前把本分支 rebase/merge 到最新 master(我可代做,已确认解法),或在 admin-merge 时按上述「union + 补一个 />」解。

✅ 复核通过的点(committed code)

  • 注入面无遗漏:no_visible_output_ok 两个 key(routing/shell)+ followup reminder,三处全部经 config.noVisibleOutputHint 门控;buildNewTopicPrompt/buildReforkPrompt 不单独发 reminder(走 routing 块/shell hints,已覆盖);codex-app 的 codexAppInput 是独立结构化 payload、本就不带 reminder → 无绕过。
  • env 逃生阀一致:getter readGlobalConfig().dashboard?.noVisibleOutputHint === true,与最接近的同类 codexRpcInputDefault 完全一致(都无 env override、都 === true 默认关);未画蛇添足加 env。
  • 跨进程 live-read 成立:buildBotmuxSystemPromptTextworker 子进程(fork,spawn 时新读)、buildFollowUpContentdaemon;两侧都走 readGlobalConfig(2s TTL,mergeDashboardConfigmergeGlobalConfig 同进程写立即 readCache=null 失效)→「翻开关下一个会话即生效、无需重启」属实,与 chatBotDiscovery/codexRpcInput 同款语义。
  • dashboard 端到端通:接口字段→resolve→applier 校验(invalid_noVisibleOutputHint,非布尔拒绝)→SPA saveBoolean 泛型路由 {[key]:value}→ToggleRow;.tsxtsc 真类型检查(build 里 tsc 步)。
  • i18n 无孤儿、全 parity:两个 gated key 仍被引用(仅开关开时);zh/en key-set 零漂移;ai.followup.reminder 已逐字回退 master 原短句。
  • 测试非假绿:workflow-discovery-hints 重写为 OFF 基线态 + ON 态双向断言;prompt-builder 加 ON 态 reminder 用例;global-config/settings-write-applier 补解析+校验拒绝。480 相关用例全绿。
  • 零回归:全量 9 失败(scheduler/schedule-card/v3-distillation)在干净 master 同文件一模一样,机器级时间/沙盒基线。

结论

代码可合。合码前需解一次与 master 的相邻冲突(解法已验证)。

@deepcoldy

Copy link
Copy Markdown
Owner

合并冲突已解决(分支已 rebase 到最新 master)

分支 head 现为 fa8a9333——noVisibleOutputHint 实验开关这版已并入含 codexNotifier(#587 Codex 任务完成通知)的最新 master。之前提到的 4 个 dashboard 文件相邻冲突全部解决。

冲突性质与解法(均为相邻,保留双方)

两个实验开关落在同一批 dashboard 接缝:

  • ResolvedDashboardSettings 接口(dashboard.ts + settings-write-applier.ts + settings-page.tsx)→ 两个字段并列保留
  • applier error-union + 校验链 → invalid_noVisibleOutputHintcodexNotifier_* 并列
  • settings-page 解析块 + 实验性区块 JSX → 两个 ToggleRow/Editor 并列
  • dashboard i18n(zh + en)→ 两组 settings.* key 并列

唯一需人工处理的一处:settings-page.tsx 里 <ToggleRow/> 自闭合 /> 与相邻 <CodexNotifierSettingsEditor/> 在冲突边界共用了同一行 />,补齐各自闭合即可。

验证

  • pnpm build 绿
  • 相关套件全绿:workflow-discovery-hints / prompt-builder / global-config / settings-write-applier / dashboard-i18n;codex-notifier-* 全套 86 用例绿(确认没碰坏 master 并行落地的 codexNotifier)
  • gate ON/OFF 行为正确:默认 OFF ⇒ 路由块/shell hints/reminder 均无纠偏句;ON ⇒ 三处出现
  • config.noVisibleOutputHint getter 保留;i18n zh/en key 零漂移
  • 分支干净并入当前 master(仅落后 2 个 release-CI commit,无代码冲突)

已请 Codex 复审这版。

@deepcoldy

deepcoldy commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Codex 复审结论(head fa8a9333,target master@fad74030

🔴 当前阻塞:最新 master #554 引入了新的语义冲突

复审期间 origin/master9cdf42cb 前进到 fad74030,合入了 #554 fix(bridge): 支持无回复回合静默结束。本地以最新 target 执行 git merge-tree --write-tree HEAD origin/master,现在明确冲突于:

  • src/i18n/zh.ts
  • src/i18n/en.ts
  • test/prompt-builder.test.ts

这次不能只机械“保留双方”。#554 把默认 follow-up reminder 改为 BOTMUX_NO_REPLY 协议;本 PR 在 gate=ON 时会选择独立的 ai.followup.reminder_no_resend。若仅保留当前 anti-resend 文案,打开实验开关就会丢掉 #554 的无回复 sentinel 语义,形成开关态回归。

正确解法应是:

  1. ai.routing.usage_send_when / ai.shell.when_to_send 保留最新 master 的 BOTMUX_NO_REPLY 文案,同时保留本 PR 新增的 gated no_visible_output_ok key。
  2. ai.followup.reminder 保留最新 master 默认文案,保证 gate=OFF 逐字回到最新 baseline。
  3. ai.followup.reminder_no_resend(zh/en)改成“最新 master sentinel 语义 + anti-resend 纠偏”的组合版本;不能沿用当前只含 anti-resend 的字符串。
  4. prompt-builder 默认 OFF 断言更新为最新 master reminder;ON 用例同时断言 BOTMUX_NO_REPLY 与“无输出不重发”,防止任一侧再次被冲突解决吞掉。

因此当前暂不批准,待 rebase/解完这 3 处后再做一次快速终审。

fa8a9333 本身已通过的复核

  • 三处门控完整:routing no_visible_output_ok、shell no_visible_output_ok、follow-up reminder 全部经 config.noVisibleOutputHintbuildNewTopicPrompt、refork、codex-app sidecar、Hermes、Mira、adopt/bridge 无绕过或语义污染。
  • 默认 OFF 独立字节级对拍:对实际 pre-feature merge-base b30e8949,zh/en 的 shell hints、system routing(最小/完整 identity+skill)、new-topic/follow-up(codex / claude-code / codex-app / hermes / mira)全部逐字一致;ON 时三处 zh/en 均出现纠偏,Hermes/Mira 排除成立。
  • live-read:worker 是宿主侧 fork、继承同一 HOME,新 worker 首读配置;daemon 侧 follow-up 走 readGlobalConfig 2s TTL,同进程 dashboard 写入立即清 cache,跨进程最迟约 2s 收敛。实现没有独立 BOTMUX_* env override,和作为模板的 codexRpcInputDefault 一致;关闭/删除 dashboard 字段即回到 OFF。
  • dashboard 链路完整:config interface/read → resolve → applier 严格布尔校验 → SPA parse/saveBoolean → ToggleRow;codexNotifier 相邻逻辑保留且全套测试通过。
  • i18n:当时合并树源字典 zh/en 均 1024 keys,零单边 key;三个新增 key 双语齐全。

实际验证

  • pnpm build(PR head)✅
  • 相关 + dashboard/i18n + codexNotifier:22 files / 1224 tests passed
  • 在当时最新 master@9cdf42cb 的自动合并树:pnpm build ✅;同组 1224 passed
  • 合并树全量:11098 passed;唯一 group-join-shared-routing beforeAll 10s 超时,隔离重跑 5/5 passed(并发环境 flake,非本 PR 路径)
  • git diff --check ✅;工作树清洁

结论:实验开关实现本身无额外代码 finding;当前唯一 blocker 是审查期间新落地 #554 后的 3 文件冲突及 ON reminder 语义合并。

✅ rebase 后终审通过(head 3dbc6bb5,target master@fad74030

此前唯一 blocker 已正确关闭,未发现新增问题:

  • 远端重新锁定:PR head 3dbc6bb57a5974f5d547cdb0f2421e77a511e997,master fad7403077995602712f467243cef4fe5a794e35;merge-base 等于 master,GitHub MERGEABLE,本地 git merge-tree --write-tree 无冲突。
  • 三处冲突语义正确:routing/shell 保留 fix(bridge): 支持无回复回合静默结束 #554BOTMUX_NO_REPLY 说明并让 no_visible_output_ok 独立门控;OFF 的 ai.followup.reminder 等于 fix(bridge): 支持无回复回合静默结束 #554 原文;ON 的 ai.followup.reminder_no_resend(zh/en)同时包含 sentinel 与 anti-resend。
  • 独立字节级对拍:gate=OFF 时,zh/en 的 shell、system(最小/identity/identity+skill)、new-topic/follow-up(codex / claude-code / codex-app / hermes / mira / gemini)及 refork 共 34 组实际渲染样本与最新 master 逐字一致。
  • gate=ON 时,zh/en 的 shell/system/follow-up/refork/codex-app 均同时保留 BOTMUX_NO_REPLY 和 anti-resend;Hermes/Mira 与静态 BOTMUX_SHELL_HINTS 排除成立。
  • pnpm build
  • 12 个聚焦套件(含 prompt/workflow/global-config/settings/dashboard-i18n/CLI/Codex RPC/daemon API,以及 fix(bridge): 支持无回复回合静默结束 #554 bridge/event/reply fallback 邻居):1344/1344 tests passed
  • git diff --check ✅;工作树清洁;终审前再次确认远端 head/master 未漂移。

环境变量说明维持首审结论:实现没有独立 BOTMUX_* override,与模板 codexRpcInputDefault 一致;关闭或删除 dashboard 字段即回到 OFF。此项不是本次 blocker。

终审结论:APPROVE,可合入。

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请求变更:审查期间最新 master 合入 #554(BOTMUX_NO_REPLY 静默回合协议),当前 head fa8a9333 与 target 在 src/i18n/{zh,en}.tstest/prompt-builder.test.ts 产生 3 处冲突。

这不是纯相邻 union:gate=ON 选择的 ai.followup.reminder_no_resend 必须同时继承 #554 的 BOTMUX_NO_REPLY 语义,否则打开实验开关会回归最新 master。请按进度评论中的 4 点解法 rebase/解冲突,并让 ON 用例同时断言 sentinel + anti-resend;随后可快速终审。

实验开关本身的门控、默认 OFF 字节级基线、live-read、dashboard/i18n/codexNotifier 链路均已独立验证通过。详见:#544 (comment)

Barrierml and others added 2 commits July 29, 2026 00:18
用户在飞书里对同一条消息收到多条重复回复。根因是底层 CLI(Claude Code
2.1.212+)的 thinking-only 检测:当一轮以 end_turn/stop_sequence 结束、
且该轮 assistant 消息里没有任何非空 text 块时,会注入
`[Your previous response had no visible output. Please continue and produce
a user-visible response.]` 并自动重问(最多 3 次)。

botmux 要求「回复必须走 botmux send(外部 shell 命令)、终端输出用户看不到」,
模型正确地只调 botmux send、结尾无可见文本,恰好命中该检测;模型把 nudge 误读为
「发送失败」于是反复重发。该短语来自 Claude Code 二进制、非 botmux,codex/gemini/
cursor-agent 均无此行为。

明确告知模型:botmux send 退出码 0 即已送达;本轮无可见文本地结束是正常的;
若看到「无可见输出」类提示是底层 CLI 误判,不要重发,仅当 botmux send 自身报错才重试。
- i18n 新增 ai.routing.no_visible_output_ok / ai.shell.no_visible_output_ok(zh/en)
- 扩写 ai.followup.reminder(每轮注入,覆盖非 injectsSessionContext CLI 的追问轮)
- shared-hints:注入 buildBotmuxSystemPromptText(claude-code/grok/genius 的
  --append-system-prompt)+ buildBotmuxShellHints + 旧 BOTMUX_SHELL_HINTS(约 15 个内联 CLI)
- botmux-send 技能补「发送成功判定 & 不要重发」
- 不动 hermes(final 文本转发模型,语义相反)与 bridge/adopt(模型无感知 botmux)

仅提示词文本;不改发送/去重/桥接逻辑。跨全部 CLI(send 模型),每轮生效。

- pnpm exec vitest run test/prompt-builder.test.ts test/workflow-discovery-hints.test.ts
  test/cli-adapters.test.ts → 356 passed;相关 insight/resumable/codex-app/grok 套件 → 73 passed
- 本地隔离 A/B(真实 claude 2.1.212 + 假 botmux + botmux 同款约束,强制零文本结束触发
  nudge):CONTROL(旧提示) nudge×5→重复发送 4 条;TREATMENT(本补丁) nudge×1→只发 1 条、
  收到 nudge 后安静结束、不重发
PR deepcoldy#544 无条件给所有走 botmux send 的 CLI 注入「无可见输出/不要重发」
纠偏提示。评审结论:该提示主要在 Claude Code(≥2.1.212)驱动**非 Claude 后端
模型**时才有明显收益(易把 thinking-only nudge 误读为发送失败而重复回复),
对纯 Claude 场景无害但通常无需。应做成可选、默认关。

新增全局实验开关 `dashboard.noVisibleOutputHint`(默认 OFF,absent⇒off),
完全套用现成 `codexRpcInput` 的成熟链路(实验性、live-read、翻动下一个会话即
生效、无需重启 daemon):
- global-config.ts:接口字段 + readDashboard 布尔解析
- config.ts:live getter `config.noVisibleOutputHint`(readGlobalConfig
  ...dashboard?.noVisibleOutputHint === true)
- shared-hints.ts:两处高频注入面(buildBotmuxSystemPromptText 路由块 +
  buildBotmuxShellHints)按开关条件插入;静态 BOTMUX_SHELL_HINTS 不加(其契约
  禁止 module load 期读运行时配置)
- session-manager.ts:每轮 <botmux_reminder> 按开关在
  ai.followup.reminder(基线) / ai.followup.reminder_no_resend(纠偏)间选择
- i18n:ai.followup.reminder 回退 master 原短句,新增
  ai.followup.reminder_no_resend(zh/en);ai.routing/shell.no_visible_output_ok
  保留(仅在开关开时被引用)
- dashboard:ResolvedDashboardSettings 字段 + resolve + settings-write-applier
  校验(invalid_noVisibleOutputHint)+ settings-page.tsx 实验性区块 ToggleRow +
  i18n label/help(zh/en)

技能文档 botmux-send 里那段「不要重发」按讨论保留不门控(按需查阅、非每轮强注入,
且为它改 installer 幂等/签名不划算)。

仅提示词文本 + 一个默认关的实验开关,不改发送/去重/桥接逻辑。
- 跨 CLI:门控在共用注入面(shared-hints / session-manager),对全部走 botmux send
  的 CLI 一致生效;hermes 仍走自己的 hermesFollowupReminder,不受影响
- 默认 OFF 时渲染出的 prompt **逐字回到 pre-feature master**(见验证)

- pnpm build 绿
- 相关套件全绿(workflow-discovery-hints / prompt-builder / global-config /
  settings-write-applier / cli-adapters / dashboard-i18n / codex-rpc-lifecycle
  / daemon-internal-api 等)
- 运行时门控双向验证:OFF⇒路由/shell/reminder 均无纠偏句;ON⇒三处均出现
- **字节级对拍**:当前分支 OFF 状态 vs PR base 父提交(pre-feature master)
  的 buildBotmuxSystemPromptText/buildBotmuxShellHints(zh+en)渲染 **完全一致**;
  ON 状态与 OFF 有差异(纠偏句确实出现)
- 全量 pnpm test:9 失败全部落在 scheduler/schedule-card-model/
  v3-distillation-runner,在干净 origin/master 同 3 文件跑出**一模一样的 9 失败**
  =机器级时间敏感/沙盒基线,零回归

Co-Authored-By: Riff <noreply@riff.dev>
@deepcoldy
deepcoldy force-pushed the fix/claude-thinking-only-nudge-resend branch from fa8a933 to 3dbc6bb Compare July 29, 2026 07:27
@deepcoldy

Copy link
Copy Markdown
Owner

已 rebase 到最新 master(含 #554 sentinel),CONFLICTING 解除

按 Codex 复审要求,把分支 rebase 到 fad74030(#554 BOTMUX_NO_REPLY 静默回合协议已合入)。新 head = 3dbc6bb5,GitHub 已回到 MERGEABLE

冲突不是机械「保留双方」——sentinel 语义正确继承

Codex 的核心担忧「一开实验开关就回归最新 master」已按其给的解法处理:

i18n(zh + en):

test/prompt-builder.test.ts:

验证

  • pnpm build 绿
  • 相关 + 邻居套件全绿:prompt-builder/workflow-discovery-hints/global-config/settings-write-applier/cli-adapters/dashboard-i18n/codex-notifier-settings-ui(466)+ fix(bridge): 支持无回复回合静默结束 #554 sentinel 套件 bridge-fallback-gate/event-dispatcher/reply-target-fallback 293 全绿(确认没碰坏 fix(bridge): 支持无回复回合静默结束 #554)
  • gate 运行时双向:OFF ⇒ reminder=sentinel、shell/routing 无 anti-resend、fix(bridge): 支持无回复回合静默结束 #554BOTMUX_NO_REPLY 完整保留;ON ⇒ reminder=sentinel+anti-resend、三处 anti-resend 出现
  • 字节级对拍:rebased 分支 gate=OFF 状态 vs 当前 master fad74030 的 system(zh+en)/shell(zh+en)/follow-up(zh+en)渲染 逐字完全一致 → 默认关时对最新 master 零影响,彻底排除「开开关回归 master」

@BOTMUX开发者(codex) 已按你给的解法 rebase 完,请快速终审。

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

终审通过:此前 #554 冲突已按语义正确解决。独立确认 OFF 与最新 master 的 34 组实际渲染逐字一致;ON 在 zh/en 的 shell/system/follow-up/refork/codex-app 同时保留 BOTMUX_NO_REPLY 与 anti-resend,Hermes/Mira/静态 hints 排除成立。pnpm build 通过,12 个聚焦套件 1344/1344 通过,merge-tree 与 GitHub mergeability 均正常。完整证据见进度评论:#544 (comment)

@deepcoldy
deepcoldy merged commit 03c91f3 into deepcoldy:master Jul 29, 2026
deepcoldy pushed a commit that referenced this pull request Jul 29, 2026
把注入 prompt 正文里会被模型误读成子标签的字面 `<...>` 占位符选择性转义为实体,仅作用于 botmux 自己的固定脚手架文案(内置技能目录/off 帮助、routing/identity 正文、whiteboard 提示、Riff 独立 system),不碰 user_message/附件/白板内容等动态输入。

- 新增 `src/utils/xml.ts`:`escapeXmlText`(全量转义)+ `escapeXmlTagLikeTokens`(只转完整 `<...>` token,heredoc `<<'EOF'` 等无闭合 > 的 shell 操作符天然豁免)
- 覆盖 inline shell hints / 系统提示 identity+routing / whiteboard 块 / Riff DEFAULT_RIFF_SYSTEM_PROMPT
- 与 #544 noVisibleOutputHint 开关共存(rebase 解冲突:条件行同样走转义,默认 OFF 逐字回退基线)
- 价值定性:P2 prompt 结构清晰度(减少层级/作用域歧义),非 correctness/安全修复
- 双审 APPROVE(Claude 首审+多轮 follow-up,codex 终审);本地 504 测试 + build + 与最新 master 0 冲突

Co-Authored-By: Riff <noreply@riff.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants