Files
Jyotisha/docs/tasks/PROGRESS-rectification-dup-question-20260926.md
T
Jesse_ChenandClaude Opus 5.5 e4c1c7a342 fix(rectification): one stem per turn, one card per focus (BUG-1045/1046)
BUG-1045 (recurrence of BUG-585 via BUG-969): the snapshot merge now drops
the attached stem from the streamed "ack + stem" text with the same
stripQuestionSentences the GET route uses; server text unchanged.

BUG-1046: a failed choice submit (409 / network) or typed send withdraws
the local answered mark, remounts the card, re-reads the Case and shows
"这次没提交上,请再点一次。"; the persisted question hangs on the latest
settled assistant message with other copies of the same focus removed
(standalone block only when nothing can carry it); willContinue and the
send() settle merge unseen assistant turns (same gap as BUG-685).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 10:49:25 +08:00

6.6 KiB
Raw Blame History

PROGRESS · 生时校正:同一轮问题两次 + 同一道题两张卡(2026-09-26)

  • 任务书:TASK-rectification-dup-question-20260926.md
  • 执行:Claude 子代理(直接执行模式,产品授权),分支 codex/rectification-dup-question-20260926,worktree .worktrees/rectification-dup-question-20260926
  • 基线:开工 ac224cef;BUG-1042 合入后变基到 origin/staging 509987b9(按任务书「以新 head 为准」)。本单未推送。
  • BUG:BUG-1045(复发自 BUG-585)、BUG-1046;开工时 staging 最大号 BUG-1044,核对无冲突。状态均 investigating(真机与部署未做)。

做了什么

项 实现 位置
T1 / D1 实时去重 mergeTurnQuestions 在快照回合带问题时对正文跑 GET 同款 stripQuestionSentences,幂等。服务端 BUG-969 兜底文本不改 src/lib/rectification-snapshot-messages.ts
T2 / D2 失败回滚 submitStructuredChoice 的 !ok 与网络错误:撤回本次本地标记(withdrawAnswerMark,题回 active、无 answer_option)、移除占位、choiceNonce++(卡片 key 变化 → 重新挂载、清掉卡内本地选中)、重读快照(applySnapshotTurnsToMessages)、错误行「这次没提交上,请再点一次。」(RECTIFICATION_CHOICE_SUBMIT_FAILED_COPY,VOICE 已加一行)。send 打字回答的 !ok / 无 body / 网络错误同样撤回 typed 标记 rectification-agentic-chat.tsx、rectification-surface-state.ts、docs/VOICE.md
T2 / D3 兜底闸门 placePersistedQuestion + turnQuestionFromCurrent:缺口为 persisted_question 时,把快照当前题挂到最新一条已结算助手消息上,其它消息上同 focus_id 的副本摘掉;独立问题块只在没有助手消息、或最新助手消息已挂另一道题时出现。缺口状态机、liveQuestionOnMessages 只看最新消息(BUG-635 合同)都不改 rectification-snapshot-messages.ts、turn-question.ts、rectification-agentic-chat.tsx
T3 / D4 漏收回合 willContinue 分支用 appendUnseenAssistantTurns(..., { beforeRenderKey }) 把未见回合插在跟进行之前;send() 成功结算后改用 applySnapshotTurnsToMessages 同上
T4 记录 BUG-1045 / 1046、CHANGELOG、DESIGN 状态表两行、VOICE 一行、真机清单、状态板 docs/

