Skip to content

feat(history): 按会话范围提示搜索边界与扩大方式 - #1002

Merged
deepcoldy merged 2 commits into
deepcoldy:masterfrom
tuomao:feat/chat-mode-context
Aug 28, 2026
Merged

feat(history): 按会话范围提示搜索边界与扩大方式#1002
deepcoldy merged 2 commits into
deepcoldy:masterfrom
tuomao:feat/chat-mode-context

Conversation

@tuomao

@tuomao tuomao commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

背景

话题会话里模型常常只搜索当前话题内的消息,而任务真正需要的上下文在话题之外的群聊里,于是它拿着不完整的信息作答;普通群 chat-scope 会话又不清楚能回溯多久。botmux history 的返回里其实已经有 sessionScope,但没有一句话告诉模型「当前能不能扩大范围、怎么扩大」,所以模型不会主动去用 --scope ambient

本 PR 原先的做法是在首轮 prompt 的 <chat_context> 里注入 <chat_mode>。评审过程中确认该方向不合适,已调整:

  • 注入的字段没有告诉模型该做什么(整块提示里没有出现 --scope ambient),模型拿到「这是话题群」也学不会扩大搜索范围,痛点依旧;
  • 决定检索范围的判据是 sessionScope(会话自身范围),不是群的 chat_mode:普通群里开出的话题,群 chat_mode 仍是 group 而 session 是 thread-scope,按群类型推断会得出「可以搜整群」的错误结论。cmdHistory 里两处 --scope 门本身也是按 isChatScope 判的;
  • chat_mode 需要每次群聊首轮额外打一次 chat.getgetChatModeStrict 有意不读缓存),落在全 fleet 热路径上;而 sessionScope 来自 session 自身状态,零 API 调用。

变更

1. botmux history 输出新增 rangeHintsrc/cli.ts)——按 (sessionScope, effectiveScope) 四种组合给出可执行提示:

场景 提示要点
话题会话(默认 thread) 说明只返回话题内消息;话题内证据不足时给出 botmux history --scope ambient --limit 20,并附隐私边界提醒
chat-scope 会话 说明返回整群最近 N 条、需要更早的调大 --limit;明确 --scope thread / --scope ambient 在此不适用
--scope ambient 结果 说明这批是话题之外的消息,以及怎么回到话题内
话题会话显式 --scope chat 说明结果包含本话题内的消息

措辞只描述模型能观测到的现象:daemon 侧的开话题指令在拼 prompt 前已被剥离,模型从未见过该 token,写进提示等于给一条它无法执行的指令;因此触发条件改用症状描述(话题内容不足以说明任务背景、出现没有出处的指代或结论),并加了断言防止回归。

提示放在工具返回而非首轮 prompt:信息出现在模型正要用它的那一刻,不占用首轮注意力预算,也不需要额外 API 调用。

2. 修正固定提示中「所有飞书会话都是话题群」的误导表述src/i18n/{zh,en}.ts)——ai.routing.intro / ai.shell.intro / ai.followup.reminder_hook 原文写死「你在飞书话题群中」,但同一套提示也会下发给单聊和普通群 chat-scope 会话,在那两种形态下是错的,改为中性的「飞书(Lark)会话」。

原先的 <chat_mode> 注入、pendingOpeningKind 生命周期改造等改动均已移除。

影响面

  • cmdHistory新增一个输出字段,不改检索逻辑、不动既有两处 --scope 门;
  • i18n 三条文案为全 CLI 共用,改后在话题群下语义不变、在单聊/普通群下不再误导;
  • 不涉及 opening 生命周期、/repo、选仓卡、重启恢复等路径(原方案为了正确落地需要改这些,风险面明显更大);
  • 共 4 个文件、+145 / -6。

验证

  • bun run build 通过(tsc 0 错)
  • 新增 test/history-range-hint.test.ts 8 项全绿;与 builtin-skills / dispatch / bots-list / topic-root-context / codex-app-clean-prompt / prompt-override-injection / customization-store / cli-adapters 合跑 9 文件 582 项全绿
  • 实跑本机 chat-scope 会话确认 rangeHint 正确下发;四种组合逐一驱动编译产物校验文案
  • 反向变异 4 组均转红:删掉 thread 支的 ambient 指引(1 红)、把四支合并成同一句(4 红)、去掉隐私边界那句(1 红)、把开话题指令措辞写回提示(1 红)
  • 确认仓库内无测试断言被修改的旧 i18n 文案

