Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
11 KiB
11 KiB
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.ts、engine-client.ts、frontend/src/mastra/rectification-v9-tools.ts、frontend/src/lib/rectification-agentic/core/rectification-decision.ts、v9/answer-choice.ts、v9/method-followup.ts(holdoutFollowupFor)、v9/agent-run.ts、scripts/rectification/contracts.py、frontend/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. 事故实证(staging,2026-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_request 对 asked_probe_keys 每条限 120 字符,超限直接 ValueError → BadRequest → 工具抛错 |
scripts/rectification/contracts.py L339 |
| 4 | TS 把 probe_id、semantic_key、candidate_split_hash 三种键都塞进 asked_probe_keys |
inference-adapter.ts::askedProbeKeysFromReceipt L53–55(BUG-559 引入) |
| 5 | 于是:任何 varga_style / nakshatra 卡答过之后,后续每一次比较都失败;账本已有 7 件事,latest_result 只算了 5 件,evidenceLedgerFingerprint 与当前账本不等 → snapshotCurrent=false |
decision-from-dossier.ts::scoreableSnapshotCurrentFromDossier |
| 6 | decideRectification 的 snapshotCurrent === false 分支排在 userStopped 之前:用户点了停止、case.status=paused,决策仍是 collect(type: ask_fact_collection、session_outcome: collect_evidence、can_adopt: false) → 卡片条件 ADOPT_OUTCOMES 不满足 → 无卡;current_question: null → 客户端显示"没有拿到下一个问题" |
rectification-decision.ts L285–290;rectification-surface-state.ts L31 |
| 7 | 工具失败被 Agent 吞掉:助手正文仍写"这条记下了…对缩小候选范围很有帮助",用户不知道比较根本没跑 | rectification-v9-tools.ts L2073 只记 receipt 后 throw,agent-run 继续作答 |
| 8 | "也再说一件你能记得大致时间的事吧"来自 oos_blind:holdout 焦点(kind: reverse_verify):holdoutFollowupFor 从过期 oos_blind_prompts 里取了"财务这条线还没用过"(账本其实已有财务事件),模型把它改写成泛泛的"再说一件事" |
method-followup.ts L1684–1694;next_user_action.user_meaning |
| 9 | 采集轮正文重复:第 1、2 轮助手文字各出现两段近似复述;工作采集题在正文出现一次、问题块又出现一次 | turns[2].text、turns[4].text |
2. 根因
- BUG-575(P0):
asked_probe_keys把candidate_split_hash也传给引擎,varga/nakshatra 的 hash 超过引擎 120 字符上限,比较请求被 400 拒绝。BUG-559 只在test_rectification_event_probes.py用短键测过,没有用真实 split hash 测过整条 compare。 - BUG-576(P1):比较失败没有任何用户可见后果:receipt 只有
tool_failed,没有错误码;Agent 继续写"记下了、很有帮助";下一轮也不重试。 - BUG-577(P1):
decideRectification让"快照过期"压过"用户已停止",停止后交付不出来;persistNextInterviewIfIdle的停止路径也不先重算。 - BUG-578(P1):
holdoutFollowupFor只排除拒答领域,不排除账本里已有事件的领域,而且用的是可能过期的oos_blind_prompts;焦点是reverse_verify,题干交给模型自由改写,成了"再说一件事"。 - P2:采集轮正文双写;题干进正文。
3. 决策记录
asked_probe_keys只传semantic_key(去重、≤120),不传probe_id与candidate_split_hash;引擎侧上限提到 200 并对超长键跳过而不是拒绝(记dropped_asked_probe_keys计数进 receipt)。TS 侧再加一层保险:超过 200 的键在engineRequestBody里过滤并console.warn。- 比较失败必须可见:receipt
detail带safe_error_code与引擎返回的错误文本前 120 字;agent-run 在工具失败后给正文追加服务端固定句"候选比较这次没跑成,下一句话时会自动再试";下一轮persistNextInterviewIfIdle见snapshotCurrent=false且账本有可评分事件时先重算一次(复用rescoreMinuteAfterWindowChange),再决策。 - 用户停止优先于快照过期:
decideRectification把userStopped && ranked.length>0提到snapshotCurrent判断之前;停止路径(route 的stop_and_review与applyRectificationChoiceSTOP)在决策前先尝试重算,重算失败则按上一次有效结果交付,旁白加一句"这是按上一次成功比较给出的范围"。 holdoutFollowupFor:候选领域 =oos_blind_prompts中 既未拒答、账本里也没有该领域带年月事件 的领域;没有则 holdout 记为unavailable,直接进交付;题干用服务端固定句(USER_COLLECT_QUESTION[domain]同源,不给模型改写),焦点 kind 改为collect_spoken而不是reverse_verify。- 正文双写:
composeCollectSpokenAssistantText与模型正文若都包含采集题,只保留问题块;模型正文里与spokenPrompt相同或高度相似(前 12 字相同)的句子删除;两段复述的根因要查(phases里intent.classified出现两次),进度记录写明。
4. 硬红线
- 不动采用门、确认门、
MIN_SEPARATION_LEAD、_relative_support。 minute_step=1路径字节不变(指纹用例)。- BUG-559 的去重用例照常通过(去重靠
semantic_key,不靠 hash)。 - 测试总数 ≥ 1899;tsc 0 错;lint 0 error。
- 任务书 / 进度 / Bug 历史不得写入产品负责人 Case JSON 里的年份、经历、Case ID。
5. 任务分解
5.1 BUG-575
askedDiscriminatorKeys拆成两份:askedSemanticKeys(给引擎)与现有全量(给 TS 去重);engineRequestBody只用前者并过滤 >200;contracts.py上限 200、超长跳过、计数进decision_receipt.dropped_asked_probe_keys。- 验收:
tests/test_rectification_input_contract.py——128 字符键不再报错、被跳过并计数;TSrectification-v9-engine-contract.test.ts——用真实 varga split hash(≥128 字符)构造 receipt,engineRequestBody.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 工具失败 → 正文追加固定句;persistNextInterviewIfIdle与inspectNonTerminalTurnExit在snapshotCurrent=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>0→completeWithRange(user_stopped),session_outcome ∈ ADOPT_OUTCOMES;rectification-window-widen或 answer-choice 测试——STOP 在过期快照上先重算再交付。
5.4 BUG-578
holdoutFollowupFor过滤已有事件领域;无可问领域 →holdoutValidation="unavailable";焦点改collect_spoken+ 服务端题干。- 验收:
rectification-choice-card或collect-direction测试——账本已有财务事件且家人/健康拒答 → 不出 holdout 题,决策直接交付;有可问领域时题干等于USER_COLLECT_QUESTION[domain],不是"再说一件事"。
5.5 P2 正文双写
- 定位
intent.classified双触发与正文拼接;去重规则见决策 5。 - 验收:
rectification-spoken-collect.test.ts——采集轮最终正文不含与spokenPrompt重复的句子;同一段复述不出现两次。
5.6 记录
docs/BUG_HISTORY.mdBUG-575~578(575 标"复发自 BUG-559");CHANGELOG.md;PROGRESS-rectification-stale-compare-fix-20260907.md;docs/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)"