63eba4208e
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
13 KiB
13 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)"
验收(Claude,2026-09-07,origin/staging @ 84a29754,实现 4e0db55f;落地编号 BUG-577~580)
| 门 | 结果 |
|---|---|
| tsc / lint | 0 错 / 0 error(84 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=1,score_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 条先复现"答完性格卡再说一件事"。