Files
Jyotisha/docs/tasks/TASK-rectification-stale-compare-fix-20260907.md
T
2026-09-07 07:53:46 +00:00

13 KiB
Raw Blame History

TASK · 生时校正修复单:答完性格卡后每次候选比较都静默失败,快照过期把对话拖进死胡同(2026-09-07)

  • 基线:origin/staging @ 7cf7705a(代码头 8e31680b,staging 已部署该版本,事故就是这版跑出来的)
  • 分支:codex/rectification-stale-compare-fix-20260907
  • 执行方:coding agent;验收:Claude
  • 涉及文件:frontend/src/lib/rectification-agentic/v9/inference-adapter.tsengine-client.tsfrontend/src/mastra/rectification-v9-tools.tsfrontend/src/lib/rectification-agentic/core/rectification-decision.tsv9/answer-choice.tsv9/method-followup.tsholdoutFollowupFor)、v9/agent-run.tsscripts/rectification/contracts.pyfrontend/src/lib/rectification-agentic/user-copy.ts
  • BUG 编号起点:BUG-575(BUG-574 已被报告列表修复占用;开工时 grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1 复核)
  • 优先级:P0,先于 docs/testing/rectification-scenarios-20260907.md 的走查;不修这条,走查全部作废

1. 事故实证(staging2026-09-07,产品负责人实测;Case JSON 已由产品负责人提供,本单只写结构,不写年份与经历)

对话走向:开场 → 三条口述采集(学业、感情、工作,共 5 件带年月事件)→ 引擎比较成功(21.6 s)→ 两张性格卡(D9 相处方式、D10 做事风格)→ 三张年月存在卡 → 家人拒答 → 财务采集(1 件)→ 搬家采集(1 件)→ 健康拒答 → 职业口述 → 助手说"也再说一件你能记得大致时间的事吧" → 用户点「先这样,先看当前范围」→ 助手写了一段"再问下去也分不开 X 和 Y…下面是这次的结果" → 没有候选卡,界面只有"目前范围 X–Y,还在收窄"和"没有拿到下一个问题"。

Case JSON 里的关键事实:

# 事实 位置
1 latest_result.createdAt 停在第 3 条口述之后;财务与搬家两轮的 rectification-compare-candidates 都是 status: failed, detail: {error: "tool_failed"},耗时 28 ms / 37 ms(不是引擎超时,是请求被拒) turns[].receipt.tool_activities
2 财务轮之后 askedProbeKeys 里带上了两张性格卡的 candidate_split_hash,形如 04:45-05:15:<9 个分钟>:varga.d9.<分组>长 128 字符nakshatra 边界探针的 hash 126 字符 inference_state.answered_probes[].candidate_split_hash
3 引擎 normalize_rectification_requestasked_probe_keys 每条限 120 字符,超限直接 ValueErrorBadRequest → 工具抛错 scripts/rectification/contracts.py L339
4 TS 把 probe_idsemantic_keycandidate_split_hash 三种键都塞进 asked_probe_keys inference-adapter.ts::askedProbeKeysFromReceipt L5355BUG-559 引入)
5 于是:任何 varga_style / nakshatra 卡答过之后,后续每一次比较都失败;账本已有 7 件事,latest_result 只算了 5 件,evidenceLedgerFingerprint 与当前账本不等 → snapshotCurrent=false decision-from-dossier.ts::scoreableSnapshotCurrentFromDossier
6 decideRectificationsnapshotCurrent === false 分支排在 userStopped 之前:用户点了停止、case.status=paused,决策仍是 collecttype: ask_fact_collectionsession_outcome: collect_evidencecan_adopt: false → 卡片条件 ADOPT_OUTCOMES 不满足 → 无卡;current_question: null → 客户端显示"没有拿到下一个问题" rectification-decision.ts L285290rectification-surface-state.ts L31
7 工具失败被 Agent 吞掉:助手正文仍写"这条记下了…对缩小候选范围很有帮助",用户不知道比较根本没跑 rectification-v9-tools.ts L2073 只记 receipt 后 throwagent-run 继续作答
8 "也再说一件你能记得大致时间的事吧"来自 oos_blind:holdout 焦点(kind: reverse_verify):holdoutFollowupFor 从过期 oos_blind_prompts 里取了"财务这条线还没用过"(账本其实已有财务事件),模型把它改写成泛泛的"再说一件事" method-followup.ts L16841694next_user_action.user_meaning
9 采集轮正文重复:第 1、2 轮助手文字各出现两段近似复述;工作采集题在正文出现一次、问题块又出现一次 turns[2].textturns[4].text

