diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 60719346..e6b22483 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -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-629,Skill 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-631~632) | | `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 报成 completed(BUG-636) | 待执行 | `codex/rectification-unwritten-evidence-claim-20260910` | ### 聊天主链路与首页 diff --git a/docs/tasks/TASK-rectification-unwritten-evidence-claim-20260910.md b/docs/tasks/TASK-rectification-unwritten-evidence-claim-20260910.md new file mode 100644 index 00000000..0496125d --- /dev/null +++ b/docs/tasks/TASK-rectification-unwritten-evidence-claim-20260910.md @@ -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 焦点分支 L522–597、`classifyRectificationTurnIntent` L544、`runV9AgentTurn({` L847)、`frontend/src/lib/rectification-agentic/v9/agent-run.ts`(`V9AgentRunOptions` L96–112、`RETRYABLE_ERROR_CODES` L150、`applyHostFallback` L843、`streamAttempt` 收尾 L1025–1090、`RectificationRunDiagnostic.stateMutationCommitted` L1109、`buildAgentMessages` L1180)、`v9/host-fallback.ts`(`publicWriteToolCompleted` L16)、`v9/stream-mapping.ts`(`mapStreamChunkToActivity` L233–257)、`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 +0800,Skill 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` completed(48 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` L522–597)先跑 `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`(L843,BUG-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-636(P2):校正流对 Mastra 的 inputSchema 拒绝信封没有识别,会把被拒的 batch 报成 completed + +BUG-278 已实测:`createTool` 的 `inputSchema` 校验失败时**不抛**,而是 resolve 一个 `{ error: true, message, validationErrors }` 信封,流层会把它当普通 `tool-result`。咨询流当时按信封结构改发 `tool.failed`;校正流的 `mapStreamChunkToActivity`(`stream-mapping.ts` L233–257)的 `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 → 正文「记下了:…」→ finish:attempt `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;做不了的写成环境缺口,不得写「通过」。