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

110 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-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` | `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-644P0):被顶替(superseded)的焦点题号永久占位,计划层看不见它,落库时被当成「已问过」
逐环都已在代码里核实:
1. **两次 batch → 两次计划落库。** 学业那一轮模型把两件事分两次调 `record-evidence-batch`(证据 `createdAt` 差 11 秒;客户端 5 步 vs 回执 3 条)。每次 batch 都跑 `autoRescoreAfterEvidenceChange``persistPlanFocus``rectification-v9-tools.ts` L911–947)。第一次只有 1 件事,方法覆盖链第一站是感情 → 落下 `collect:relationship:collect_method_evidence`;第二次有 2 件事,`nextDatedCollectFollowup``DATED_COLLECT_ORDER` 先问家人 → 落下 `collect:family:…`
2. **顶替不是删除。** `set_v10_conversation_focus` RPC`20260814020000_rectification_v10_runtime.sql` L536543)把所有 `active` 焦点改成 `superseded` 再插新行。感情采集题于是成了一条**从未展示给用户**的 `superseded` 行。模型随后再调 `set-focus`(想写感情或家人),撞号 → 回执里那条 `focus_idempotency_conflict`
3. **题号唯一约束包含已结束行。** 同 RPC L523535:同 `question_id` 已存在且不是「完全相同的 active」→ 一律 `focus_idempotency_conflict``unique (case_id, question_id)`L142)不分状态。
4. **计划层看不见 superseded。** 汇总 `declined_skipped_topics` 只聚合 `status in ('declined','skipped')`(同文件 L290294)。于是 `domainCollectFocusAsked(askedRows, "relationship")``method-followup.ts` L536)为 false → `collectRetry=false`L19321935)→ `persistCollectFocus``server-focus.ts` L359)在撞号时**不走 `:next` 重试**,直接返回 `duplicate_focus`
5. **`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`
6. **读取接口不知道这些。** 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. 决策记录
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) === true``selection_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_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` 不落 adoptBUG-644
- `answer-choice.ts` `persistExhaustionCollect`:删除 `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.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. 开工前置命令
```bash
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/health``deployment.gitCommit` 等于最新含代码改动的提交。
- 真人清单见 T5;做不了的写成环境缺口。