Files
Jyotisha/docs/tasks/TASK-rectification-superseded-focus-duplicate-20260910.md
T

12 KiB
Raw Blame History

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.tsanswer-choice.tspersistExhaustionCollect)。与 TASK-rectification-collect-semantics-20260910.mdBUG-641643,改 route.ts / method-followup.ts / 分类器)有 answer-choice.ts 交集(那份改 applyCollectFocusDenial 的入口状态,本单改它下游的 persistExhaustionCollect),本单先做,那份在本单分支之上叠加。与 BUG-635/636、BUG-637640 两单无交集。
  • 执行方: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:2320:29 +0800Skill 10.0.21;只写结构)

顺序:开场 → 学业两件(同一轮)→ 家人题「没有」→ 财务一件 → 搬家一件 → 六道带年月点选题(事业 / 感情 / 迁居 / 学业发挥质量 / 事业 / 感情)→ 健康采集题「没有」→ 助手说 「这次给出的范围 04:48–05:07。对照了 4 件经历,事件吻合率 100%。这只是代表性候选…」 → 下面是「没有拿到下一个问题。」,顶部「目前范围 04:48–05:07,还在核对」。

最后一轮 走确定性路径(原字匹配「没有」):phases=[]tool_activities=[];正文是交付三句(deliveryTurnNarration 形状)
current_question nullquestion_source="focus"choice_card=null;该轮 assistant 行 question=null
interview type=ask_fact_collectionsession_outcome=collect_evidencecan_adopt=falsecan_offer_range=falseselection_allowed=true
next_user_action id=ask_method_followupuser_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-644P0):被顶替(superseded)的焦点题号永久占位,计划层看不见它,落库时被当成「已问过」

逐环都已在代码里核实:

  1. 两次 batch → 两次计划落库。 学业那一轮模型把两件事分两次调 record-evidence-batch(证据 createdAt 差 11 秒;客户端 5 步 vs 回执 3 条)。每次 batch 都跑 autoRescoreAfterEvidenceChangepersistPlanFocusrectification-v9-tools.ts L911–947)。第一次只有 1 件事,方法覆盖链第一站是感情 → 落下 collect:relationship:collect_method_evidence;第二次有 2 件事,nextDatedCollectFollowupDATED_COLLECT_ORDER 先问家人 → 落下 collect:family:…
  2. 顶替不是删除。 set_v10_conversation_focus RPC20260814020000_rectification_v10_runtime.sql L536543)把所有 active 焦点改成 superseded 再插新行。感情采集题于是成了一条从未展示给用户superseded 行。模型随后再调 set-focus(想写感情或家人),撞号 → 回执里那条 focus_idempotency_conflict
  3. 题号唯一约束包含已结束行。 同 RPC L523535:同 question_id 已存在且不是「完全相同的 active」→ 一律 focus_idempotency_conflictunique (case_id, question_id)L142)不分状态。
  4. 计划层看不见 superseded。 汇总 declined_skipped_topics 只聚合 status in ('declined','skipped')(同文件 L290294)。于是 domainCollectFocusAsked(askedRows, "relationship")method-followup.ts L536)为 false → collectRetry=falseL19321935)→ persistCollectFocusserver-focus.ts L359)在撞号时不走 :next 重试,直接返回 duplicate_focus
  5. duplicate_focus 被当成「这题已经问过并关掉了」。 persistExhaustionCollectanswer-choice.ts)注释原文:「duplicate_focus: this ask is already closed; fall through to adopt/gate.」于是跳过口语采集,进入 ceiling.acceptanceAllowed && trainingGate.open && decision.canAdopt 分支,用 adoptHostNarration 写出交付三句并 terminalNote=true
  6. 读取接口不知道这些。 GET 的 decideFromDossier 仍说下一题是感情采集,next_user_action=ask_method_followup;但数据库永远拒绝这个题号,于是每次走到这里都同样断流。客户端 current_question=nullquestionGap → 「没有拿到下一个问题」。

这也是 BUG-462(「区间出口采集撞上开场题号」)和 BUG-626(「撞同 id 焦点 duplicate_focus 静默」)同一片土壤的第三次露头:题号唯一约束与「顶替不删行」叠加,任何被顶替过的采集题都再也问不出来。

2.2 BUG-645(P1):出口旁白与能力矛盾——说了「这次给出的范围」却 can_adopt=false、没有卡

persistExhaustionCollect 的 adopt 分支只看内部 decision.canAdopt;公开 can_adoptpublicCanAdoptcore/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. 决策记录

  1. 被顶替且从未答过的采集题必须能再问。 不改 RPC 的前提下,在 TS 侧闭合:persistCollectFocus 撞号时总是尝试后缀题号(:next:next2:next3,上限 3 次),不再以 collect_retry 作为「允不允许换号」的开关;collect_retry 只保留「换用重问文案」的语义,且只在该领域确实 declined / skipped / resolved 过才为 true。
  2. duplicate_focus 不再等于「已问过」。 persistExhaustionCollect 收到 duplicate_focus 时与其它落库失败同样处理:返回口语采集题(或中间态范围句),不得落到 adopt 分支;并把 duplicate_focus 连同题号写进 rectification_exhaustion_collect 日志。
  3. 交付口吻只能配交付能力。 任何路径要输出 deliveryTurnNarration / adoptHostNarration 之前,必须 publicCanAdopt(decision) === trueselection_allowed 且明确走 offer_provisional_range;否则改用 nonConvergingRangeNarration(variant="intermediate") + 当前采集题。做成一个守卫函数,三处调用(persistNextInterviewAfterChoice 的 skip 分支、persistExhaustionCollect 的 adopt 分支、路由的 isNonConvergingRangeOffer 分支)共用。
  4. 同一轮多次 batch 不再各落一次计划焦点。 persistPlanFocus 在同一 turnId 内若已经为本轮创建过一个 collect_method_evidence 焦点,第二次只做 resolve(status="superseded")+ 新建时带 askedTurnId——这一条只是减少 superseded 行,不是本单的闭合点;若实现复杂度高可延后,写进度记录。
  5. 不改 Skill、不改引擎、不加迁移;不给 declined_skipped_topics 加 superseded(那会让「没问过」被当成「拒答」,是 BUG-520 的反面)。

4. 硬红线

  • 不得把 superseded 当 declinedBUG-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_focuscollect_retry 不再参与换号判定。
  • 验收(rectification-spoken-collect.test.ts / 新 rectification-superseded-focus.test.ts,用假 RPC):基础题号撞号、:next 成功 → status=createdquestionId 带后缀、prompt 为原文案(非重问文案);collect_retry=true 时文案换重问模板;四个号全撞 → duplicate_focus

T2 · duplicate_focus 不落 adoptBUG-644

  • answer-choice.ts persistExhaustionCollect:删除 duplicate_focus 的特判,与其它失败同路返回 spoken ?? range;日志加 persist_statusquestion_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.ts persistPlanFocus:同一 turnId 第二次落采集焦点时,先 resolve 本轮已创建的那条再新建,避免堆 superseded 行。
  • 验收:假两次 batch(各 1 件)→ 最终只有一条 active 采集焦点,且上一条不以 superseded 留下;点选探针焦点不受影响。

T5 · 记录

  • docs/BUG_HISTORY.md BUG-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 --noEmit 0 错、npm run lint 0 error、相关套件 fail=0、测试总数 ≥ 基线;next build/ 仍 Static,首屏 gzip ±2%。
  • 推 staging 后 /api/healthdeployment.gitCommit 等于最新含代码改动的提交。
  • 真人清单见 T5;做不了的写成环境缺口。