Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
12 KiB
TASK · 被顶替的采集题号永久占位:最后一条线答「没有」后,感情采集题落库撞号被当成「已问过」,出口误走交付旁白却无卡(2026-09-10)
- 基线:
origin/staging@82eb3b79(代码同8f9eb587,已部署;含另一会话实现的 BUG-635~640,未触及server-focus.ts/answer-choice.ts) - 分支:
codex/rectification-superseded-focus-duplicate-20260910,基于origin/staging - 串行:改
server-focus.ts、answer-choice.ts(persistExhaustionCollect)。与TASK-rectification-collect-semantics-20260910.md(BUG-641~643,改route.ts/method-followup.ts/ 分类器)有answer-choice.ts交集(那份改applyCollectFocusDenial的入口状态,本单改它下游的persistExhaustionCollect),本单先做,那份在本单分支之上叠加。与 BUG-635/636、BUG-637~640 两单无交集。 - 执行方:coding agent;验收:Claude
- BUG 编号起点:BUG-644(641~643 已由采集语义单占用;开工时核对
docs/BUG_HISTORY.md最大号) - 优先级:BUG-644 P0(真实用户第二次当天断流;凡是一轮里 batch 被调两次的 Case 都会在采集收尾时复现);BUG-645 P1(出口旁白说「这次给出的范围」却没有卡、
can_adopt=false,文案与能力矛盾) - 不改 Skill 10.0.21、不改引擎、不加迁移(TS 侧即可闭合;若执行方认为必须改 RPC,先写
BLOCKED.md说明并停,等产品拍板——动表要 Docker 跑npm run test:db)。
0. 给产品的即时绕行
这个 Case 现在没有活动焦点。两条路都能走:直接打一句带年月的感情经历(例如「某年某月开始交往」),batch 会照常写入并重算,不需要那道采集题;或者按「先这样」,selection_allowed=true,会出区间卡。
1. 事故实证(staging 2026-09-10 20:23–20:29 +0800,Skill 10.0.21;只写结构)
顺序:开场 → 学业两件(同一轮)→ 家人题「没有」→ 财务一件 → 搬家一件 → 六道带年月点选题(事业 / 感情 / 迁居 / 学业发挥质量 / 事业 / 感情)→ 健康采集题「没有」→ 助手说 「这次给出的范围 04:48–05:07。对照了 4 件经历,事件吻合率 100%。这只是代表性候选…」 → 下面是「没有拿到下一个问题。」,顶部「目前范围 04:48–05:07,还在核对」。
| 项 | 值 |
|---|---|
| 最后一轮 | 走确定性路径(原字匹配「没有」):phases=[]、tool_activities=[];正文是交付三句(deliveryTurnNarration 形状) |
current_question |
null;question_source="focus";choice_card=null;该轮 assistant 行 question=null |
interview |
type=ask_fact_collection、session_outcome=collect_evidence、can_adopt=false、can_offer_range=false、selection_allowed=true |
next_user_action |
id=ask_method_followup,user_meaning=「可以先说一段记得大概时间的感情变化…本题绑定 D9」——读取接口的计划说下一题是感情采集题 |
| 学业那一轮 | 两件证据 createdAt 相差 11 秒(12:23:36 / 12:23:47);回执 batch 只有一条(12:23:47 起);客户端显示「已完成 5 步」而回执只有 3 条活动;rectification-set-focus failed focus_idempotency_conflict;该轮最终问题是家人采集题 |
| 家人 / 财务 / 搬家 / 健康采集题 | 全部问过;感情采集题从头到尾没出现过,但感情是 DATED_COLLECT_ORDER 最后一项,计划层一直想问 |
2. 根因
2.1 BUG-644(P0):被顶替(superseded)的焦点题号永久占位,计划层看不见它,落库时被当成「已问过」
逐环都已在代码里核实:
- 两次 batch → 两次计划落库。 学业那一轮模型把两件事分两次调
record-evidence-batch(证据createdAt差 11 秒;客户端 5 步 vs 回执 3 条)。每次 batch 都跑autoRescoreAfterEvidenceChange→persistPlanFocus(rectification-v9-tools.tsL911–947)。第一次只有 1 件事,方法覆盖链第一站是感情 → 落下collect:relationship:collect_method_evidence;第二次有 2 件事,nextDatedCollectFollowup按DATED_COLLECT_ORDER先问家人 → 落下collect:family:…。 - 顶替不是删除。
set_v10_conversation_focusRPC(20260814020000_rectification_v10_runtime.sqlL536–543)把所有active焦点改成superseded再插新行。感情采集题于是成了一条从未展示给用户的superseded行。模型随后再调set-focus(想写感情或家人),撞号 → 回执里那条focus_idempotency_conflict。 - 题号唯一约束包含已结束行。 同 RPC L523–535:同
question_id已存在且不是「完全相同的 active」→ 一律focus_idempotency_conflict;unique (case_id, question_id)(L142)不分状态。 - 计划层看不见 superseded。 汇总
declined_skipped_topics只聚合status in ('declined','skipped')(同文件 L290–294)。于是domainCollectFocusAsked(askedRows, "relationship")(method-followup.tsL536)为 false →collectRetry=false(L1932–1935)→persistCollectFocus(server-focus.tsL359)在撞号时不走:next重试,直接返回duplicate_focus。 duplicate_focus被当成「这题已经问过并关掉了」。persistExhaustionCollect(answer-choice.ts)注释原文:「duplicate_focus: this ask is already closed; fall through to adopt/gate.」于是跳过口语采集,进入ceiling.acceptanceAllowed && trainingGate.open && decision.canAdopt分支,用adoptHostNarration写出交付三句并terminalNote=true。- 读取接口不知道这些。 GET 的
decideFromDossier仍说下一题是感情采集,next_user_action=ask_method_followup;但数据库永远拒绝这个题号,于是每次走到这里都同样断流。客户端current_question=null→questionGap→ 「没有拿到下一个问题」。
这也是 BUG-462(「区间出口采集撞上开场题号」)和 BUG-626(「撞同 id 焦点 duplicate_focus 静默」)同一片土壤的第三次露头:题号唯一约束与「顶替不删行」叠加,任何被顶替过的采集题都再也问不出来。
2.2 BUG-645(P1):出口旁白与能力矛盾——说了「这次给出的范围」却 can_adopt=false、没有卡
persistExhaustionCollect 的 adopt 分支只看内部 decision.canAdopt;公开 can_adopt(publicCanAdopt,core/rectification-decision.ts L125 注释:「Public can_adopt is true only in these outcomes. Internal decision.canAdopt may still be true during collect」)在 session_outcome=collect_evidence 时为 false,客户端据此不出卡。用户读到一句交付口吻的话,却没有任何可点的东西,也没有下一问。
3. 决策记录
- 被顶替且从未答过的采集题必须能再问。 不改 RPC 的前提下,在 TS 侧闭合:
persistCollectFocus撞号时总是尝试后缀题号(:next、:next2、:next3,上限 3 次),不再以collect_retry作为「允不允许换号」的开关;collect_retry只保留「换用重问文案」的语义,且只在该领域确实declined / skipped / resolved过才为 true。 duplicate_focus不再等于「已问过」。persistExhaustionCollect收到duplicate_focus时与其它落库失败同样处理:返回口语采集题(或中间态范围句),不得落到 adopt 分支;并把duplicate_focus连同题号写进rectification_exhaustion_collect日志。- 交付口吻只能配交付能力。 任何路径要输出
deliveryTurnNarration/adoptHostNarration之前,必须publicCanAdopt(decision) === true或selection_allowed且明确走offer_provisional_range;否则改用nonConvergingRangeNarration(variant="intermediate")+ 当前采集题。做成一个守卫函数,三处调用(persistNextInterviewAfterChoice的 skip 分支、persistExhaustionCollect的 adopt 分支、路由的isNonConvergingRangeOffer分支)共用。 - 同一轮多次 batch 不再各落一次计划焦点。
persistPlanFocus在同一turnId内若已经为本轮创建过一个collect_method_evidence焦点,第二次只做resolve(status="superseded")+ 新建时带askedTurnId——这一条只是减少 superseded 行,不是本单的闭合点;若实现复杂度高可延后,写进度记录。 - 不改 Skill、不改引擎、不加迁移;不给
declined_skipped_topics加 superseded(那会让「没问过」被当成「拒答」,是 BUG-520 的反面)。
4. 硬红线
- 不得把 superseded 当 declined(BUG-520:点选题答「没有」不是拒答;这里同理,没问过更不是)。
- 不得在
can_adopt=false时输出任何含「这次给出的范围」「代表性候选」的交付句(静态词表合同里加断言)。 - 题号后缀只用于
collect_method_evidence;点选探针题号不得加后缀(它们带candidate_split_hash,撞号即真重复,BUG-559)。 - 既有测试总数不降;改断言写「原值 / 新值 / 原因」。
- Bug 历史与测试不得出现真实用户的年月事件与 Case id。
5. 任务分解
T1 · persistCollectFocus 撞号必换号(BUG-644)
server-focus.ts:撞号 → 依次尝试${questionId}:next、:next2、:next3;全部撞号才返回duplicate_focus。collect_retry不再参与换号判定。- 验收(
rectification-spoken-collect.test.ts/ 新rectification-superseded-focus.test.ts,用假 RPC):基础题号撞号、:next成功 →status=created、questionId带后缀、prompt为原文案(非重问文案);collect_retry=true时文案换重问模板;四个号全撞 →duplicate_focus。
T2 · duplicate_focus 不落 adopt(BUG-644)
answer-choice.tspersistExhaustionCollect:删除duplicate_focus的特判,与其它失败同路返回spoken ?? range;日志加persist_status与question_id。- 验收:构造「计划层 next_followup=感情采集、落库返回 duplicate_focus、acceptance_allowed=true、training gate open、内部 canAdopt=true」→ 返回
hostNarration为采集口语、terminalNote不为 true、无交付句。
T3 · 交付口吻守卫(BUG-645)
- 新增
deliveryNarrationAllowed(decision, nextAction),三处调用;不允许时用中间态范围句 + 采集题。 - 验收:
can_adopt=false+session_outcome=collect_evidence的决策,三处都不出交付句;agent-voice-copy-contract加「can_adopt=false时不得出现『这次给出的范围』」的源码/词表断言;can_adopt=true路径回归不变(rectification-delivery-report-facts.test.ts)。
T4 · 同轮多次 batch 的计划焦点(决策 4,可延后)
rectification-v9-tools.tspersistPlanFocus:同一turnId第二次落采集焦点时,先resolve本轮已创建的那条再新建,避免堆 superseded 行。- 验收:假两次 batch(各 1 件)→ 最终只有一条 active 采集焦点,且上一条不以
superseded留下;点选探针焦点不受影响。
T5 · 记录
docs/BUG_HISTORY.mdBUG-644、BUG-645(关联 BUG-462、BUG-520、BUG-525、BUG-559、BUG-626、BUG-627、BUG-633)。CHANGELOG.md:「采集题被系统顶替后仍会再问;没有可采用结果时助手不再用交付口吻收尾」。docs/tasks/PROGRESS-rectification-superseded-focus-duplicate-20260910.md。docs/testing/:真人清单——新 Case 一轮里分两句说两件学业经历(诱发两次 batch),走完所有采集题后回「没有」,期望出现感情采集题而不是「没有拿到下一个问题」。
6. 让步顺序
T4 可延后(写进度记录);T1 + T2 是最低交付;T3 与 T2 同批。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/rectification-superseded-focus-duplicate-20260910 .worktrees/rectification-superseded-focus-duplicate-20260910 origin/staging
grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | tail -1
cd frontend && npm test -- tests/rectification-spoken-collect.test.ts tests/rectification-exhaustion-exit-20260906.test.ts tests/rectification-delivery-report-facts.test.ts tests/agent-voice-copy-contract.test.ts
8. 验收口径
tsc --noEmit0 错、npm run lint0 error、相关套件 fail=0、测试总数 ≥ 基线;next build后/仍 Static,首屏 gzip ±2%。- 推 staging 后
/api/health的deployment.gitCommit等于最新含代码改动的提交。 - 真人清单见 T5;做不了的写成环境缺口。