Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
10 KiB
TASK · 生时校正修复单:停止后重算丢掉全部答题、采用报 candidate_state_inconsistent;无探针的区分题退化成开场句(2026-09-07)
- 基线:
origin/staging@63eba420(代码头4e0db55f,staging 已部署84a29754,事故就是这版跑出来的) - 分支:
codex/rectification-stop-rescore-fix-20260907 - 执行方:coding agent;验收:Claude
- 涉及文件:
frontend/src/lib/rectification-agentic/v9/block-scan-answer.ts(rescoreMinuteAfterWindowChange)、frontend/src/mastra/rectification-v9-tools.ts(scoreAndPersistCurrentEvidence抽出)、v9/tool-service.ts、v9/server-focus.ts(persistSpokenChoiceFallback)、v9/method-followup.ts(spokenCollectFallbackFollowup、precisionStageFollowup、holdout 排序) - BUG 编号起点:BUG-581(开工时
grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1复核) - 优先级:P0(采用按钮点了就报错;BUG-579 的重算把用户答过的题全部作废)
1. 事故实证(staging 84a29754,2026-09-07 第三次实测;只写结构)
| # | 现象 | 代码定位 |
|---|---|---|
| 1 | 五张卡答完,旁白说范围已收到 X–Y(11 分钟内的更窄一段);健康拒答后助手问回开场那句"从你最容易想起来的一件事开始就好——比如哪年上的大学…"(GENERIC_COLLECT_QUESTION 原文) |
见 §2.2 |
| 2 | 用户点「先这样,先看当前范围」→ 旁白"再问下去也分不开 A 和 B…已从 30 分钟收到 X'–Y' 这 11 分钟"——范围比答完最后一张卡时反而变宽了,还多出一个原本已被淘汰、在可信区间之外的分钟 | 见 §2.1 |
| 3 | 候选卡出现 4 张,"相对支持度 16 / 13 / 12 / 8"——这是引擎裸结果的 relative_support,不是答题后的 posterior(答题后应是 21 / 18 / 9 一类) |
同上 |
| 4 | 点任一"采用此时间" → {"error":"候选状态已更新,请刷新后重试","code":"candidate_state_inconsistent"} |
采用 RPC accept_agentic_rectification_candidate_for_case_v2(20260906020000_adopted_credible_range.sql L162–183)要求 decision_receipt.inference_state 存在且 candidates 非空、revision/candidate_set_id 与最新 transition 一致;否则抛 candidate_state_inconsistent |
2. 根因
2.1 BUG-581(P0):停止路径用的是"裸重算",把推断态整个丢了
BUG-579 让 STOP / 无焦点停止 / 闲置持久化在快照过期时先调 rescoreStaleMinuteSnapshotIfNeeded → rescoreMinuteAfterWindowChange(block-scan-answer.ts L160–200)。这个函数是为"时段选定 / 放宽窗口"写的:只 runV9CandidateScore 然后 persistV9Candidate,不调 buildCaseInferenceState,不带 previous,不写 inference_state,不写 transition。而正规比较路径 rectification-v9-tools.ts::scoreAndPersistCurrentEvidence(L879–1190)会用上一份 inference_state 作 previous,按 semantic_key 重放已答探针,再把 inference_state 放进 receipt。
后果链:停止 → 裸重算 → 最新结果没有 inference_state → (a) 投影退回引擎裸 relative_support,范围变宽、被淘汰分钟复活、卡片数字变成 16/13/12/8;(b) 采用 RPC 校验 inference_state 为空 → candidate_state_inconsistent。同样的裸重算也用于时段选定和放宽窗口之后,那两条路的采用同样会报错(尚未实测,按代码必然)。
2.2 BUG-582(P1):没有探针支撑的"区分题"退化成开场句
buildMethodFollowupPlan 在几个分支会给出 frameless 的 distinguish_candidates followup(precisionStageFollowup 的 lagna_frame 分支 domain: null;varga_observation 分支;else if (precisionCard) next = precisionCard),它们没有 choice_frame。persistServerOwnedFocus 对无框区分题走 persistSpokenChoiceFallback → spokenCollectFallbackFollowup:intent 改成采集、domain = collectQuestionDomain(null) = "other"、source = "method_coverage"。spokenFollowupForUser 看到 domain=other && source=method_coverage && !collect_retry 判成 开场(openingOther),题干就是 GENERIC_COLLECT_QUESTION;rectification-v9-tools.ts L1447 的 USER_COLLECT_QUESTION[""] ?? GENERIC_COLLECT_QUESTION 是同一句的第二条来路。这就是"死问题"再次出现的机制,与 BUG-558 的 other 兜底无关,是另一条路。
2.3 P3:holdout 改成采集题后排在带年月采集之前
BUG-580 把 holdout 改为 collect_method_evidence,但它在计划里仍排在 nextDatedCollectFollowup 之前,所以这轮先问了健康(oos 领域)再轮到搬家;健康拒答后搬家没被问到就进了 §2.2 的分支。顺序应当是:带年月采集(DATED_COLLECT_ORDER)→ 职业 → holdout。
3. 决策记录
- 只有一条重算路径。 把
scoreAndPersistCurrentEvidence从工具闭包抽到tool-service.ts(或新文件v9/rescore.ts),rescoreMinuteAfterWindowChange与rescoreStaleMinuteSnapshotIfNeeded都调它。同窗口重算带previous(重放已答探针);窗口变了(时段选定 / 放宽)不带previous,但仍要写出新的inference_state(空答案)与 transition。 - 采用 RPC 的校验不放宽。
- 无框区分题不得持久化为口述题。
persistSpokenChoiceFallback只在followup.domain落在USER_COLLECT_QUESTION且intent原本就是采集时才落地;无框的distinguish_candidates一律skipped。计划层:precisionStageFollowup/varga_observation分支只在choice_frame存在时才当next,否则记进dropped_probes(reason="frameless_distinguish")并继续往下走(带年月采集 → 职业 → holdout → 交付)。spokenFollowupForUser的openingOther判定加followup.source === "method_coverage" && followup.method_id === "dasha_events" && !evidence.some(isConfirmedDated),即只有真正的开场才出 GENERIC;rectification-v9-tools.tsL1447 的 GENERIC 兜底删除,域不在表里就返回invalid_spoken_prompt不落地。 - holdout 采集排到
nextDatedCollectFollowup与职业之后。 - 不动采用门、确认门、
MIN_SEPARATION_LEAD、_relative_support、minute_step=1指纹。
4. 任务分解
4.1 BUG-581
- 抽出并复用
scoreAndPersistCurrentEvidence;rescoreMinuteAfterWindowChange(…, { keepAnswers: boolean }):停止/闲置 →keepAnswers=true(previous= 上一份 inference);时段/放宽 →false。 - 验收:
rectification-stale-compare-fix-20260907.test.ts扩——(a) 5 条已答探针 + 快照过期 → STOP 后latest_result.decisionReceipt.inference_state.answered_probes.length === 5,credible_range与停止前一致或更窄,候选posterior_score不是引擎裸支持度;(b) 采用 RPC 夹具(database-rectification-*.test.ts,Docker)在 STOP 重算后能通过inference_state校验;(c) 时段选定 / 放宽后inference_state存在且answered_probes为空,采用不报 inconsistent。
4.2 BUG-582
- 决策 3 三处改动;删除 GENERIC 兜底。
- 验收:形状用例——7 件事、家人/健康拒答、职业未问、
precision_stage=lagna_frame或 d4 观察分歧、无可渲染探针 → 下一问是搬家采集或职业口述,不是GENERIC_COLLECT_QUESTION;persistSpokenChoiceFallback对domain=null的区分题返回skipped;agent-voice-copy-contract增加"GENERIC 只允许出现在collect:other:*开场焦点"。
4.3 P3 顺序
- holdout 后移;验收:健康 oos 领域存在且搬家未问时,下一问是搬家。
4.4 记录
docs/BUG_HISTORY.mdBUG-581(复发自 BUG-579)、BUG-582(复发自 BUG-558:同一"死问题"现象、不同来路);CHANGELOG.md;PROGRESS-rectification-stop-rescore-fix-20260907.md;docs/testing/rectification-scenarios-20260907.md第 0 条加"停止后卡片数字必须等于答题后的支持度,且点采用成功"。
5. 让步顺序
4.1 先做、当天部署;4.2 必做;4.3 可后置;4.4 不可省。
6. 开工前置命令
git fetch origin --prune
git worktree add -b codex/rectification-stop-rescore-fix-20260907 .worktrees/rectification-stop-rescore-fix-20260907 origin/staging
cd .worktrees/rectification-stop-rescore-fix-20260907
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
ln -s /workspace/Jyotisha/.venv .venv
cd frontend && ls tests/rectification-*.test.ts tests/consultation-*.test.ts tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"
验收(Claude,2026-09-07,origin/staging @ ab57d03f;与区间交付卡同一提交)
| 门 | 结果 |
|---|---|
| tsc | 1 错(在同提交的区间交付卡改动里:rectification-agentic-chat.tsx L1655 先访问 candidateResult.resultId 再判空,'candidateResult' is possibly 'null')。执行方进度记录写 tsc 0,与实测不符——应是最后一次改动后没有复跑 |
| lint | 0 error / 103 warning(比上一轮多 19 条,分散在测试文件与 rectification-v9-tools.ts,未清) |
| 前端 rectification + consultation + birth-time + skill + voice(非 DB) | 1605 / 0 |
| Python v5_services / input_contract / growth contract | 全绿 |
引擎 minute_step=1 字节级不变 |
全等 |
| 项 | 结论 |
|---|---|
| 4.1 BUG-581 | 通过。scoreAndPersistCurrentEvidence 抽到 v9/score-persist.ts,停止/闲置 keepAnswers=true 带 previous 重放;时段/放宽 keepAnswers=false 仍写出空答案的 inference_state;VedAstro 校验与对比包逻辑随之搬迁 |
| 4.2 BUG-582 | 通过。无框区分题一律 skipped;工具层与 spokenFollowupForUser 的 GENERIC 兜底删除;dropped_probes(frameless_distinguish) 记录 |
| 4.3 顺序 | 通过。holdout 排在带年月采集与职业之后 |
| 4.4 记录 | 通过 |
结论:本单逻辑通过,但同一提交因区间交付卡的 tsc 错误整体不可部署,见 TASK-rectification-range-delivery-card-20260907.md 验收段。