Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
54 lines
6.8 KiB
Markdown
54 lines
6.8 KiB
Markdown
# TASK · 生时校正:同一轮问题出现两次 + 同一道选择题连画两张(2026-09-26)
|
||
|
||
## 基线
|
||
|
||
- `origin/staging` 当前 head(`a57c4310` 代码;BUG-1042 修复单合入后以新 head 为准)。分支 `codex/rectification-dup-question-20260926`。
|
||
- 与 `TASK-rectification-latency-20260926`(改 route / agent-run / 校正面活动状态)**冲突**:本单先做,那单后做。与 `TASK-scroll-anchor-hook-fixes-20260926` 不交叉。
|
||
|
||
## 事故实证(产品 09-26 真机,staging)
|
||
|
||
**A · 同一轮里问题出现两次**:用户打字回答后,助手这一轮先是正文「已记录,范围没变;14:58–14:59 领先,14:40–14:43 落后。」「2024 年 3 月前后,有没有收入明显变化、大笔支出或欠债?」,下面又是加粗的同一句问题块和选项。刷新后只剩一遍。
|
||
|
||
- `frontend/src/lib/rectification-agentic/v9/answer-choice.ts` `persistApplied`:下一问是选择题时 `hostNarration = composeCollectSpokenAssistantText(body, choiceStem)`,把题干拼进 `assistant_message` 并作为 `applied.narration` 返回;`persistCollectDenialTurn` 同样。
|
||
- `frontend/src/app/api/rectification/agent/route.ts`:打字回答命中待答选择题(`action === "message"` + `choice` 分支)→ `applyRectificationChoice` → `completedMessageResponse(applied.narration)` 整段作为一个 `answer.delta` 下发;不经模型 runner,`agent-run.ts` 里 BUG-585 的 `stripQuestionSentences(...)` 不执行。
|
||
- `rectification-agentic-chat.tsx` `send` 结算后保留原始流式正文;`loadCaseSnapshot` → `frontend/src/lib/rectification-snapshot-messages.ts` `mergeTurnQuestions` 只拷 `question` / `candidateOffer`,**不替换 `text`**。`rectification-message-entry.tsx` 先渲染 `message.text` 再渲染 `question.prompt` → 两遍。去重只在 GET 组装的 `attachQuestionsToTurns`(`turn-question.ts`)里,所以刷新后消失。
|
||
- 排查已在 tsx 里复现:合并快照后正文仍含题干;`stripQuestionSentences` 对同一文本能正确去掉。
|
||
- **复发**:BUG-585(修复 `9d1c08ca`,防复发"不得恢复把题干拼进 assistant_message")被 **BUG-969 第③项**(`824ecff0`,2026-09-18)带回:为防卡片加载失败时看不到题而拼回题干,声称"挂卡后去重",但测试(`account-dialog-inert-20260918.test.ts`)只覆盖刷新路径,并把 `rectification-answer-choice.test.ts` 断言改成期望题干在正文里。
|
||
|
||
**B · 同一道选择题连画两张**:用户在第 N 轮点选 **B 发生过但程度较弱**(高亮),其下是操作按钮、「目前范围 14:35-15:05,还在核对」,再下面又出现完全相同的题干和四个选项(未选中)。
|
||
|
||
- 第二张不是新一轮助手回答,而是独立的"持久化问题块":`rectification-agentic-chat.tsx` 中 `questionGap === "persisted_question"` 时渲染 `currentQuestion.prompt` + 新的 `RectificationChoiceCard`(`selectedKey=""`)。条件是 `rectificationQuestionGapState` 判定"没有任何消息挂着与 `currentQuestion.focus_id` 匹配的活题,但快照里仍有持久化问题"。同类:BUG-675 / 678(「目前范围」下挂裸题);BUG-678 意图"只在没有助手消息时兜底",但闸门没强制。
|
||
- 两个假设(需真实会话区分):
|
||
- **H1(更可能)**:`submitStructuredChoice` 先 `markQuestionAnswered`(本地标 B)再 POST `/api/rectification/agent`(`action: "answer_choice"`);`!response.ok` / `catch` 只移除占位、`choiceNonce++`、`setError`,**不撤回已答标记、不重载快照** → 活题判定落空 → 兜底块再画一张。服务端可能的错误:`stale_question` / `revision_conflict` / `stale_probe` / 500。
|
||
- **H2**:POST 成功但 `nextInterviewPersisted` 为假且 `nextAction` 为 `ask_candidate_discriminator` 的 `willContinue` 分支只跑 `mergeTurnQuestions`、没跑 `appendUnseenAssistantTurns`(BUG-685 在 `requestTieBreak` 修过同型缺口),承载下一问的那轮没进客户端,兜底块替它画了出来;若题目完全相同,还说明选题重复了(BUG-540 / 559 / 592 守卫仍在代码里,需确认)。
|
||
|
||
## 根因
|
||
|
||
A:服务端为兜底把题干拼进正文,客户端在实时路径上没有"挂卡即去重"。B:选择题提交失败不回滚本地状态 + 兜底问题块的出现条件过宽(H2 另有漏收回合)。
|
||
|
||
## 决策记录
|
||
|
||
- D1 保留 BUG-969 的兜底意图(卡片没挂上时用户仍能在正文看到题),但**卡片一挂上就去掉正文里的同一句**:`mergeTurnQuestions` 在快照回合带问题时,用服务端已去重的 `turn.text` 覆盖,或对 `message.text` 执行 `stripQuestionSentences(text, question.prompt)`;实时流里若同一轮已收到问题卡,也立即去重。
|
||
- D2 选择题提交失败:撤回本地已答标记(题目回到 active、无 `answer_option`)、重载快照、只保留**一张**可点的卡 + 错误提示「这次没提交上,请再点一次。」(对照 VOICE)。
|
||
- D3 兜底问题块只在**没有任何助手消息**时渲染;已有助手消息时把问题挂到最新助手消息上(BUG-678 原意),且同 `focus_id` 的题永远只画一张。
|
||
- D4 H2 分支:`willContinue` 与 `send()` 结算后的快照合并同时执行 `appendUnseenAssistantTurns`(同 BUG-685)。若复现确认服务端真出了重复题,另开 BUG 关联 BUG-540 / 592,不在本单改选题。
|
||
|
||
## 硬红线
|
||
|
||
1. 回归测试必须走**实时路径**:流式收到"确认句 + 题干"→ 合并快照 → 断言题干在正文与问题块合计只出现一次;打字回答命中选择题、采集拒答两条路径各一条。
|
||
2. 提交失败的回滚测试:模拟 409 `stale_question` 与网络错误,断言页面只有一张可点卡 + 错误提示,已答标记被撤回。
|
||
3. 不改服务端选题与打分;不改 BUG-969 的服务端兜底文本(只改客户端去重)——若必须改服务端,写明理由。
|
||
4. 全量测试与基线逐条一致、新增 0;改动断言写三栏(含把 BUG-969 改成"期望题干在正文"的那条,改回并说明);真实浏览器截图(CDP 假数据)验证 A、B 两个场景。
|
||
5. 不改 `useConversationScrollAnchor`(另一单在改)。
|
||
|
||
## 任务分解
|
||
|
||
- T1 实时去重(D1)+ 测试。
|
||
- T2 提交失败回滚(D2)+ 兜底块闸门(D3)+ 测试。
|
||
- T3 H2 漏收回合(D4)+ 测试。
|
||
- T4 记录:BUG-1045(A,**复发自 BUG-585**,写明 BUG-969 如何绕过防复发、为何测试没拦住);BUG-1046(B,关联 BUG-675 / 678 / 685);PROGRESS;`docs/testing/` 真机清单(打字回答后问题只出现一遍;选择题提交失败时只有一张卡 + 提示;刷新前后一致)。
|
||
|
||
## BUG 编号
|
||
|
||
写单时最大 BUG-1044(1042–1044 由按钮间距单预留);本单 **BUG-1045 / 1046**,开工时核对。
|