Files
Jyotisha/docs/tasks/TASK-rectification-unwritten-evidence-claim-20260910.md
T
Jesse_ChenandCursor a998b6ec53
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
fix(rectification): stop unwritten-evidence claims and same-cluster dasha false conflicts (BUG-635–640)
Host only says 记下了 after a real write; Mastra schema rejections fail closed. Ledger year keys no longer drop quality probes, dual-dasha agreement is per cluster, width uses cluster span, and public house tables follow the inference minute.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 17:38:37 +08:00

18 KiB
Raw Blame History

TASK · 证据轮模型只说「记下了」却没写证据、没设下一问:财务一件落空后流程停在原题(2026-09-10)

  • 基线:origin/staging @ d96b24c2BUG-626/627/633/634 已合入并部署;/api/healthdeployment.gitCommit = d96b24c2
  • 分支:codex/rectification-unwritten-evidence-claim-20260910,基于 origin/staging
  • 串行:无同文件并行单。TASK-rectification-evidence-turn-empty-answer-20260910.mdBUG-633/634)已合入基线,本单在其 applyHostFallback / 失败刷新之上叠加,不得回退它的任何断言。
  • 执行方:coding agent;验收:Claude
  • 涉及文件(行号按 d96b24c2,定位以符号为准):frontend/src/app/api/rectification/agent/route.tscollect 焦点分支 L522597、classifyRectificationTurnIntent L544、runV9AgentTurn({ L847)、frontend/src/lib/rectification-agentic/v9/agent-run.tsV9AgentRunOptions L96112、RETRYABLE_ERROR_CODES L150、applyHostFallback L843、streamAttempt 收尾 L10251090、RectificationRunDiagnostic.stateMutationCommitted L1109、buildAgentMessages L1180)、v9/host-fallback.tspublicWriteToolCompleted L16)、v9/stream-mapping.tsmapStreamChunkToActivity L233257)、frontend/src/lib/rectification-agentic/user-copy.tsRECTIFICATION_USER_COPY)、frontend/src/components/rectification-agentic-chat.tsxliveQuestionOnMessages L1501、questionGap L1513、输入框占位 L1855)、frontend/src/lib/rectification-surface-state.tsrectificationQuestionGapState L300
  • BUG 编号起点:BUG-635docs/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 completedphases = 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_evidencestatus=activequestion_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_evidenceanswer_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_truncatedmax_steps/provider_error!answerText.trim()empty_stream)。正文非空就直接 completeAttempt()不看 toolTerminalStatus 里有没有公开写工具完成stateMutationCommitted = publicWriteToolCompleted(...) || hostFallbackUsed 这个值算出来了(L1109),但只进日志,不参与决策。
  4. applyHostFallbackL843BUG-633)只覆盖「batch 已完成、正文为空」。它的镜像——「正文说记下了、batch 没跑」——没有任何守卫。
  5. 成功收尾的焦点守卫(L1056)只在 decision.nextAction === "ask_candidate_discriminator" 时要求有已持久化的点选焦点;采集阶段里旧的财务焦点仍是 activedecideFromDossier 认为一切正常。askedFocus?.askedTurnId === turnIdL1066)也不成立,不会补 collectHandoff
  6. 客户端 liveQuestionOnMessagesL1501)只要求任一 settled assistant 消息上挂着与 current_question.focus_id 相同的问题——上一条消息的财务题满足条件,于是 questionGap 不报缺口、不补问题行、输入框占位照常写「请回答上面的问题…」。用户看到的是一句「记下了」和一片空白,要往上翻才知道「上面的问题」还是刚答过的那道。
  7. 没有重试:empty_stream 至少还有「没有写工具完成时可重试一次」(BUG-633);本情形正文非空,连这条路都不走。

2.2 BUG-636P2):校正流对 Mastra 的 inputSchema 拒绝信封没有识别,会把被拒的 batch 报成 completed

BUG-278 已实测:createToolinputSchema 校验失败时不抛,而是 resolve 一个 { error: true, message, validationErrors } 信封,流层会把它当普通 tool-result。咨询流当时按信封结构改发 tool.failed;校正流的 mapStreamChunkToActivitystream-mapping.ts L233257)的 tool-result 分支没有同样的判定,batchResultFromToolChunkhost-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=completedanswer_origin=host_fallbackphases 含 answer.host_fallback不结算计费(用户这一轮什么都没得到)。若现有 finalizeTurn("completed") 与结算不可分离,允许改为 failed + 主持人正文,但客户端必须按 BUG-633 的失败路径 loadCaseSnapshot 并渲染问题行;两种实现选哪种写进进度记录。
  4. 守卫的触发信号2026-09-10 产品口径修订:业务判断不得靠正则):路由在 collect 焦点分支已拿到分类结果,通过 runV9AgentTurn 新 option expectedWrite: "evidence" | "none" | "unknown" 传入(provide_new_evidence、或 answer_current_focushas_new_dated_event"evidence")。消息不在 collect 焦点分支时,也对用户原话跑一次同一分类器(无焦点时 current_question 传空),拿 has_new_dated_event / provide_new_evidence 判定。分类器为 null / 出错:重试一次,仍失败则 "unknown",守卫 fail-open 并在 RectificationRunDiagnosticexpectedWrite=unknown不得用年份正则或关键词兜底actionopening / 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-636tool-result 分支按结构识别信封(error === truevalidationErrors 为对象),发 tool.activity status=failedcode=tool_call_rejectedtoolTerminalStatus 记 failedbatchResultFromToolChunk 对信封返回 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;非 collect 的 message 路径也对原话跑同一分类器。分类失败两次则 "unknown",守卫 fail-open,不用年份正则。
  • V9AgentRunOptions 新增 expectedWrite?: "evidence" | "none" | "unknown"
  • 验收:单测覆盖 provide_new_evidence"evidence"answer_current_focus + has_new_dated_event"evidence"answer_current_focus + no 不进 Agent(既有);分类器两次都抛错 → "unknown" 且守卫不触发、诊断里有记录;源码合同断言 agent-run.ts / route.ts 不含年份正则。

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.composedspoken 已收回。
    • 第二 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" + 带年份原话:不触发。
    • RectificationRunDiagnosticstateMutationCommitted=false 且新增 expectedWrite 字段。

T3 · 重试 attempt 的约束句(BUG-635

  • buildAgentMessagesattempt > 1 且上一 attempt 错误码为 evidence_not_written 时追加决策 2 的约束句;其他重试原因保持原句。
  • 验收:单测断言两种重试原因各自的 bootstrap 内容。

T4 · 客户端再显示仍未答完的问题(BUG-635)

  • liveQuestionOnMessages 只认最后一条 settled assistant 消息;完成轮无 questioncurrent_question.question_source==="focus" → 渲染主持人问题行。
  • 验收:rectification-surface-state.test.ts / 聊天组件测试:旧消息带同 focus_id 不算 live;问题行 focus_id 来自 current_questionfrontend/DESIGN.md 补「主持人问题行也用于完成轮的未答焦点」。

T5 · 校正流识别 schema 拒绝信封(BUG-636

  • mapStreamChunkToActivity / mapStreamChunkToPhase / batchResultFromToolChunk 按决策 6 处理信封。
  • 验收:单测喂 tool-call + 信封 tool-result:事件流有 tool.activity failed code=tool_call_rejected、无 completedtoolTerminalStatus 为 failedpublicWriteToolCompleted 为 falsecomposeHostFallbackNarration 不把信封当 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. 开工前置命令

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/healthdeployment.gitCommit 等于含代码改动的最新提交。
  • 真实环境清单见 T6;做不了的写成环境缺口,不得写「通过」。