Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
14 KiB
任务书 · 采用流程整治:采集阶段不出采用卡、采用后核对题可答、采用卡沉淀进历史、运行可观测(2026-09-02)
基线:origin/staging 8dfe457f(TASK-rectification-question-ownership-fix-20260902.md 的实现,已验收)。本任务书来自真实环境 case bf7a8de6-4873-4e74-94fe-bfaa3b368e32 的两份 GET 快照与用户操作记录,覆盖两组问题:A. 采集阶段同时出现采集题和三张"采用此时间"卡;B. 点"采用此时间"之后卡片消失、弹出一道不可答的题和一个裸按钮、运行 178 秒。两组问题的根都在"卡片/题目/采用状态各自为政",一起修。
0. 真实环境复现(事实,不要重新猜)
快照一(采用前,case_revision 5):
interview.session_outcome = "collect_evidence",propose_allowed=false,can_adopt=true,selection_allowed=true;current_question = probe:family.2021(collect_spoken,active)。- 候选 05:06 / 04:53 / 05:03 支持度 14 / 13 / 13,
margin_percent 2.45,unique_top.passed=false,diagnostic_quality.passed=false。 - 界面:最新助手消息里是采集题"2021 年前后,家里……",同一条消息下面是"当前可能的出生时间"+ 三张"采用此时间"卡。
用户点了 05:06 的"采用此时间",随后:
- 三张卡消失(续跑期间
busy),列表末尾出现"用这个时间看盘"按钮。 - 续跑 turn
3fe40677:started_at 13:27:05 → completed_at 13:30:03(178 秒,上限 210 秒),tool_activities: read-case completed → compare-candidates completed → set-focus failed;正文"前面几轮的作答都记下了,候选之间的差异在一步步变清楚。我们继续——……"(没有一句承认已采用)。 - 该 turn 的
question:question_id reverse_verify:education_style:score,kind reverse_verify,四个选项,status active;题干是服务端模板原文。 - 快照二
choice_card: null,current_question.kind = "choice"。前端嵌入卡disabled,用户"这个选择题没法选择"。卡片底部仍显示"先这样,先看当前范围"。 probe:family.2021被静默标成skipped(persistNextInterviewIfIdle的isStalePreAdoptFocus分支)。
1. P0-A · collect_evidence 阶段出采用卡(BUG-313 / BUG-323 复发)
根因:core/rectification-decision.ts 的 collect() 把 capability(含 canAdopt)原样展开进 decision,publicDecisionFields 再把 canAdopt 直接投影成 can_adopt。于是 session_outcome=collect_evidence 时 can_adopt 仍是 true。前端 showSelectionCards(rectification-agentic-chat.tsx)只看 selectionAllowed && canAdopt && canRenderRectificationSelectionCards && !showLiveChoiceCard && !busy,showLiveChoiceCard 只在 A–D 卡活着时为真,采集题(collect_spoken)不算,卡片就露出来了。v9 删除 rectification-offer-candidates 门后,BUG-313/323 的防复发条款失效。
修法:
- 服务端单一真值:
publicDecisionFields里can_adopt = decision.canAdopt && ADOPT_OUTCOMES.has(decision.sessionOutcome),ADOPT_OUTCOMES = {adopt_representative, provisional_range_user_stopped, awaiting_confirmation}(validated_range/exact_minute_confirmed/completed_with_range按现有语义处理,不得新增放行)。collect_evidence/discriminate_candidates/validate_holdout一律 false。内部capability.canAdopt保留给 decision 逻辑用,不外泄。 POST /candidates/accept复用同一判断:can_adopt=false时返回409 { error: "adoption_not_allowed", session_outcome },不调 RPC。- 前端
parseRectificationCandidateResult把session_outcome解析进RectificationCandidateResult.sessionOutcome;showSelectionCards追加ADOPT_OUTCOMES.has(sessionOutcome)(双保险,前端不得单靠selection_allowed)。 collect_evidence/discriminate_candidates且credible_range非空时,只允许一行只读文案(如"目前范围 04:53–05:06,还在收窄"),放在RectificationCandidateCards同位置,无按钮、无支持度数字。- 采集题(
collect_spoken)也要有"先这样,先看当前范围"的出口:composer 旁加一个次要按钮,走现有stop动作(CHOICE_STOP_MESSAGE),服务端把当前 collect focus 标skipped、userStopped=true,进入provisional_range_user_stopped后才出卡。
2. P0-B · 采用后的核对题永远不可答
根因:采用后服务端建核对 focus 用的是 method-followup.ts input.accepted 分支(method_id "reverse_verify",ask_theme 如 education_style),stableFollowupQuestionId 落成 reverse_verify:education_style:score。之后每次 GET / set-focus 重算 followup 时,active focus 已是 reverse_verify,走的是"承接当前焦点"分支(keepAcceptedFocus,makeFollowup({ method_id: "active_focus", ask_theme: "active_focus", ... }, true, keepChoice)),choice_frame.question_id 变成 active_focus:active_focus:score。projectRectificationChoiceCard 对没有 semantic_key / candidate_split_hash 的 followup 要求 activeFocus.questionId === frame.question_id,不等 → 返回 null → GET choice_card: null → 前端 liveQuestion=false → 嵌入卡 disabled。同一 id 不一致极可能也是本轮 set-focus failed(probe_mismatch)的原因。
修法:
keepAcceptedFocus分支(以及out_of_sample_check同路径)承接时沿用已持久化 focus 的questionId、ask_theme、domain:makeFollowup前先从focus.questionId/focus.expectedAnswerSchema还原method_id/ask_theme(reverse_verify 的 question_id 形状是${method_id}:${ask_theme}:score),保证stableFollowupQuestionId(keepNext) === focus.questionId。不要靠放宽projectRectificationChoiceCard的校验来绕过——校验是防串题的,要保留。distinguish_candidates的 keep 分支不动(它有semantic_key),但补一条断言测试防止同类回归。- set-focus 在 active focus 为 reverse_verify 时:
validateSpokenPrompt的questionId比对必须能通过(同上),且题干年份要求按probe_year(核对探针有 year,如 2016)执行。 - 前端
liveQuestion对kind === "reverse_verify"的题也走同一条件;choiceCardFromQuestion的scoring: question.kind !== "reverse_verify"语义保留。
3. P1 · 采用卡沉淀进历史、采用后的界面形态
现状:三张卡永远锚在 latestSettledAssistant,busy 时隐藏;采用后的续跑把它们抹掉,跑完又出现在新消息下(05:06 标"已采用")——再次与核对题并存。"用这个时间看盘"是 savedStatus==="accepted" 时挂在列表末尾的裸按钮(rectification-consult-handoff)。
目标形态(产品拍板):
- 出卡的那条消息拥有这组卡:
RenderMessage增加candidateOffer(result_id + 三候选 + 出卡时的 session_outcome),出卡时写入该消息,刷新后由turns[].receipt/latest_result.resultId重建(服务端 GET 给每条 assistant turn 补offer_result_id,或在 turn receipt 里记candidates.offered)。卡只在拥有它的消息里渲染,不随最新消息漂移。 - 点"采用"后,这组卡原地变成结算态:05:06 "已采用",其余两张保留"改选为此时间";不再在后续消息里重复出卡。
- 采用后在会话顶部(或 composer 上方)出一条常驻状态条:"已采用 05:06 · 范围 04:53–05:06 · 改选 · 用这个时间看盘"。"用这个时间看盘"从列表末尾移进状态条;
rectification-consult-handoff删除。 - 核对题(reverse_verify / out_of_sample_check)的卡片不显示"先这样,先看当前范围",改为"这题跳过"(走现有
skipped)。 - 采用瞬间被
isStalePreAdoptFocus关掉的采集题:对应消息里的题干保留、卡片灰掉并标"已跳过(已采用 05:06)",不能像现在这样无痕消失。
4. P1 · 采用后的续跑话术与 set-focus 失败
- 续跑(
send("read_only",""))的 Agent 正文必须承认已采用并说明接下来是核对:method-followup.tsaccepted 分支的user_prompt_hint已写"当前排盘已采用……",但active_focus承接分支的 hint 没有;两处统一,并在agentic-rectification.ts指令里加一条"case.accepted_time 非空时,正文第一句要说明已按该时间采用、现在在核对"。 - §2 修好后,在
rectification-question-ownership.test.ts追加"采用后续跑:Agent 只传 spokenPrompt + 服务端 questionId → set-focusok:true、focus id 不变、选项 4 个"。 - set-focus
failed的 receipt 目前只有safeErrorCode;把reason(year_missing/probe_mismatch等)也写进tool_activities[].detail,后台可查。
5. P2 · 运行可观测与超时
178 秒的运行从回执上分不出时间花在哪(tool_activities 只有 tool/status/methods)。
tool_activities[]增加started_at/elapsed_ms;rectification-compare-candidates额外记引擎各段耗时(engine compare / vedastro-validate / persist),来源用engine-client.ts已有的调用边界,不改 Python。- 前端时间线:某一步超过 45 秒时,状态文案从"读取校正记录/正在分析"切成"引擎在比较候选,可能需要一两分钟"(按当前活跃步骤,不是固定第一步);超过
RECTIFICATION_AGENT_ATTEMPT_TIMEOUT_MS前 20 秒提示"即将超时,会自动重试或提示"。 - 采用后的续跑是
read_only:确认compare-candidates在该动作下不重复调用vedastro-validate(上一轮 receipt 已有vedastro_event_validation且evidenceLedgerFingerprint未变时直接沿用);若现有代码已如此,写一条测试固定它;若没有,加这条快路径。不得改确认门语义(沿用的结果仍是 failed 就仍 failed)。
6. 测试
rectification-decide-next-action.test.ts/rectification-confirmation-gate.test.ts:collect_evidence+capability.canAdopt=true→can_adopt=false、selection_allowed不变;provisional_range_user_stopped→ true;adopt_representative→ true。- accept route 测试:
session_outcome=collect_evidence时 POST 返回 409 且 RPC 未调用。 rectification-candidate-result.test.ts:快照一形状 →showSelectionCards为 false,只读范围行可渲染;快照二形状(accepted)→ 不再出新卡。rectification-server-focus.test.ts/rectification-choice-card.test.ts:accepted + active reverse_verify focus →projectRectificationChoiceCard非空、focus_id一致、question_id === focus.questionId;连续两次 GET 结果一致。rectification-question-in-message.test.ts:采用后消息里的核对题liveQuestion=true(结构断言:choice_card.focus_id === turns[last].question.focus_id)。- 组件/渲染测试:采用后卡片留在原消息、05:06 "已采用"、新消息无卡;状态条含"用这个时间看盘";
rectification-consult-handoff不再渲染。 rectification-activity-receipt.test.ts:elapsed_ms序列化与时间线行映射。- 用 §0 的两份快照做 fixture(脱敏后放
frontend/tests/fixtures/rectification-case-bf7a8de6-*.json)。
7. 硬红线
- 确认门恒 fail-closed;
can_adopt只能收紧不能放宽;不得用正文字符串判断状态。 - 选项、answer_class、计分仍全部服务端所有;Agent 只写
spokenPrompt。 - 不恢复问题槽、不改
asked_turn_id契约;不改d9404976/8dfe457f已定的消息形态。 - Python 引擎不动;Skill 10.0.14 不 bump。
./node_modules/.bin/tsc --noEmit通过(不用npx tsc);npm run lint --prefix frontend0 错误;rectification-*/consultation-*/consult-*/chat-*测试 fail=0;改动的既有断言逐条三栏说明(旧→新→保留语义)。- 无凭据不得声称已真实环境验证。
8. 开工前置
git fetch origin --prune
git worktree add -b codex/rectification-adopt-flow-20260902 \
../.worktrees/rectification-adopt-flow-20260902 origin/staging
读 TASK-rectification-question-in-message-20260902.md、TASK-rectification-question-ownership-fix-20260902.md、docs/BUG_HISTORY.md(BUG-313、BUG-323 及 BUG-492 起的链)、frontend/docs/VOICE.md。BUG 编号从 BUG-495 起(先 grep 确认最大号):495 = 采集阶段出采用卡(复发自 313/323),496 = 采用后核对题不可答,497 = 采用卡不沉淀/裸按钮,498 = 运行耗时不可观测。行号是线索,按符号名定位。
9. 验收标准
- 快照一形状:GET
can_adopt=false,页面只有采集题 + 只读范围行,无"采用此时间";composer 旁有"先这样"入口,点后session_outcome=provisional_range_user_stopped、三张卡出现。 - 快照二形状:GET
choice_card非空且focus_id与最新题一致;A–D 可点,点后answer_option落库、候选比较更新。 - 采用后:卡片留在原消息且 05:06 标"已采用";新消息无卡;状态条含"改选"和"用这个时间看盘";列表末尾无裸按钮;被关掉的采集题标"已跳过"。
- 采用后续跑:正文第一句承认已采用;set-focus
completed;核对题卡片无"先这样",有"这题跳过"。 - 回执含每步
elapsed_ms;45 秒后文案切换。 - tsc + lint + 四组测试 fail=0;BUG_HISTORY 追加 495–498。
- 真实环境人工清单(部署后):走一遍"两件事 → 三道区分题 → 采集题阶段无卡 → 先这样 → 出卡 → 采用 → 核对题可答 → 看盘"。