@tuomao
tuomao requested a review from deepcoldy as a code owner August 25, 2026 08:56
@deepcoldy

Copy link
Copy Markdown
Owner

感谢这个 PR!chat_mode 注入的核心机制是对的——fail-closed、类型安全(undefined=未查 / null=查了为空,避免伪造空 <name> 误导模型)、i18n 文案与隐藏上下文白名单都到位,测试也有牙。自动评审跑下来只有一个建议合前处理的问题:

P1:/repo 作为首条消息时,「空启动」语义被 pendingChatContext 副作用打破

forkPendingCli 里的 hasBufferedInputcommand-handler.ts:1794)在 master 就已经把 pendingChatContext !== undefined 计入(原为入群自动开工路径设计)。本 PR 在 daemon.ts:17995/repocmdPending 加了 pendingChatContext,于是 /repo 首条(pendingPrompt:'',本应静默待命)被翻转:

  • buildNewTopicCliInput 被调用 → CLI 收到非空开场(context 块 + 空的 <user_message></user_message>),而非 forkWorker(ds,'',false) 静默待命;
  • emptyStart 变 false → markInitialUserTurnPending 不再调用 → 下一条真实消息会被当 follow-up,而非 new-topic opening。

复现(复刻真实 daemon cmdPending,带 pendingChatContext):forkWorker 收到 {content:"WRAPPED:"}(原为 ''),buildNewTopicCliInput 被调用 1 次,initialUserTurnPendingundefined(原为 true)。

影响:① 首条发 /repo / /repo <名> 时 CLI 会对这个空回合产生非预期回复(如"准备好了"),而非静默待命,所有 CLI + 两种后端都受影响;② daemon 重启若 CLI 未持久化该空回合,--resume 恢复空会话后下一条 follow-up 会丢开场上下文(路由/身份/skills)。另外既有测试 command-handler.test.ts:3236 因手工构造 ds 时未设 pendingChatContext(绕过了 daemon cmdPending),仍断言旧行为却照样通过,是「假信心」;其上方「the CLI just boots and waits」注释也已过时。

建议二选一:

  • (a) 最干净:回退 /repo cmdPending 的注入,改为在两条 opening 路径(daemon.ts:20247 / 20507wantsOpening = isInitialUserTurnPending)resolve 并传 chatContext —— /repo 恢复空启动,下一条 opening 自带 chat_mode。(注意这两处 builder 的 opts 目前未传 chatContext,采用 (a) 需同时补线,否则 /repo 后首个 opening 会丢 chat_mode。)
  • (b) 改动更小:保留现状,但更新过时注释 + 修正测试 3236 让它复刻 daemon cmdPending,并接受 /repo 的非预期开场回复(等于接受一处 UX 回退)。

另一个非阻断的小点:bare /t(仅 setup、无 AI 回合)也会白打一次 getChatModeStrict——openingChatContextPromisedaemon.ts:18096)在 isBareForceTopic 提前 return(18130)之前就创建了,群聊下这个 promise 从不 await 却仍会发请求。可挪到分支之后。

以上为自动评审的初步意见,仅供参考,最终以维护者审阅为准。再次感谢贡献 🙏

@deepcoldy

Copy link
Copy Markdown
Owner

感谢按建议重做!09a608cd3 这版增量比原先提的两个方案都更干净,自动复审确认之前的问题已闭合:

P1(/repo 空启动被打破)已修好

  • /repocmdPending 回退成原样,不再注入 chatContext,「the CLI just boots and waits」注释重新成立;
  • 新增显式的 pendingOpeningKind + hasPendingOpeningTurn()(不含 pendingChatContext),把「这一轮是否拥有开场」与「哪条指令准备了路由」解耦——chat context 再也不能凭自己制造出一个开场回合;
  • chatContext 改为在 commit 边界惰性 resolve,setup-only 路线零 Lark 读、零副作用(bare /t 白打 getChatModeStrict 的问题也一并解决);
  • wantsOpeningdaemon.ts:20267)已补上 resolve + 传参,所以 /repo 恢复空启动后下一条 opening 仍自带 chat_mode,不会丢。

