Files
Jyotisha/docs/tasks/TASK-rectification-duplicate-narration-20260907.md
T

95 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK · 生时校正修复单:同一轮旁白说两遍、题干在正文与问题块各出现一次(2026-09-07)
- 基线:`origin/staging` @ `f3e72702`(代码头 `ab57d03f`,含 BUG-583 未修的 tsc 错;staging 已部署 `84a29754`,事故在多轮实测中稳定复现)
- 分支:`codex/rectification-duplicate-narration-20260907`,基于 `origin/staging`。与 `codex/rectification-range-delivery-fix-20260907`BUG-583,只改 `rectification-agentic-chat.tsx`)无文件重叠,可并行;谁后合谁 rebase。
- 执行方:coding agent;验收:Claude
- 涉及文件:`frontend/src/lib/rectification-agentic/v9/step-answer.ts``v9/agent-run.ts``v9/collect-prompt.ts``v9/turn-question.ts``frontend/tests/rectification-step-answer.test.ts``rectification-collect-prompt.test.ts`
- BUG 编号起点:**BUG-584**BUG-583 已由区间卡修复单占用;开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核)
- 优先级:**P1**(每一轮采集都复现;用户读到两段近似旁白 + 两遍题干,以为系统出错)
## 1. 事故实证(staging,多次实测;只写结构)
一条助手消息自上而下是:
1. 旁白第一段:承接用户刚说的事,说范围/对照情况,**末尾把下一题问了一遍**(自然语言,含年份与领域,以「?」结尾)。
2. 旁白第二段:换个说法把第一段又说一遍(有时更短,有时又带一遍题)。
3. 「本轮对照了 …」方法句(客户端 `vargaSentenceFromMethods` 渲染)。
4. 问题块:服务端 `ConversationFocus.spokenPrompt` 的题干,与第 1 段末尾那句同义或逐字相同。
## 2. 根因(两条,叠加)
### 2.1 BUG-584P1):set-focus 前后两个 step 的正文都被放行并拼接
`step-answer.ts` 的规则:文本先于 public 工具调用的算「规划稿」,工具调用时 `retractLive` 撤回。但 `KEEP_LIVE_SPOKEN_TOOLS = {rectification-set-focus}` 是例外(`a793edb1`,BUG-533:为了不把开场问候撤掉)——set-focus 之前已直播的中文保留,set-focus `tool-result``resetBuffers` 不撤回;随后模型在**下一个 step** 看到 set-focus 结果再写一段正文,`step-finish(reason=stop)``shouldPublishStepText` 正常发布。`agent-run.ts``publishSpokenStep``spokenRaw += spoken`,两段正文无分隔、无去重直接拼成 `answerText`,落库为 `assistant_message`
固定这个行为的正是测试 `rectification-step-answer.test.ts::keeps a live opening greeting when the next public tool is set-focus`:它断言 set-focus 之后的第二段也 `live` 发布。开场轮两段短句无伤;证据轮模型在 set-focus 前先总结一遍、拿到结果后又总结一遍,就是用户看到的"说了两遍"。receipt 里每轮两个 `intent.classified` 是同一件事的旁证。
附带损失:set-focus 前尚未触发直播(没遇到句末标点)的文本、以及已直播部分之后 `publishedUpTo` 以外的尾巴,在 `tool-result``resetBuffers` 里被静默丢掉——这是偶发"旁白半句"的来路。
### 2.2 BUG-585(P1):题干去重只认逐字后缀,模型写进正文的问句剪不掉
服务端只在 `turn-question.ts::attachQuestionsToTurns``detachCollectSpokenAssistantText(turn.text, question.prompt)` 剥题干,而它只剥两种形态:正文整体 === 题干,或正文以 `\n\n${题干}` 结尾。模型自己在正文里问出来的句子(同义改写、或逐字但嵌在段落中间)一概保留;问题块再从 focus 渲染一遍 → 同一题两遍。`composeCollectSpokenAssistantText` 里那套「按题干前 12 字剪句」只用于拒答确认句(`answer-choice.ts` L1059),agent 路径根本没走。
不让模型提问的约束只有提示词:`SKILL.md` L59「正文……不得自行提出、复述、改写或预告问题」、set-focus 描述「你只写 spokenPrompt」、opening brief「不要提问」。BUG-488 的防复发写的是「不得同时让模型提问又拼接同一句服务端题干」,靠"模型仍不自己提问"兜住;`d9404976` 把题干改成消息内问题块后,这条防线没有任何代码守卫,于是复发。
另外 `agent-run.ts` L284 `const collectSpokenEmitted = false` 是常量,只被原样透传进结果,没有任何读者——它就是当年打算做"题干已出、正文别再出"守卫的残骸。
## 3. 决策记录
1. **有服务端问题的轮次,正文不得含问句;含则服务端剪掉。** 这是把 SKILL.md L59 从提示词升级为代码守卫:`answer.composed` 之前,若本轮 set-focus 成功(focus.askedTurnId = 本轮),对 `answerText``stripQuestionSentences(body, stem)`:按 `。!?\n` 切句,删掉 (a) 与题干相同、(b) 以题干前 12 字开头、(c) 以「?」/「?」结尾 的句子。剪空则用一句固定承接(`RECTIFICATION_USER_COPY` 新增 `collectHandoff`,对照 `VOICE.md`,不得说"请回答下面的问题"之类机器话)。剪后文本以 `answer.delta replace:true` 再发一次,保证直播与落库一致。选择题轮(`choice_frame`)同样适用——选项由卡片承载,正文不问。
2. **set-focus 之后不再接受新一段正文。** `step-answer.ts`set-focus `tool-result` 后置 `state.stemAttached = true`(随 turn 存活,不随 `resetBuffers` 清);此后 step 的 `text-delta` 只有在本轮此前**没有**发布过任何正文时才放行(保住 BUG-533 的开场问候路径:问候在 set-focus 前,或模型只在 set-focus 后说一句),否则 `discard`。两段都有时保留**前一段**——它是模型对刚才证据的直接回应;后一段只是对工具结果的复述。
3. **set-focus 前的未直播文本不得静默丢弃。** `tool-call(set-focus)` 时把 `state.text` 未发布部分作为 `publish` 效果发出(它是给用户的话,不是规划稿),再 `resetBuffers`。若该文本没有 CJK(英文规划稿),维持原 discard。
4. 删除 `collectSpokenEmitted` 常量与其类型字段。
5. 提示词不动、Skill 不 bump(不是口径变化,是守卫补齐)。不动采用门、确认门、`MIN_SEPARATION_LEAD``_relative_support``minute_step=1` 指纹;不动 `frontend/src/app/page.tsx`
## 4. 任务分解
### 4.1 BUG-584:单段正文
- `step-answer.ts` 按决策 2、3 改;`StepAnswerState``stemAttached``publishedAny`(后者由 `agent-run.ts` 在首次 `publishSpokenStep` 成功后写回,或由 state 自记)。
- 改既有测试 `keeps a live opening greeting when the next public tool is set-focus`:原值——set-focus 后第二段 `live` 发布;新值——第二段 `discard`;原因——决策 2。新增:(a) set-focus 前无正文、后有一段 → 后段 `live/publish`BUG-533 路径仍通);(b) set-focus 前有未闭合半句「范围已经收到 05:00 到 05:10」(无句末标点)→ `tool-call(set-focus)` 时以 `publish` 发出,不丢;(c) 英文规划稿 + set-focus → 仍 discard。
- `agent-run` 级:`rectification-v9-agent-run*.test.ts`(现有 mock stream 夹具)构造「文本 → set-focus → 文本 → finish」序列,断言 `answerText` 只含第一段,`answerDeltas` 无第二段,且没有 `replace:true` 的清空事件。
### 4.2 BUG-585:正文剪问句
- `collect-prompt.ts` 新增 `stripQuestionSentences(body, stem)`(复用 `composeCollectSpokenAssistantText` 的切句与前缀规则 + 问号规则),`composeCollectSpokenAssistantText` 内部改调它,行为对拒答句路径不变(既有 `rectification-collect-prompt.test.ts` 全部保持)。
- `agent-run.ts`:流结束、`answer.composed` 之前,读本轮 focus`loadV9CaseDossier` 已在 `discriminatorInvariant` 里做过,复用那次读取,不加第二次 RPC);`askedTurnId === turnId` 且 schema 里有 spokenPrompt → 剪;剪空 → `collectHandoff`;变化时 `emitVisibleSpoken(stripped)`(它自会发 `replace:true`)。
- `turn-question.ts::attachQuestionsToTurns` 同样改调 `stripQuestionSentences`,让历史消息回看也干净(老数据不迁移)。
- 验收:单元——(a) 正文「…范围收在 05:00–05:10。你大概哪一年搬过家?有年份就行。」+ 题干「你大概是哪一年搬的家?」→ 剪成「…范围收在 05:00–05:10。」;(b) 正文中段逐字含题干 → 剪掉那句,前后句保留;(c) 正文只有问句 → `collectHandoff`;(d) 无本轮 focus → 原样。合同——`agent-voice-copy-contract` 增加「有 focus 的已结算助手正文不得以『?』结尾的句子出现」(对 `docs/testing` 之外的夹具跑)。
### 4.3 记录
- `docs/BUG_HISTORY.md` BUG-584(关联 BUG-533:其修复放行了第二段)、BUG-585(复发自 BUG-488:防线只在提示词;关联 BUG-461、BUG-471、`d9404976`);`CHANGELOG.md` 一条;`PROGRESS-rectification-duplicate-narration-20260907.md``docs/testing/rectification-scenarios-20260907.md` 加一条「每轮助手气泡只有一段旁白,题干只在问题块出现一次」。
## 5. 让步顺序
4.2 是用户直接看到的"两遍题",先做;4.1 次之;决策 3(半句不丢)可后置为 P3 但要在进度记录里写明;4.3 不可省。
## 6. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-duplicate-narration-20260907 .worktrees/rectification-duplicate-narration-20260907 origin/staging
cd .worktrees/rectification-duplicate-narration-20260907
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && ./node_modules/.bin/tsc --noEmit; npm run lint 2>&1 | tail -1
ls tests/rectification-*.test.ts tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"
```
开工时 tsc 若仍报 `rectification-agentic-chat.tsx` L1655BUG-583),先确认 `codex/rectification-range-delivery-fix-20260907` 是否已合入;未合入则本单不修它,进度记录里写清基线含该错。
## 验收(Claude2026-09-08`origin/staging` @ `df182c16`staging 已部署同 SHA
| 门 | 结果 |
| --- | --- |
| tsc | 0 错 |
| lint | 0 error / 102 warning |
| 前端 rectification + consultation + voice + skill-registry(非 DB | 1253 / 0 |
| Python v5_services + growth contract | 全绿(执行方记的 15s 壁钟用例本机未复现超时) |
| Docker DB 套件、浏览器走查 | 环境缺口,同执行方记录 |
| 项 | 结论 |
| --- | --- |
| 4.1 BUG-584 | 通过。`stemAttached`/`publishedAny` 守卫;set-focus 后第二段 discard;半句 publish;三栏齐全。2026-09-08 真实 Case 每轮只剩一段旁白 |
| 4.2 BUG-585 | 通过。`stripQuestionSentences` 在 agent-run 与 `attachQuestionsToTurns` 两处生效,`collectSpokenEmitted` 删除。真实 Case 正文不再含问句 |
| 4.3 记录 | 通过 |
结论:通过。