fix(desk): 保存那一拍对话区也别出声 —— 「在办事」不亮、say 落工作日志不上对话区

用户:「每次保存的时候右边也会出现 ai 推进中,然后会说一句话,能不能把这个也取消掉」

两处,同一个根:件上按保存会 wake 这一单(「保存即铺格」),runtime 被拨成 running。

① 右边那条办事条:`TaskChat` 的 `working` 只看 runtime === 'running'。它自己的注释
早写着「等人、等外部、已收尾都不算 —— 那时亮『工作中』就是撒谎:环没在转,是在等」——
save-wake 恰恰把「在等人」的单拨成了 running,把这条原则架空了。加 `!awaitingHuman(...)`
是**恢复**那句注释说的语义:有未办结工单 / 未回的卡就是在等人,哪怕 runtime 写着 running。
判断与工作区那一侧同一个函数。

② 那句话:quiet 拍只跑 project 与 say,于是人按几次保存,同一句交代往对话区说几遍。
对话区的纪律是**一轮一句,不是一拍一句**(2026-08-01):只有交出控制权那一拍的交代
摆到人面前;而 quiet 拍压根没交出控制权(人正对着卡填),算不上新一轮。
改:quiet 拍的 say 标 `internal`。压下去不是丢掉 —— `internal` 是两个投影共用的一个旗
(终审 I5/F2):`chat-projection` 据此挡住对话区,`task-log.machinery()` 对 `detail.auto`
开了例外,这一笔在工作日志里照旧看得见(话有插值时值的变化也查得到)。

测:quiet 的 say 标 internal 且工作日志仍在(3 条变异各红一条 —— 退回原样 / 反过来连
非 quiet 也标 / 连 auto 一起去掉;其中「反过来标」红的是既有那条「非 quiet 不标」,
两条互为护栏)。后一半(标了 internal 就进不了对话区)由 chat-projection 既有测钉着,
projectChat 没出模块边界,两半各钉在自己模块里、注释互指。
pnpm gates 退出码 0(2494 测)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-31 11:16:09 +08:00
co-authored by Claude Opus 5
parent c8988c912f
commit 2265ab7833
3 changed files with 64 additions and 3 deletions
@@ -465,6 +465,38 @@ describe('配方留痕真的到得了工作日志(spec §5.9 验收 · R10)', ()
expect(appended[0]?.detail?.['internal']).toBeUndefined();
});
/**
* **人正在件上填的那几拍(quiet),say 不上对话区**(2026-08-31 用户:「每次保存的时候
* 右边也会出现 ai 推进中,然后会说一句话,能不能把这个也取消掉」)。
*
* 件上按保存会 `wake` 这一单(「保存即铺格」),配方跟着跑一趟 quiet —— 而 quiet 只跑
* project 与 say,于是人按几次保存,同一句交代就往对话区说几遍。对话区的纪律是
* **一轮一句,不是一拍一句**(2026-08-01),而 quiet 拍压根没有交出控制权(人正对着
* 那张卡填),算不上新一轮。
*
* 压下去不是丢掉:`internal` 是两个投影共用的一个旗(终审 I5/F2)。这一条钉住的是
* **前一半**(quiet → 标 internal,而工作日志按 `detail.auto` 照旧放行);
* **后一半**(标了 internal 就进不了对话区)钉在
* `apps/api/src/modules/interaction/__tests__/chat-projection.test.ts`
* 「引擎内部观察(internal 标)进日志不进对话」那一条 —— `projectChat` 没出模块边界,
* 跨模块深入 internal 会被 depcruise 拦,两半各钉在自己模块里。
*/
it('**quiet 拍的 say:标 internal(对话区据此挡住),而工作日志照旧看得见**', async () => {
const { runner, appended, ctx } = harness({ n: 2 });
await runner.run(
{ do: [{ kind: 'say', text: { cat: ['已核对 ', { in: 'n' }, ' 颗料'] } }] },
{ ...ctx, quiet: true, carded: true },
);
expect(appended).toHaveLength(1);
expect(appended[0]?.detail?.['internal']).toBe(true);
/* 话本身没丢:`detail.auto` 那条例外让它留在工作日志里(值有插值时也查得到) */
expect(appended[0]?.title).toBe('已核对 2 颗料');
expect(typeof appended[0]?.detail?.['auto']).toBe('string');
const lines = structuredLog([asLogEntry(appended[0]!)]);
expect(lines).toHaveLength(1);
expect(lines[0]?.who).toBe('system');
});
it('project 那一笔:进日志且归系统,**而且标着 internal(对话区那一侧据此挡住)**', async () => {
const { runner, appended, ctx } = harness({});
await runner.run(