另外几处超出建议范围、但方向正确的收敛也确认过了:重启恢复路径新增的 setup-only 分支(空启动契约跨重启同样保住)、forkWorker 改为检查返回值且被拒时不再先污染 pendingFollowUpInput/rememberLastCliInputemptyStart 不再占 turnId 且两处 twin 对称、以及把原先 3 处手抄的判据统一到一个谓词(顺带修掉 daemon.ts:17019pendingCodexAppText 的潜在不一致)。

新增的 does not let chat context alone manufacture an ordinary opening turn 这条测试尤其到位——把之前那个「手工构造 ds 时漏设 pendingChatContext 所以照样绿」的覆盖缺口补上了。本地反向变异验证:把该问题原样引回后这条测试会转红,另外拆掉 wantsOpening 接线、破坏重启恢复分支也各有对应测试转红,说明断言都有实际约束力。

验证结果(试合并到当前 master tip 后):pnpm build 0 错;相关 10 个测试文件 830/830 通过;daemon-rename-route 的 2 个失败经与干净 master 对照确认为既有失败(/workflow new NEW-TOPIC),与本 PR 无关。

唯一待办:需要 rebase(目前 GitHub 显示 CONFLICTING)。分支落后 master 约 82 个 commit,但实际冲突只有 1 处:test/command-handler.test.ts 里 master 新增的 cliLaunchSnapshot 用例与本 PR 新增的 chatContext 用例落在同一位置、共用了闭合括号,保留双方两个 it() 即可,属机械冲突而非逻辑冲突(本地这样解完全绿)。

以上为自动评审的初步意见,仅供参考,最终以维护者审阅为准。辛苦了 🙏

@deepcoldy

Copy link
Copy Markdown
Owner

先说明:R2(09a608cd3)的工程质量没有问题,pendingOpeningKind 那套解耦做得很干净,测试也确实有约束力——这一点上次已经确认过了。

这次是想请你补充一下使用场景,因为维护者在评审时提了一个更前置的问题:注入 <chat_mode> 之后,AI 有哪个行为会因此变得不同?

我们顺着这个问题查了一遍会话形态真正影响到的路径,结果是大部分似乎已经被覆盖了:

  • botmux history:skill 描述里已经写明它按当前 session scope 自动处理(话题/thread 会话拉话题内,普通群 chat-scope 拉整群最近 N 条),AI 不需要先知道 chat_mode;
  • @ 策略--mention / --mention-back / --no-mention 是按内容价值三选一,与群类型无关;
  • 只在普通群可用的能力(私密卡片 / /reply-mode / /substitute):运行时本身就 fail-closed 并回明确文案(例如"私密卡片仅支持普通群聊,话题群 / 单聊无法发送"),不依赖 AI 提前判断。

所以想请教:**你遇到的具体场景是什么?**比如"AI 因为不知道当前是私聊/普通群/话题群,所以做错了 XX"这类实际现象。如果有这样的 case,那这个字段的价值就成立了,我们也会据此改判;如果暂时没有,维护者倾向的方向是拆分——

  • PR 里修正固定提示"你在飞书话题群中"这个误导表述的部分价值很明确(在私聊和普通群下原文案确实是错的),这部分只涉及两个 i18n 字符串,风险为零,建议单独提一个小 PR 先合;
  • <chat_mode> 注入的部分先搁置,等场景明确后再推进。主要顾虑是:为了让它正确落地,R2 需要动到 opening 生命周期(新增 pendingOpeningKind、收敛三处 opening 判据、改 forkWorker 接受语义、调整重启恢复分支),而这些是 /repo、选仓卡、重启恢复等最容易产生静默缺陷的路径(R1 的空启动回归就来自这一层);另外每次群聊首轮会多一次 chat.getgetChatModeStrict 有意不读缓存),这是全 fleet 的热路径。

如果方便的话,欢迎进群直接聊,沟通会快很多:

