Files
Jyotisha/docs/tasks/TASK-rectification-stop-rescore-fix-20260907.md
T

10 KiB
Raw Blame History

TASK · 生时校正修复单:停止后重算丢掉全部答题、采用报 candidate_state_inconsistent;无探针的区分题退化成开场句(2026-09-07)

  • 基线:origin/staging @ 63eba420(代码头 4e0db55fstaging 已部署 84a29754,事故就是这版跑出来的)
  • 分支:codex/rectification-stop-rescore-fix-20260907
  • 执行方:coding agent;验收:Claude
  • 涉及文件:frontend/src/lib/rectification-agentic/v9/block-scan-answer.tsrescoreMinuteAfterWindowChange)、frontend/src/mastra/rectification-v9-tools.tsscoreAndPersistCurrentEvidence 抽出)、v9/tool-service.tsv9/server-focus.tspersistSpokenChoiceFallback)、v9/method-followup.tsspokenCollectFallbackFollowupprecisionStageFollowup、holdout 排序)
  • BUG 编号起点:BUG-581(开工时 grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1 复核)
  • 优先级:P0(采用按钮点了就报错;BUG-579 的重算把用户答过的题全部作废)

1. 事故实证(staging 84a297542026-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_v220260906020000_adopted_credible_range.sql L162183)要求 decision_receipt.inference_state 存在且 candidates 非空、revision/candidate_set_id 与最新 transition 一致;否则抛 candidate_state_inconsistent

2. 根因

2.1 BUG-581P0):停止路径用的是"裸重算",把推断态整个丢了

BUG-579 让 STOP / 无焦点停止 / 闲置持久化在快照过期时先调 rescoreStaleMinuteSnapshotIfNeededrescoreMinuteAfterWindowChangeblock-scan-answer.ts L160–200)。这个函数是为"时段选定 / 放宽窗口"写的:只 runV9CandidateScore 然后 persistV9Candidate不调 buildCaseInferenceState,不带 previous,不写 inference_state,不写 transition。而正规比较路径 rectification-v9-tools.ts::scoreAndPersistCurrentEvidenceL8791190)会用上一份 inference_stateprevious,按 semantic_key 重放已答探针,再把 inference_state 放进 receipt。

后果链:停止 → 裸重算 → 最新结果没有 inference_state → (a) 投影退回引擎裸 relative_support,范围变宽、被淘汰分钟复活、卡片数字变成 16/13/12/8(b) 采用 RPC 校验 inference_state 为空 → candidate_state_inconsistent。同样的裸重算也用于时段选定和放宽窗口之后,那两条路的采用同样会报错(尚未实测,按代码必然)。

2.2 BUG-582P1):没有探针支撑的"区分题"退化成开场句

buildMethodFollowupPlan 在几个分支会给出 framelessdistinguish_candidates followupprecisionStageFollowuplagna_frame 分支 domain: nullvarga_observation 分支;else if (precisionCard) next = precisionCard),它们没有 choice_framepersistServerOwnedFocus 对无框区分题走 persistSpokenChoiceFallbackspokenCollectFallbackFollowupintent 改成采集、domain = collectQuestionDomain(null) = "other"source = "method_coverage"spokenFollowupForUser 看到 domain=other && source=method_coverage && !collect_retry 判成 开场openingOther),题干就是 GENERIC_COLLECT_QUESTIONrectification-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. 决策记录

  1. 只有一条重算路径。scoreAndPersistCurrentEvidence 从工具闭包抽到 tool-service.ts(或新文件 v9/rescore.ts),rescoreMinuteAfterWindowChangerescoreStaleMinuteSnapshotIfNeeded 都调它。同窗口重算带 previous(重放已答探针);窗口变了(时段选定 / 放宽)不带 previous,但仍要写出新的 inference_state(空答案)与 transition。
  2. 采用 RPC 的校验不放宽。
  3. 无框区分题不得持久化为口述题。 persistSpokenChoiceFallback 只在 followup.domain 落在 USER_COLLECT_QUESTIONintent 原本就是采集时才落地;无框的 distinguish_candidates 一律 skipped。计划层:precisionStageFollowup / varga_observation 分支只在 choice_frame 存在时才当 next,否则记进 dropped_probes(reason="frameless_distinguish") 并继续往下走(带年月采集 → 职业 → holdout → 交付)。spokenFollowupForUseropeningOther 判定加 followup.source === "method_coverage" && followup.method_id === "dasha_events" && !evidence.some(isConfirmedDated),即只有真正的开场才出 GENERIC;rectification-v9-tools.ts L1447 的 GENERIC 兜底删除,域不在表里就返回 invalid_spoken_prompt 不落地。
  4. holdout 采集排到 nextDatedCollectFollowup 与职业之后。
  5. 不动采用门、确认门、MIN_SEPARATION_LEAD_relative_supportminute_step=1 指纹。

4. 任务分解

4.1 BUG-581

  • 抽出并复用 scoreAndPersistCurrentEvidencerescoreMinuteAfterWindowChange(…, { keepAnswers: boolean }):停止/闲置 → keepAnswers=trueprevious = 上一份 inference);时段/放宽 → false
  • 验收:rectification-stale-compare-fix-20260907.test.ts 扩——(a) 5 条已答探针 + 快照过期 → STOP 后 latest_result.decisionReceipt.inference_state.answered_probes.length === 5credible_range 与停止前一致或更窄,候选 posterior_score 不是引擎裸支持度;(b) 采用 RPC 夹具(database-rectification-*.test.tsDocker)在 STOP 重算后能通过 inference_state 校验;(c) 时段选定 / 放宽后 inference_state 存在且 answered_probes 为空,采用不报 inconsistent。

4.2 BUG-582

  • 决策 3 三处改动;删除 GENERIC 兜底。
  • 验收:形状用例——7 件事、家人/健康拒答、职业未问、precision_stage=lagna_frame 或 d4 观察分歧、无可渲染探针 → 下一问是搬家采集或职业口述,不是 GENERIC_COLLECT_QUESTIONpersistSpokenChoiceFallbackdomain=null 的区分题返回 skippedagent-voice-copy-contract 增加"GENERIC 只允许出现在 collect:other:* 开场焦点"。

4.3 P3 顺序

  • holdout 后移;验收:健康 oos 领域存在且搬家未问时,下一问是搬家。

4.4 记录

  • docs/BUG_HISTORY.md BUG-581(复发自 BUG-579)、BUG-582(复发自 BUG-558:同一"死问题"现象、不同来路);CHANGELOG.mdPROGRESS-rectification-stop-rescore-fix-20260907.mddocs/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)"

验收(Claude2026-09-07origin/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=trueprevious 重放;时段/放宽 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 验收段。