2. 根因

  1. BUG-575P0asked_probe_keyscandidate_split_hash 也传给引擎,varga/nakshatra 的 hash 超过引擎 120 字符上限,比较请求被 400 拒绝。BUG-559 只在 test_rectification_event_probes.py 用短键测过,没有用真实 split hash 测过整条 compare。
  2. BUG-576P1:比较失败没有任何用户可见后果:receipt 只有 tool_failed,没有错误码;Agent 继续写"记下了、很有帮助";下一轮也不重试。
  3. BUG-577P1decideRectification 让"快照过期"压过"用户已停止",停止后交付不出来;persistNextInterviewIfIdle 的停止路径也不先重算。
  4. BUG-578P1holdoutFollowupFor 只排除拒答领域,不排除账本里已有事件的领域,而且用的是可能过期的 oos_blind_prompts;焦点是 reverse_verify,题干交给模型自由改写,成了"再说一件事"。
  5. P2:采集轮正文双写;题干进正文。

3. 决策记录

  1. asked_probe_keys 只传 semantic_key(去重、≤120),不传 probe_idcandidate_split_hash;引擎侧上限提到 200 并对超长键跳过而不是拒绝(记 dropped_asked_probe_keys 计数进 receipt)。TS 侧再加一层保险:超过 200 的键在 engineRequestBody 里过滤并 console.warn
  2. 比较失败必须可见:receipt detailsafe_error_code 与引擎返回的错误文本前 120 字;agent-run 在工具失败后给正文追加服务端固定句"候选比较这次没跑成,下一句话时会自动再试";下一轮 persistNextInterviewIfIdlesnapshotCurrent=false 且账本有可评分事件时先重算一次(复用 rescoreMinuteAfterWindowChange),再决策。
  3. 用户停止优先于快照过期:decideRectificationuserStopped && ranked.length>0 提到 snapshotCurrent 判断之前;停止路径(route 的 stop_and_reviewapplyRectificationChoice STOP)在决策前先尝试重算,重算失败则按上一次有效结果交付,旁白加一句"这是按上一次成功比较给出的范围"。
  4. holdoutFollowupFor:候选领域 = oos_blind_prompts既未拒答、账本里也没有该领域带年月事件 的领域;没有则 holdout 记为 unavailable,直接进交付;题干用服务端固定句(USER_COLLECT_QUESTION[domain] 同源,不给模型改写),焦点 kind 改为 collect_spoken 而不是 reverse_verify
  5. 正文双写:composeCollectSpokenAssistantText 与模型正文若都包含采集题,只保留问题块;模型正文里与 spokenPrompt 相同或高度相似(前 12 字相同)的句子删除;两段复述的根因要查(phasesintent.classified 出现两次),进度记录写明。

4. 硬红线

  • 不动采用门、确认门、MIN_SEPARATION_LEAD_relative_support
  • minute_step=1 路径字节不变(指纹用例)。
  • BUG-559 的去重用例照常通过(去重靠 semantic_key,不靠 hash)。
  • 测试总数 ≥ 1899tsc 0 错;lint 0 error。
  • 任务书 / 进度 / Bug 历史不得写入产品负责人 Case JSON 里的年份、经历、Case ID。

5. 任务分解

5.1 BUG-575

  • askedDiscriminatorKeys 拆成两份:askedSemanticKeys(给引擎)与现有全量(给 TS 去重);engineRequestBody 只用前者并过滤 >200contracts.py 上限 200、超长跳过、计数进 decision_receipt.dropped_asked_probe_keys
  • 验收:tests/test_rectification_input_contract.py——128 字符键不再报错、被跳过并计数;TS rectification-v9-engine-contract.test.ts——用真实 varga split hash(≥128 字符)构造 receiptengineRequestBody.asked_probe_keys 全部 ≤120 且不含 :varga.端到端rectification-probe-year-dedupe 加一例——答过 D9 卡后 compare 请求体合法。

5.2 BUG-576

  • receipt failed detail 加 safe_error_code + engine_message(截 120 字,不含用户资料);agent-run 工具失败 → 正文追加固定句;persistNextInterviewIfIdleinspectNonTerminalTurnExitsnapshotCurrent=false 时先重算一次(有节流:同一证据指纹只重试一次,记 rescore_attempted_fingerprint)。
  • 验收:rectification-exhaustion-exit 或新文件——比较抛错的回合正文含固定句;下一回合看到过期快照会调用一次 runV9CandidateScore,成功后决策不再是 collect