https://applink.larkoffice.com/client/chat/chatter/add_by_link?link_token=cb7jd70a-bd95-41d7-bdce-733e5c4f3f70

以上为自动评审的初步意见,仅供参考,最终以维护者审阅为准。感谢你在这个 PR 上的投入 🙏

@deepcoldy

Copy link
Copy Markdown
Owner

感谢补充场景!你说的这个痛点是真实的,也把问题定位得更准了:

话题群的搜索历史应该限定在话题里,普通群可以扩展到群组。现在搜索范围有点模糊,经常容易出问题
thread 模式下经常只在话题内搜索,但很多上下文在群组里,它拿不到

顺着这个场景我们查了一遍实现,发现真正的缺口和这个 PR 目前的做法之间有个偏差,想跟你同步一下——痛点不在「模型不知道群类型」,而在「模型不知道该把 history 的搜索范围放多大、以及怎么安全放大」。具体三点:

1) 目前注入的内容没有告诉模型该怎么做

实际渲染出来是这样:

<chat_context source="lark" trust="untrusted" fetch_status="ok">
  <chat_id>oc_xxx</chat_id>
  <chat_mode>topic</chat_mode>
</chat_context>

它只声明了「这是话题群」,但没有任何一处提到 --scope ambient —— 而那正是你的场景所需要的动作。所以即使注入了,模型也不会因此学会扩大搜索范围,thread 里拿不到群上下文的问题大概率照旧。

2) 更关键:决定搜索范围的是 sessionScope,不是群的 chat_mode

src/cli.ts--scope ambient / --scope thread 的可用性判的是 isChatScope(即 session 自身的 scope),与群的 chat_mode 无关——chat-scope 会话下 ambient 会直接报错「没有 thread root 可作为 ambient 边界」(src/cli.ts:6708)。

这两者并不等价:普通群里用 /t(或 per-bot 的普通群开话题回复配置)开出来的话题,群的 chat_mode 仍然是 group,但 session 是 thread。于是模型若按 chat_mode=group 推断「我可以搜整个群」,在这种 /t 话题里就会推错。也就是说这个字段在该场景下不仅用不上,方向上还可能误导。

3) botmux history 已经免费拿到了更准的信息

它的返回里已经带了(src/cli.ts:6781-6782):

{ "sessionId": "", "chatId": "oc_…", "scope": "chat", "sessionScope": "chat", "messages": [ ] }

sessionScopechat / thread)来自 session 自身状态,零 API 调用;而 chat_mode 需要额外打一次 chat.getgetChatModeStrict 有意不读缓存),且落在每次群聊首轮这个全 fleet 热路径上。缺的其实只有一句话:没有告诉模型"当前能不能扩大、怎么扩大"

建议的方向(改动比现在小很多)

history 输出里按当前 sessionScope 补一条可执行提示,位置就在现有那个 hint 旁边(src/cli.ts:6796):

  • sessionScope=thread → 说明本次只返回当前话题内消息;需要话题外的群聊背景时用 botmux history --scope ambient --limit 20(读 thread root 之前的群消息、排除当前话题),并提示注意隐私边界、优先用较小的 limit;
  • sessionScope=chat → 说明本次返回整群最近 N 条,需要更早的调大 --limit(并说明 ambient 在此不适用,省掉模型试错报错的那一次)。

这样是给已有 JSON 加一个字段,零新 API、不涉及 opening 生命周期;信息也出现在模型正要用它的那一刻,而不是首轮塞进 prompt 等它自行联想。相比之下,为了让 <chat_mode> 注入正确落地,09a608cd3 需要动 pendingOpeningKind、收敛三处 opening 判据、改 forkWorker 接受语义、调整重启恢复分支(18 文件 +574),风险面落在 /repo、选仓卡、重启恢复这些容易产生静默缺陷的路径上(R1 的空启动回归就出自这一层)。

所以想请你考虑调整方向:把 <chat_mode> 注入这部分收掉,改成在 history 输出加 scope 提示来解决你遇到的问题。你有真实使用场景,提示词该怎么写你比我们更清楚,所以想先把这个方案交回给你。另外 PR 里修正固定提示"你在飞书话题群中"误导表述的那两个 i18n 字符串价值明确(私聊和普通群下原文案确实是错的),建议保留或单独提一个小 PR 先合。