偏离与说明

  1. D3 的一个例外:最新助手消息已经挂着另一道题(不同 focus_id)时,当前题无处可挂,仍用独立问题块。这时不会重复同一 focus;硬挂到更早的消息会让题出现在已答题上方、打乱顺序。任务书写的是「兜底块只在没有任何助手消息时渲染」,此处放宽一格,写进 DESIGN 状态表。
  2. BUG-635 合同保留:旧消息上的同焦点仍不算「仍在显示」(liveQuestionOnMessages 只读最新消息,源码合同未动);D3 的做法是把题挪到最新消息、摘掉旧副本,所以「只画一张」与 BUG-635 同时成立。
  3. 最新消息上同焦点但本地已答、服务端仍 active 时(失败未回滚的残留),D3 用服务端的 active 题替换本地标记。只在缺口为 persisted_question(非 busy)时发生,不会在提交途中闪回。
  4. 为不改 7 处按 {messages.map((message) => { 切片的源码合同,渲染循环保留 messages.map,循环内按 renderKey 取放置后的消息(entryMessage)。
  5. rectification-agentic-chat.tsx 注释里提到 requestTieBreak 会撞两条禁用该标识符的源码合同,已改写措辞(第一次全量跑出 2 条新失败,修正后通过)。
  6. 未改 use-conversation-scroll-anchor.ts(另一单在改)。H2 若真机确认服务端出了完全相同的题:BUG-540 / 559 / 592 守卫已核对仍在(pickProbe 先 probe_id → semantic_key;sameYearProbeAsked / same_year_asked 硬排除),本单不改选题。

改动的既有断言

没有改任何既有断言。rectification-answer-choice.test.ts(BUG-969 改成「正文含题干」的那条)与 account-dialog-inert-20260918.test.ts 的服务端断言保持原样——服务端文本没变,按任务书只追加客户端合并断言(正文不再含题干、问题块是这道题)。DESIGN.md 状态表改写时保留了 rectification-unwritten-evidence.test.ts 锁定的两句。

验证

项 结果
tsc --noEmit 0 错
npm run lint 0 error,126 warnings(与基线 126 相同;改动文件无新增)
新测试 tests/rectification-dup-question-20260926.test.tsx 11/11 通过;挂载真实 RectificationAgenticChat,假 fetch 走「流式 → 结算 → 快照合并」。前 10 条在 509987b9 代码上 9 条失败(「无助手消息兜底」本来就对)
全量 npm test(Node 20.19.2,无 Docker) 3972 条:3884 通过 / 61 失败 / 27 跳过。失败名单与基线 test11.log(3960 / 61)逐条一致,新增失败 0;测试名消失 0、新增 12(本单 11 条 + BUG-1042 合入的 1 条)
npm run build -- --webpack 通过;┌ ○ / Static
首屏 gzip rootMainFiles 4 个文件 130933 B → 130933 B(0%);index.html 引用的 36 个静态文件 656741 → 657563 B(+0.13%)
浏览器(next start + Chrome 151 无头,CDP 回放虚构 /api/*) 见下表

浏览器,同一份虚构数据,基线 509987b9 构建 vs 本分支构建(1280 宽;计数排除 legend.sr-only):

场景 修复前 修复后 截图
A 打字回答命中选择题 → 流 ack+stem → 快照挂卡 下一题可见 2 次 1 次;4 个可点选项 docs/testing/rectification-dup-question-20260926/A-before-1280.png / A-after-1280.png
B 点 B → POST 409 stale_question → 快照不变 题干 2 次、2 张卡、B 仍高亮、兜底块 1 个、错误行为服务端原句 1 张卡、0 个已选、4 个可点、错误行「这次没提交上,请再点一次。」 B-before-1280.png / B-after-1280.png

B 修复前截图与产品真机截图同形(已选 B 的卡 → 操作按钮 → 同题空白卡),即 H1 在虚构数据上可复现。截图右下「合盘历史暂时无法读取」是回放脚本没 mock /api/synastry-reports,与本单无关。

环境缺口

  • 无登录态真机、无真实模型:真机清单 docs/testing/rectification-dup-question-20260926.md。H1 / H2 以哪条为准需真机当时 /api/rectification/agent 的状态码。
  • Node 20:mock.module 类测试 ERR_MODULE_NOT_FOUND;无 Docker:DB 套件。失败清单与基线逐条一致(61 条)。