Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
8.9 KiB
TASK · 生时校正穷尽出口修复单:门槛句重复写两遍、穷尽分支吞掉剩余问题、范围小字伪装成按钮(2026-09-06)
- 基线:
origin/staging@b938c76a(含 BUG-558~560 实现150d7ef1/3a9ae736/62afa521) - 分支:
codex/rectification-convergence-exit-fix-20260906 - 执行方:coding agent;验收:Claude
- 涉及文件:
frontend/src/lib/rectification-agentic/v9/answer-choice.ts、frontend/src/app/api/rectification/agent/route.ts、frontend/src/components/rectification-agentic-chat.tsx、frontend/tests/rectification-exhaustion-exit-20260906.test.ts - BUG 编号起点:BUG-565(开工时
grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1复核) - 父任务书:
TASK-rectification-convergence-exit-20260906.md(验收记录在其末尾)
0. 验收时的门(供对照)
b938c76a 上:tsc 0 错;lint 0 error;非 DB 校正套件 905/0;校准脚本数字与进度记录一致。Python 定向套件结论见父任务书验收段(test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow 执行方报告在基线上也失败,本单不负责,但 §5.4 要求把它记进 BLOCKED.md)。
1. 事故实证
1.1 P1 · 门槛翻译句每回合写两条相同助手消息(BUG-565)
在 A4 用例 2 的形状(方法覆盖完成、无剩余采集、引擎 acceptance_allowed=false)上,对 finalizeSuccessfulTurnExit({ action: "message" }) 计数 append_agentic_rectification_turn:2 次,两条 p_assistant_message 完全相同(范围句 + 门槛句)。原因链:
persistExhaustionCollect的门槛分支自己调用persistV9DeterministicTurn(requestId: randomUUID(),不幂等)写第 1 条。finalizeSuccessfulTurnExit先persistNextInterviewIfIdle(触发上面的写入,返回值被丢弃),再ensureNonTerminalTurnExit→inspectNonTerminalTurnExit.satisfied仍只认「有焦点 / 已采用 / 用户已停 / 可采用」,门槛句不算载体 → 判不满足 → 再次persistExhaustionCollect→ 写第 2 条。agent-run.ts对opening/evidence动作在 finalize 之前还会先调一次persistNextInterviewIfIdle→ 这两种动作是 3 条。- 点选路径:
persistApplied→persistNextInterviewAfterChoice→ 门槛分支写 1 条独立消息;persistApplied又把同一句拼在「已记录你的选择」后面作为点选回合的正文写第 2 条。 route.ts无焦点分支:persistNextInterviewIfIdle(写 1 条)之后又persistV9DeterministicTurn同一段文字(第 2 条)。
用户看到的就是同一段"范围已经收到…还差带月份的经历"连出两三遍。
1.2 P2 · 穷尽分支排在问题持久化之前,吞掉仍可问的题
persistNextInterviewIfIdle 新增的分支 !remainingCollect && !acceptedTime && !decision.canAdopt → persistExhaustionCollect 排在 persistNextInterviewAfterChoice 之前,不看 decision.nextAction。当引擎采用门关闭(例如 low_date_quality)但决策层仍给出 ask_candidate_discriminator(有可渲染区分卡)或 ask_holdout_validation(holdout 题)时,这些题被直接换成门槛句。答题仍能收窄范围,而门槛句只在用户补一件事之后才有意义,两者不冲突,不该二选一。
1.3 P2 · 范围小字变成按钮但文案还是状态句
RectificationReadonlyRange 现在渲染成 <button>,点击即停止访谈,文案仍是「目前范围 …–…,还在收窄」。用户看不出它可点,点了却是"先这样"。同一屏下方已经有明确的「先这样,先看当前范围」按钮,这个入口多余且歧义。产品偏好:多余入口宁可删除。
2. 校准结论(不是本单任务,但要写进记录)
父任务书 C2 的门"覆盖 ≥ 旧方案 − 1 且中位宽度更窄"设计有缺陷:旧方案 proportional 在 20 例公开 holdout 上中位宽度 21 分钟 = 整个候选窗口(半径 10),它"覆盖 17/20"只是因为不收窄。把它当基线,任何真正收窄的方案都过不了。真正的发现是:先验一旦按引擎原始分拉尖(offset),真实分钟落在领先集合里的只有 12/20(中位宽 10 分钟),与随机取 10 分钟(≈48%)相当。引擎原始分在分钟级几乎没有区分力,这是产品级事实:现有八法计分不足以在 20 分钟窗口内可靠收窄;"收敛"只能来自用户答题和更强的引擎证据,不能靠换尺度。本单只要求把这段结论写进 BUG-560(状态保持 blocked,补"根因升级"一段)与 BLOCKED.md,不做算法改动。
3. 决策记录
- 门槛翻译句是回合正文,不是独立消息。写入方只能有一个:谁负责写本回合的助手消息,谁把门槛句并入正文;
persistExhaustionCollect本身不得写 turn。 inspectNonTerminalTurnExit的"满足"要认得穷尽态:!remainingCollect && blockingMethodsCovered && !canAdopt(同persistNextInterviewIfIdle的判定,抽成一个纯函数共用),满足即视为已交付,不再修复。- 穷尽分支只在
decision.nextAction ∈ {offer_provisional_range, complete_with_range, ask_fact_collection}且无剩余采集时进入;ask_candidate_discriminator/ask_holdout_validation照常持久化问题。 - 范围小字还原为纯状态句(
<p role="status">),删掉onStop入口;口述态停止按钮保留。 - 不改 Skill、不改引擎、不改任何门槛数值。
4. 硬红线
- BUG-558 三个形状用例原样通过(改断言写三栏);BUG-503/520/544/546/547/549/550 用例不动。
- 不得重新引入
USER_COLLECT_QUESTION.other兜底。 - tsc 0 错、lint 0 error、测试总数 ≥ 905(非 DB 校正套件)。
5. 任务分解
5.1 单一写入方(BUG-565)
persistExhaustionCollect门槛分支:删除内部persistV9DeterministicTurn,只返回{ hostNarration, terminalNote: true, persisted: false }。finalizeSuccessfulTurnExit:persistNextInterviewIfIdle返回terminalNote时,由它写一次 deterministic turn(requestId用${askedTurnId}:gate之类可幂等的键),并跳过ensureNonTerminalTurnExit;agent-run.ts对 opening/evidence 的那次调用返回terminalNote时同样只写一次,finalize 不再写。route.ts无焦点分支:只保留 route 自己的那次写入。- 点选路径:
persistApplied拼进正文即可,不再有独立消息。 - 验收:在
rectification-exhaustion-exit-20260906.test.ts加三例——finalizeSuccessfulTurnExit({action:"message"})、{action:"evidence"}经 agent-run 路径、applyRectificationChoice最后一张卡——各自append_agentic_rectification_turn计数为 1,正文含门槛句一次。
5.2 穷尽态视为已交付
- 抽
isExhaustedGateState(decision, remainingCollect, methods)纯函数;inspectNonTerminalTurnExit.satisfied与persistNextInterviewIfIdle共用。 - 验收:A4 用例 2 之后再调
ensureNonTerminalTurnExit,append计数不增加。
5.3 穷尽分支不吞题
- 分支加
decision.nextAction白名单(§3.3)。 - 验收:新用例——同 A4 用例 2 形状但
discriminating_event_probes留一条可渲染探针、receiptacceptance_allowed=false→ 持久化的是区分卡焦点,不是门槛句;holdout 同理一例。
5.4 范围小字还原 + 记录
RectificationReadonlyRange回到<p>;删onStopprop;frontend/DESIGN.md对应行改回;源扫描断言(rectification-exhaustion-exit"collect spoken stop button and range line share CHOICE_STOP_LABEL")改为只要求口述态按钮,写三栏。docs/BUG_HISTORY.md:BUG-565(重复消息)、BUG-566(吞题)、BUG-567(伪按钮);BUG-560 追加 §2 的校准结论。BLOCKED.md:test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow在基线失败一事(复现命令、失败断言、首次失败 SHA 若能二分)。CHANGELOG.md、docs/tasks/PROGRESS-rectification-convergence-exit-fix-20260906.md、docs/testing/rectification-convergence-exit-20260906.md第 1 条补"门槛句只出现一次"。
6. 让步顺序
5.1 + 5.2 不可拆;5.3 必做;5.4 的 UI 还原可后置到同分支第二次提交,记录不可省。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/rectification-convergence-exit-fix-20260906 .worktrees/rectification-convergence-exit-fix-20260906 origin/staging
cd .worktrees/rectification-convergence-exit-fix-20260906
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && 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)"
收尾同一命令 fail=0、总数 ≥ 905;tsc --noEmit;npm run lint。