R2 的工程质量本身没有问题(pendingOpeningKind 那套解耦做得很干净、测试也确实有约束力),这次是方向上的建议,不是实现上的问题——辛苦你了 🙏

以上为自动评审的初步意见,仅供参考,最终以维护者审阅为准。

@deepcoldy

Copy link
Copy Markdown
Owner

按上面讨论的方向,我把实现直接做出来了,方便你参考或直接拿走:分支 feat/history-scope-hint(基于当前 master,单个 commit eb5109ba9)。

master...feat/history-scope-hint

改了什么botmux history 输出新增一个 rangeHint 字段,按 (sessionScope, effectiveScope) 四种组合给出可执行提示——

  • 话题会话(默认):说明本次只返回话题内消息;若上下文在话题之外,给出 botmux history --scope ambient --limit 20(读话题根之前的群聊消息、自动排除本话题),并带上隐私边界提醒(仅在确实需要群聊背景时用、优先小 limit);
  • chat-scope 会话:说明返回整群最近 N 条、需要更早的调大 --limit,并明确 --scope thread / --scope ambient 在此不适用——省掉模型试错、撞到 process.exit(1) 报错的那一次;
  • --scope ambient 结果:说明这批是话题之外的消息、怎么回到话题内;
  • 话题会话显式 --scope chat:说明结果含本话题内消息。

判据用 sessionScope 而不是群的 chat_mode,理由就是前面提的那点:普通群里用 /t 开出的话题,群 chat_mode 仍是 group 但 session 是 thread-scope,按群类型推断会得出「可以搜整群」的错误结论;而 cmdHistory 里两处 --scope 门本身也是按 isChatScope 判的,保持一致。提示放在工具返回里而非首轮 prompt,信息出现在模型正要用它的那一刻,也不需要额外的 chat.get

影响面:只在 cmdHistory 输出加一个字段,不改检索逻辑、不动既有 --scope 门,其它 CLI / 后端 / 会话类型不受影响(+13 行 / 1 个新测试文件)。

验证bun run build 通过;新增 test/history-range-hint.test.ts 7 项全绿,与 builtin-skills / dispatch / bots-list / topic-root-context 合跑 5 文件 129 项全绿;本机 chat-scope 会话实跑确认 rangeHint 正确下发,四种组合逐一驱动编译产物校验文案。反向变异 3 组都会转红:删掉 thread 支的 ambient 指引(1 红)、把分支合并成同一句(4 红)、去掉隐私边界那句(1 红)。

怎么用都行——你可以 cherry-pick 到本 PR、按自己的想法改文案(提示词具体怎么写你比我们清楚),或者我们单独提一个 PR 走。这个分支我只是推上去供参考,没有开 PR、没有动你的 fork、也不会合码,合不合、怎么合都由维护者定。

如果采用这个方向,本 PR 里 <chat_mode> 注入那部分建议就可以收掉了;两个修正「你在飞书话题群中」误导表述的 i18n 字符串仍然值得保留(私聊和普通群下原文案确实是错的)。

以上为自动评审的初步意见,仅供参考,最终以维护者审阅为准。辛苦 🙏

deepcoldy and others added 2 commits August 28, 2026 04:07
话题会话里模型常只搜话题内消息,而所需上下文在群聊里拿不到;普通群会话
又不清楚能回溯多久。history 返回里已有 sessionScope,但没有一句告诉模型
"当前能不能扩大、怎么扩大",所以模型不会主动用 --scope ambient。

在 history 输出加 rangeHint,按 (sessionScope, effectiveScope) 四种组合给出
可执行提示:话题会话说明只返回话题内并给出 `--scope ambient --limit 20`
及隐私边界;chat-scope 说明返回整群最近 N 条、调大 --limit,并说明
ambient/thread 在此不适用(省掉模型试错报错那一次)。

判据用 sessionScope 而非群的 chat_mode:普通群里开出的话题,群 chat_mode
仍是 group 但 session 是 thread-scope,按群类型推断会得出"可以搜整群"的
错误结论;cmdHistory 里两处 --scope 门本身也是按 isChatScope 判的。提示放在
工具返回而非首轮 prompt,信息出现在模型正要用它的那一刻,且无需额外
chat.get 调用。