5.3 BUG-577

  • decideRectification 顺序调整(决策 3);stop 路径先重算。
  • 验收:rectification-decision-authority.test.ts 三栏——snapshotCurrent=false && userStopped && ranked>0completeWithRange(user_stopped)session_outcome ∈ ADOPT_OUTCOMESrectification-window-widen 或 answer-choice 测试——STOP 在过期快照上先重算再交付。

5.4 BUG-578

  • holdoutFollowupFor 过滤已有事件领域;无可问领域 → holdoutValidation="unavailable";焦点改 collect_spoken + 服务端题干。
  • 验收:rectification-choice-cardcollect-direction 测试——账本已有财务事件且家人/健康拒答 → 不出 holdout 题,决策直接交付;有可问领域时题干等于 USER_COLLECT_QUESTION[domain],不是"再说一件事"。

5.5 P2 正文双写

  • 定位 intent.classified 双触发与正文拼接;去重规则见决策 5。
  • 验收:rectification-spoken-collect.test.ts——采集轮最终正文不含与 spokenPrompt 重复的句子;同一段复述不出现两次。

5.6 记录

  • docs/BUG_HISTORY.md BUG-575578575 标"复发自 BUG-559");CHANGELOG.mdPROGRESS-rectification-stale-compare-fix-20260907.mddocs/testing/rectification-scenarios-20260907.md 加第 0 条"答完性格卡后再说一件事,顶部范围必须变化或旁白说明比较结果"。

6. 让步顺序

5.1 单独就能解封线上,当天必须合入并部署5.2 + 5.3 其次;5.4 必做;5.5 可后置到同分支第二次提交;5.6 不可省。

7. 开工前置命令

git fetch origin --prune
git worktree add -b codex/rectification-stale-compare-fix-20260907 .worktrees/rectification-stale-compare-fix-20260907 origin/staging
cd .worktrees/rectification-stale-compare-fix-20260907
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
ln -s /workspace/Jyotisha/.venv .venv
.venv/bin/python -c "print(len('04:45-05:15:04:47,04:51,04:53,04:59,05:00,05:06,05:08,05:13,05:15:varga.d9.04:47|04:51/05:00|05:06|04:59|04:53/05:08|05:13|05:15'))"   # 128
.venv/bin/python -m pytest tests/test_rectification_input_contract.py tests/test_rectification_v5_services.py -q
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 @ 84a29754,实现 4e0db55f;落地编号 BUG-577580

结果
tsc / lint 0 错 / 0 error84 warning,既有类型)
前端 rectification + consultation + birth-time + voice(非 DB 1556 / 0
Python input_contract / v5_services / event_probes / engine_convergence / growth contract 全绿
引擎实测:把事故里 128 字符的 varga split hash + 一个 250 字符键传给 normalize_rectification_request 不再 400:128 字符保留,250 字符跳过并计数 dropped_asked_probe_keys=1score_candidates 正常返回,receipt 带计数
minute_step=1 字节级不变(虚构 7 件事新旧复跑) candidate_scores / result_id / spec_hash 全等
结论
5.1 BUG-577 通过。TS 只传 semantic_key + 账本年份键,再过滤 :varga. 与 >200;引擎上限 200、超长跳过计数。与任务书"跳过 128"冲突处按决策 1(上限 200)处理,执行方已写明
5.2 BUG-578 通过。失败 receipt 带 safe_error_code + engine_message(120 字);正文追加固定句;闲置/非终止退出在快照过期时先重算,同 case+指纹+turn 只一次
5.3 BUG-579 通过。userStopped && ranked>0 提前;STOP 与无焦点停止先重算,失败则按上一次成功比较交付并加旁白
5.4 BUG-580 通过。holdout 领域排除账本已有带年月事件;题干改 USER_COLLECT_QUESTION[domain] 口述采集。执行方没把决策层改成 unavailable(避免落到无卡的 offer),理由成立
5.5 P2 正文双写 部分:只做了与题干近重复句的删除;两段复述的根因(intent.classified 双触发)未拆,执行方如实记为偏离
P3 rescoreAttempts 是进程内 Map,只增不减(有测试用 reset 钩子),长期运行会缓慢增长;建议加上限或按 case 清理

部署:staging 当前 3f4d38d4,本轮无迁移,直接部署即可;部署后按 docs/testing/rectification-scenarios-20260907.md 第 0 条先复现"答完性格卡再说一件事"。