docs(tasks): brief for unwritten evidence claim dead-end (BUG-635/636)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
Jesse_Chen
2026-09-10 06:03:06 +00:00
co-authored by Claude Fable 5.1
parent d96b24c276
commit a15fc3efbe
2 changed files with 131 additions and 0 deletions
+1
View File
@@ -95,6 +95,7 @@
| `TASK-rectification-yearless-probe-downgrade-20260909.md` | `PROGRESS-rectification-yearless-probe-downgrade-20260909.md` | 产品拍板:无年月性格题(D9/D10 风格、月宿边界)降级为平局裁决——带年月题问完且候选仍分不开才问,分值减半(±1)、不计淘汰,报告标「参考」;三列卡性格描述不动;新增离线命中率测量脚本 + 导出 SQL(有出生证用户,聚合);Skill 10.0.21 | 已验收通过 | `fef54b6e`BUG-629Skill 10.0.21 |
| `TASK-rectification-followups-20260909.md` | `PROGRESS-rectification-followups-20260909.md` | 验收补漏:申报时段拦截只看钟点样式,带钟点的经历(『20:00 左右分手』『3 点到 5 点被车撞』)会被吞(BUG-631);by_time 只算引擎前 9 个候选,一小时窗 17 个候选时卡片列写『还没对照』(BUG-632) | 待验收 | `codex/rectification-followups-20260909`BUG-631632 |
| `TASK-rectification-evidence-turn-empty-answer-20260910.md` | `PROGRESS-rectification-evidence-turn-empty-answer-20260910.md` | 证据轮模型无正文被判整轮失败:证据、评分、下一问都已落库却只剩『没有拿到下一个问题』(BUG-633);答题旁白只说『范围没变』、时间线写死『还在收窄』(BUG-634) | 待验收 |
| `TASK-rectification-unwritten-evidence-claim-20260910.md` | `PROGRESS-rectification-unwritten-evidence-claim-20260910.md` | 证据轮模型只说『记下了』却没调 batch、没设下一问,财务一件静默丢失、流程停在原题且照常扣点(BUG-635);校正流不识别 Mastra schema 拒绝信封,会把被拒的 batch 报成 completedBUG-636 | 待执行 | `codex/rectification-unwritten-evidence-claim-20260910` |
### 聊天主链路与首页
@@ -0,0 +1,130 @@
# TASK · 证据轮模型只说「记下了」却没写证据、没设下一问:财务一件落空后流程停在原题(2026-09-10)
- 基线:`origin/staging` @ `d96b24c2`BUG-626/627/633/634 已合入并部署;`/api/health``deployment.gitCommit` = `d96b24c2`
- 分支:`codex/rectification-unwritten-evidence-claim-20260910`,基于 `origin/staging`
- 串行:无同文件并行单。`TASK-rectification-evidence-turn-empty-answer-20260910.md`BUG-633/634)已合入基线,本单在其 `applyHostFallback` / 失败刷新之上叠加,不得回退它的任何断言。
- 执行方:coding agent;验收:Claude
- 涉及文件(行号按 `d96b24c2`,定位以符号为准):`frontend/src/app/api/rectification/agent/route.ts`collect 焦点分支 L522597、`classifyRectificationTurnIntent` L544、`runV9AgentTurn({` L847)、`frontend/src/lib/rectification-agentic/v9/agent-run.ts``V9AgentRunOptions` L96112、`RETRYABLE_ERROR_CODES` L150、`applyHostFallback` L843、`streamAttempt` 收尾 L10251090、`RectificationRunDiagnostic.stateMutationCommitted` L1109、`buildAgentMessages` L1180)、`v9/host-fallback.ts``publicWriteToolCompleted` L16)、`v9/stream-mapping.ts``mapStreamChunkToActivity` L233257)、`frontend/src/lib/rectification-agentic/user-copy.ts``RECTIFICATION_USER_COPY`)、`frontend/src/components/rectification-agentic-chat.tsx``liveQuestionOnMessages` L1501、`questionGap` L1513、输入框占位 L1855)、`frontend/src/lib/rectification-surface-state.ts``rectificationQuestionGapState` L300
- BUG 编号起点:**BUG-635**`docs/BUG_HISTORY.md` 当前最大 BUG-634
- 优先级:BUG-635 **P1**(证据被静默丢弃、流程停住,用户看不出任何异常);BUG-636 **P2**(同一路径的 schema 拒绝会被报成 completed,诊断盲区)
- 不改 Skill 版本(保持 `10.0.21`),不改引擎、不改淘汰阈值、不改分类器提示词。
## 0. 给产品的即时绕行(不等修复)
在同一个校正会话里**再发一次**那句带年月的财务经历(原句即可)。当前活动焦点仍是财务采集题,分类器会再跑一次,模型大概率会走 batch。或者回「没有」关闭财务题,服务端会接着问下一条线。两种都不丢已有的 4 件证据和 4 道已答题。
## 1. 事故实证(staging 2026-09-10 13:50 +0800Skill 10.0.21;只写结构,不写个人资料)
顺序:开场 → 学业两件 → 感情两件 → 三道带年月选择题(事业 / 迁居 / 事业)→ D9 风格题 → 家人题「不记得了」→ 财务采集题「钱的方面,还记得哪一年…」→ 用户答「某年某月开始欠债」→ 助手只回一句 **「记下了:某年某月开始欠债。」**,下面没有任何问题、没有卡。顶部时间线仍是「目前范围 04:45–05:15,还在核对」。
Case JSON 里这一轮:
| 项 | 值 |
| --- | --- |
| 用户 turn `status` | `completed``phases` = `run.started, skill.bound, case.loaded, intent.classified, answer.composed, billing.settled, run.completed`**没有 `evidence.proposed`** |
| `tool_activities` | 只有 `rectification-read-case` completed48 ms);**没有 `rectification-record-evidence-batch`,也没有 `rectification-set-focus`**(前两轮证据轮都是 read-case → batch → set-focus 三步) |
| `evidence` | 仍是 4 件(学业 2、感情 2),没有 finance 域;`latest_result.createdAt` 仍是上一证据轮的时间 |
| `current_question` | 仍是 `collect:finance:collect_method_evidence``status=active``question_source=focus`,焦点的 `askedTurnId` 是上一轮 |
| `turns[].question` | 这一轮的 assistant 行 `question=null` |
| `step_state` | 「第 1 步·收集经历 / 继续回答当前这题」 |
| `interview.collection_progress` | `scoreable=3, minimum=3, missing=0` |
| 计费 | `billing.settled` 已出现:一轮什么都没写,照常扣点 |
同一 Case 里另一个与本单无关但产品会问的事实:4 道选择题全部有效计分(deltas ±2/±1),没有一个候选被淘汰,领先 04:52–04:53 为 14 分、落后 8 分。可信区间是全部未淘汰候选的包络,所以「范围没变」是算术结果(BUG-634 已让旁白说出领先/落后)。带年月的区分探针已经问完(`dropped_probes` 里剩下的全是无年份对照题,按 BUG-627 决策被丢弃),**继续收窄只能靠再收一件带年月的经历**——而这件恰好在本轮落空。
## 2. 根因
### 2.1 BUG-635(P1):模型声称「记下了」但没有调用任何写工具,宿主没有「证据轮必须落地」的不变量
1. 路由 collect 焦点分支(`route.ts` L522597)先跑 `classifyRectificationTurnIntent`。本句不是「没有 / 记不清」,分类结果应为 `provide_new_evidence``answer_current_focus` + `has_new_dated_event`。这两种都**直接落到 Agent**,分类结果被丢掉:`runV9AgentTurn` 只拿到 `action="evidence"` 和原话,没有任何字段告诉运行器「这一轮必须产生一次 batch 写入」。
2. 模型只调了 `rectification-read-case`,然后按系统提示里「证据轮正文只写一句复述,格式『记下了:年 月 事件短语』」的**格式要求**写了正文,却没做格式前提的那件事(`rectification-record-evidence-batch`),也没调 `rectification-set-focus`。模型为什么跳过,staging 没有落库的 run diagnostic 可查(`RectificationRunDiagnostic` 只进 `console.info`)。候选原因:「开始欠债」被当成持续状态而非带日期事件;`2026 年` 对模型像未来时间(`timeContext` 已给服务端时间,但不保证被采信);finance 域在 Skill 里写着「只有用户主动说才问」,模型可能把服务端主动问的财务题当成不计分。任何一种都不该由用户承担。
3. `streamAttempt` 收尾(`agent-run.ts` L1025–1045)只有三种异常分支:`answer_truncated``max_steps/provider_error``!answerText.trim()``empty_stream`)。正文非空就直接 `completeAttempt()`**不看 `toolTerminalStatus` 里有没有公开写工具完成**。`stateMutationCommitted = publicWriteToolCompleted(...) || hostFallbackUsed` 这个值算出来了(L1109),但只进日志,不参与决策。
4. `applyHostFallback`L843BUG-633)只覆盖「batch 已完成、正文为空」。它的镜像——「正文说记下了、batch 没跑」——没有任何守卫。
5. 成功收尾的焦点守卫(L1056)只在 `decision.nextAction === "ask_candidate_discriminator"` 时要求有已持久化的点选焦点;采集阶段里旧的财务焦点仍是 `active``decideFromDossier` 认为一切正常。`askedFocus?.askedTurnId === turnId`L1066)也不成立,不会补 `collectHandoff`
6. 客户端 `liveQuestionOnMessages`L1501)只要求**任一** settled assistant 消息上挂着与 `current_question.focus_id` 相同的问题——上一条消息的财务题满足条件,于是 `questionGap` 不报缺口、不补问题行、输入框占位照常写「请回答上面的问题…」。用户看到的是一句「记下了」和一片空白,要往上翻才知道「上面的问题」还是刚答过的那道。
7. 没有重试:`empty_stream` 至少还有「没有写工具完成时可重试一次」(BUG-633);本情形正文非空,连这条路都不走。
### 2.2 BUG-636P2):校正流对 Mastra 的 inputSchema 拒绝信封没有识别,会把被拒的 batch 报成 completed
BUG-278 已实测:`createTool``inputSchema` 校验失败时**不抛**,而是 resolve 一个 `{ error: true, message, validationErrors }` 信封,流层会把它当普通 `tool-result`。咨询流当时按信封结构改发 `tool.failed`;校正流的 `mapStreamChunkToActivity``stream-mapping.ts` L233257)的 `tool-result` 分支没有同样的判定,`batchResultFromToolChunk``host-fallback.ts`)也会把信封当 batch 返回值。本事故里模型根本没调 batch,所以这不是触发原因;但若模型传了一个不在 `EVIDENCE_KINDS` 的 kind(例如 `debt`),回执会显示 `evidence.proposed` completed、`toolTerminalStatus` 记 completed、`publicWriteToolCompleted` 为 true——BUG-635 的守卫会被这条假 completed 绕过,且「有写工具完成不得重试」会误判。必须一起补。
## 3. 决策记录(产品授权范围)
1. **「记下了」只能由写入事实支撑。** 任何正文含「记下了」且本 attempt 没有公开写工具 completed、也没走 `applyHostFallback` 的,该正文不得到达用户;流式已发出的部分用既有 `retractSpoken()` 收回。这是对系统提示第 4 条格式要求的宿主侧兜底,不是改提示词。
2. **允许重试一次,条件与 BUG-633 完全一致**:仅当本 attempt 没有任何公开写工具 completed(无证据重放风险,不触犯 BUG-186)。新增错误码 `evidence_not_written`,与 `empty_stream` 同法——不进 `RETRYABLE_ERROR_CODES` 常量,由收尾分支按 `publicWriteToolCompleted` 决定 `retryable` / `failed`。重试 attempt 的 bootstrap 追加一行:「【重试约束】上一 attempt 没有调用 rectification-record-evidence-batch 就写了『记下了』。本轮必须先把用户原话里的带日期事件提交 batch,再用 rectification-set-focus 写下一问,最后才写正文。」
3. **两次都没写 → fail closed 但不断流。** 主持人正文用新增文案 `RECTIFICATION_USER_COPY.evidenceNotRecorded` = 「这件我还没记上。请再说一次大概年月和发生的事。」(对照 `frontend/docs/VOICE.md`,不写内部错误、不写「系统」「模型」)。turn `status=completed``answer_origin=host_fallback`phases 含 `answer.host_fallback`;**不结算计费**(用户这一轮什么都没得到)。若现有 `finalizeTurn("completed")` 与结算不可分离,允许改为 `failed` + 主持人正文,但客户端必须按 BUG-633 的失败路径 `loadCaseSnapshot` 并渲染问题行;两种实现选哪种写进进度记录。
4. **守卫的触发信号**:路由在 collect 焦点分支已拿到分类结果,通过 `runV9AgentTurn` 新 option `expectedWrite: "evidence" | null` 传入(`provide_new_evidence`、或 `answer_current_focus``has_new_dated_event``"evidence"`)。分类器为 null / 出错、或消息不在 collect 焦点分支时,退回对用户原话的年份判定 `/(?:19|20)\d{2}\s*年/u`。不得只用正则替代分类器,也不得因分类器失败而放弃守卫。`action``opening` / `read_only` 时不触发。
5. **客户端**:完成轮的 assistant 行没有 `question`,而快照 `current_question.question_source === "focus"` 且该焦点不是本轮新设(`askedTurnId` ≠ 本轮 / 本轮回执无 `rectification-set-focus`)→ 在主持人正文下**再渲染一次**问题行(复用 BUG-633 加的主持人问题行,`focus_id` 只能来自 `current_question`)。`liveQuestionOnMessages` 改为只认**最后一条** settled assistant 消息;旧消息上的同 `focus_id` 不再算「仍在显示」。
6. **BUG-636**`tool-result` 分支按结构识别信封(`error === true``validationErrors` 为对象),发 `tool.activity` `status=failed``code=tool_call_rejected``toolTerminalStatus` 记 failed`batchResultFromToolChunk` 对信封返回 null。不匹配上游英文文案。公开回执不泄漏 `validationErrors` 内容。
7. 不做的事:不改 `turn-intent-classifier.ts` 提示词;不改 Skill 10.0.21;不改引擎计分与淘汰阈值(「范围没变」另议);不给财务/健康域加特殊规则。
## 4. 硬红线
1. 有任何公开写工具 completed 的 attempt 不得重试(BUG-186、BUG-633)。
2. 兜底正文只能来自 batch 返回或本单的固定文案,不得由宿主自己复述用户原话里的「年月 + 事件」当成「记下了」——那等于宿主替模型撒谎。
3. 不得出现第二套 spinner / 骨架;问题行复用既有组件。
4. 既有测试总数不得降低;改任何既有断言写「原值 / 新值 / 原因」。
5. Bug 历史、测试、进度记录不得出现真实用户的年月事件;fixture 用虚构年份。
6. 不动 `scripts/jyotish_api_server.py`、不动迁移、不升依赖。
## 5. 任务分解
### T1 · 路由把「本轮应有写入」传给运行器(BUG-635)
- `route.ts` collect 焦点分支:分类结果落到 Agent 的两种情形计算 `expectedWrite`;其他路径传 `null`,由运行器按年份正则兜底。
- `V9AgentRunOptions` 新增 `expectedWrite?: "evidence" | null`
- 验收:单测覆盖 `provide_new_evidence``"evidence"``answer_current_focus` + `has_new_dated_event``"evidence"``answer_current_focus` + `no` 不进 Agent(既有);分类器抛错 → `null`;运行器对 `null` + 带年份原话判定为需要写入,对 `null` + 无年份原话不触发。
### T2 · 收尾守卫:无写入的「记下了」不得交付,可重试一次(BUG-635)
- `streamAttempt` 收尾在 `empty_stream` 分支之后、`loadV9CaseDossier` 之前加:`needsWrite && !publicWriteToolCompleted(toolTerminalStatus) && !hostFallbackUsed` → 第一次返回 `status="retryable"``errorCode="evidence_not_written"``retractSpoken()`,不记 `answer.composed`;第二次按决策 3 写主持人正文收口。
- 验收(`frontend/tests/rectification-v9-stream.test.ts` 或新 `rectification-unwritten-evidence.test.ts`,用假流):
- read-case → 正文「记下了:…」→ finishattempt `retryable`,无 `answer.composed`spoken 已收回。
- 第二 attempt 同形:turn `completed`,正文 = `evidenceNotRecorded` 文案,phases 含 `answer.host_fallback`,公开回执 `answer_origin=host_fallback`,未结算计费(或按决策 3 的备选路径)。
- read-case → batch completed → set-focus → 正文:行为与基线完全一致(回归)。
- read-case → batch completed → 无正文:仍走 BUG-633 的 `applyHostFallback`(回归)。
- `action="read_only"` + 带年份原话:不触发。
- `RectificationRunDiagnostic``stateMutationCommitted=false` 且新增 `expectedWrite` 字段。
### T3 · 重试 attempt 的约束句(BUG-635
- `buildAgentMessages``attempt > 1` 且上一 attempt 错误码为 `evidence_not_written` 时追加决策 2 的约束句;其他重试原因保持原句。
- 验收:单测断言两种重试原因各自的 bootstrap 内容。
### T4 · 客户端再显示仍未答完的问题(BUG-635)
- `liveQuestionOnMessages` 只认最后一条 settled assistant 消息;完成轮无 `question``current_question.question_source==="focus"` → 渲染主持人问题行。
- 验收:`rectification-surface-state.test.ts` / 聊天组件测试:旧消息带同 `focus_id` 不算 live;问题行 `focus_id` 来自 `current_question``frontend/DESIGN.md` 补「主持人问题行也用于完成轮的未答焦点」。
### T5 · 校正流识别 schema 拒绝信封(BUG-636
- `mapStreamChunkToActivity` / `mapStreamChunkToPhase` / `batchResultFromToolChunk` 按决策 6 处理信封。
- 验收:单测喂 `tool-call` + 信封 `tool-result`:事件流有 `tool.activity failed code=tool_call_rejected`、无 completed`toolTerminalStatus` 为 failed`publicWriteToolCompleted` 为 false`composeHostFallbackNarration` 不把信封当 recap;公开回执序列化后不含 `validationErrors`
### T6 · 记录
- `docs/BUG_HISTORY.md` 新增 BUG-635、BUG-636(状态、现象、触发、根因、修复、验证、防复发、关联 BUG-186/278/359/449/633)。
- `docs/tasks/PROGRESS-rectification-unwritten-evidence-claim-20260910.md`:每条 T 的测试名与结果、决策 3 选了哪条实现、`next build``/` 是否仍 Static、首屏 gzip 变化。
- `CHANGELOG.md`:「证据没记上时助手会明说并再显示原题,不再只说记下了」。
- `docs/testing/` 新增一条真人清单:在 staging 走到财务采集题,回一句带年月的欠债/收入变化;期望要么证据数 +1 且下一问出现,要么助手说「这件我还没记上」并再显示财务题;**绝不出现**「记下了」而证据数不变。
## 6. 让步顺序
T5 → T4 → T3 可依次延后(延后的写 `BLOCKED.md` 或进度记录并说明);T1 + T2 是本单的最低交付,不可省。若 T4 延后,T2 的 fail-closed 文案已含「请再说一次」,用户仍能直接回答。
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-unwritten-evidence-claim-20260910 .worktrees/rectification-unwritten-evidence-claim-20260910 origin/staging
cd .worktrees/rectification-unwritten-evidence-claim-20260910/frontend
grep -c "^## BUG-" ../docs/BUG_HISTORY.md # 核对最大号仍为 634
npm test -- tests/rectification-v9-stream.test.ts tests/rectification-host-fallback.test.ts tests/rectification-spoken-collect.test.ts tests/rectification-surface-state.test.ts # 记下基线 pass 数
```
## 8. 验收口径
- `tsc --noEmit` 0 错;`npm run lint` 0 error;上述四个测试文件加新文件 fail=0,测试总数 ≥ 基线。
- `next build``/``○ Static`,首屏 gzip ±2%。
- 推 staging 后 `/api/health``deployment.gitCommit` 等于含代码改动的最新提交。
- 真实环境清单见 T6;做不了的写成环境缺口,不得写「通过」。