措辞只描述模型能观测到的现象:daemon 侧的开话题指令在拼 prompt 前已被剥离,
模型从未见过该 token,写进提示等于给一条它无法执行的指令;因此改用症状描述
(话题内容不足以说明任务背景、出现没有出处的指代或结论),并加断言防回归。

影响面:仅 cmdHistory 输出新增一个字段,不改检索逻辑、不动既有 --scope 门,
其它 CLI / 后端 / 会话类型不受影响。

验证:
- bun run build 通过(tsc 0 错)
- 新增 test/history-range-hint.test.ts 8 项全绿;与 builtin-skills /
  dispatch / bots-list / topic-root-context 一起跑 5 文件 130 项全绿
- 实跑本机 chat-scope 会话确认 rangeHint 正确下发;四种组合逐一驱动编译产物
  校验文案
- 反向变异 4 组均转红:删 thread 支的 ambient 指引(1 红)、把分支合成同一句
  (4 红)、去掉隐私边界那句(1 红)、把开话题指令措辞写回提示(1 红)
`ai.routing.intro` / `ai.shell.intro` / `ai.followup.reminder_hook` 原文写死
「你在飞书话题群中」,但同一套提示也会下发给单聊和普通群 chat-scope 会话,
在那两种形态下这句话是错的。改为中性表述「飞书(Lark)会话」。

影响面:仅三条 i18n 文案(zh/en 各三条),不改任何逻辑;所有 CLI 与会话
类型共用这套提示,改后在话题群下语义不变、在单聊/普通群下不再误导。

验证:bun run build 通过;仓库内无测试断言旧文案。
@deepcoldy
deepcoldy force-pushed the feat/chat-mode-context branch from 09a608c to 811e528 Compare August 28, 2026 11:09
@deepcoldy deepcoldy changed the title feat(lark): 向首轮上下文注入会话模式 feat(history): 按会话范围提示搜索边界与扩大方式 Aug 28, 2026
@deepcoldy

Copy link
Copy Markdown
Owner

经维护者确认方向后,我把本 PR 的分支直接更新为按 botmux history 输出提示的实现,并同步改了标题和描述(head 现为 811e52876,已 rebase 到当前 master,冲突消除)。

保留了你的成果:修正固定提示「你在飞书话题群中」误导表述那部分单独成一个 commit,作者署名仍是你fix(i18n): 固定提示不再断言所有飞书会话都是话题群)——这个问题是真实存在的,单聊和普通群 chat-scope 会话都会收到那句话。

移除的部分<chat_mode> 注入,以及为让它正确落地而做的 pendingOpeningKind 生命周期改造。原因在前面的评论里详述过,核心是两点:注入的字段没告诉模型该做什么(整块提示里不含 --scope ambient),而决定检索范围的判据是 sessionScope 而非群的 chat_mode(普通群里开出的话题两者不一致,按群类型推断会得出错误结论)。

你提的痛点用另一种方式解决了botmux history 输出新增 rangeHint,在模型正要读历史的那一刻按会话范围告诉它边界和扩大方式——话题会话里明确给出 botmux history --scope ambient --limit 20(带隐私边界提醒),chat-scope 则说明 ambient/thread 不适用,省掉模型试错撞报错那一次。

你原来的 R2 实现(09a608cd3)工程质量是好的,pendingOpeningKind 那套解耦做得很干净、测试也确实有约束力——只是方向上被替换掉了,这一点很抱歉。如果你对新的提示文案有更好的写法(你有真实使用场景,比我们更清楚模型在什么情况下会误判范围),欢迎直接在这个分支上改或提出建议。

现交由复审确认后由维护者决定合入。以上为自动评审流程的一部分,最终以维护者审阅为准。感谢你在这个问题上的投入 🙏

@deepcoldy
deepcoldy merged commit 30bda74 into deepcoldy:master Aug 28, 2026
@github-actions

Copy link
Copy Markdown

🚀 Released in v3.18.1

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.

2 participants