Files
Jyotisha/TASK-rectification-adopt-flow-20260902.md
T

14 KiB
Raw Blame History

任务书 · 采用流程整治:采集阶段不出采用卡、采用后核对题可答、采用卡沉淀进历史、运行可观测(2026-09-02)

基线:origin/staging 8dfe457fTASK-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=falsecan_adopt=trueselection_allowed=truecurrent_question = probe:family.2021collect_spokenactive)。
  • 候选 05:06 / 04:53 / 05:03 支持度 14 / 13 / 13margin_percent 2.45unique_top.passed=falsediagnostic_quality.passed=false
  • 界面:最新助手消息里是采集题"2021 年前后,家里……"同一条消息下面是"当前可能的出生时间"+ 三张"采用此时间"卡。

用户点了 05:06 的"采用此时间",随后:

  • 三张卡消失(续跑期间 busy),列表末尾出现"用这个时间看盘"按钮。
  • 续跑 turn 3fe40677started_at 13:27:05 → completed_at 13:30:03178 秒,上限 210 秒),tool_activities: read-case completed → compare-candidates completed → set-focus failed;正文"前面几轮的作答都记下了,候选之间的差异在一步步变清楚。我们继续——……"(没有一句承认已采用)。
  • 该 turn 的 questionquestion_id reverse_verify:education_style:scorekind reverse_verify,四个选项,status active;题干是服务端模板原文。
  • 快照二 choice_card: nullcurrent_question.kind = "choice"。前端嵌入卡 disabled,用户"这个选择题没法选择"。卡片底部仍显示"先这样,先看当前范围"。
  • probe:family.2021 被静默标成 skippedpersistNextInterviewIfIdleisStalePreAdoptFocus 分支)。

1. P0-A · collect_evidence 阶段出采用卡(BUG-313 / BUG-323 复发)

根因core/rectification-decision.tscollect()capability(含 canAdopt)原样展开进 decisionpublicDecisionFields 再把 canAdopt 直接投影成 can_adopt。于是 session_outcome=collect_evidencecan_adopt 仍是 true。前端 showSelectionCardsrectification-agentic-chat.tsx)只看 selectionAllowed && canAdopt && canRenderRectificationSelectionCards && !showLiveChoiceCard && !busyshowLiveChoiceCard 只在 AD 卡活着时为真,采集题(collect_spoken)不算,卡片就露出来了。v9 删除 rectification-offer-candidates 门后,BUG-313/323 的防复发条款失效。

修法

  1. 服务端单一真值:publicDecisionFieldscan_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 逻辑用,不外泄。
  2. POST /candidates/accept 复用同一判断:can_adopt=false 时返回 409 { error: "adoption_not_allowed", session_outcome },不调 RPC。
  3. 前端 parseRectificationCandidateResultsession_outcome 解析进 RectificationCandidateResult.sessionOutcomeshowSelectionCards 追加 ADOPT_OUTCOMES.has(sessionOutcome)(双保险,前端不得单靠 selection_allowed)。
  4. collect_evidence / discriminate_candidatescredible_range 非空时,只允许一行只读文案(如"目前范围 04:5305:06,还在收窄"),放在 RectificationCandidateCards 同位置,无按钮、无支持度数字。
  5. 采集题(collect_spoken)也要有"先这样,先看当前范围"的出口:composer 旁加一个次要按钮,走现有 stop 动作(CHOICE_STOP_MESSAGE),服务端把当前 collect focus 标 skippeduserStopped=true,进入 provisional_range_user_stopped 后才出卡。

2. P0-B · 采用后的核对题永远不可答

根因:采用后服务端建核对 focus 用的是 method-followup.ts input.accepted 分支(method_id "reverse_verify"ask_themeeducation_style),stableFollowupQuestionId 落成 reverse_verify:education_style:score。之后每次 GET / set-focus 重算 followup 时,active focus 已是 reverse_verify,走的是"承接当前焦点"分支(keepAcceptedFocusmakeFollowup({ method_id: "active_focus", ask_theme: "active_focus", ... }, true, keepChoice)),choice_frame.question_id 变成 active_focus:active_focus:scoreprojectRectificationChoiceCard 对没有 semantic_key / candidate_split_hash 的 followup 要求 activeFocus.questionId === frame.question_id,不等 → 返回 null → GET choice_card: null → 前端 liveQuestion=false → 嵌入卡 disabled。同一 id 不一致极可能也是本轮 set-focus failedprobe_mismatch)的原因。

修法

  1. keepAcceptedFocus 分支(以及 out_of_sample_check 同路径)承接时沿用已持久化 focus 的 questionIdask_themedomainmakeFollowup 前先从 focus.questionId / focus.expectedAnswerSchema 还原 method_id/ask_themereverse_verify 的 question_id 形状是 ${method_id}:${ask_theme}:score),保证 stableFollowupQuestionId(keepNext) === focus.questionId。不要靠放宽 projectRectificationChoiceCard 的校验来绕过——校验是防串题的,要保留。
  2. distinguish_candidates 的 keep 分支不动(它有 semantic_key),但补一条断言测试防止同类回归。
  3. set-focus 在 active focus 为 reverse_verify 时:validateSpokenPromptquestionId 比对必须能通过(同上),且题干年份要求按 probe_year(核对探针有 year,如 2016)执行。
  4. 前端 liveQuestionkind === "reverse_verify" 的题也走同一条件;choiceCardFromQuestionscoring: question.kind !== "reverse_verify" 语义保留。

3. P1 · 采用卡沉淀进历史、采用后的界面形态

现状:三张卡永远锚在 latestSettledAssistantbusy 时隐藏;采用后的续跑把它们抹掉,跑完又出现在新消息下(05:06 标"已采用")——再次与核对题并存。"用这个时间看盘"是 savedStatus==="accepted" 时挂在列表末尾的裸按钮(rectification-consult-handoff)。

目标形态(产品拍板):

  1. 出卡的那条消息拥有这组卡:RenderMessage 增加 candidateOfferresult_id + 三候选 + 出卡时的 session_outcome),出卡时写入该消息,刷新后由 turns[].receipt / latest_result.resultId 重建(服务端 GET 给每条 assistant turn 补 offer_result_id,或在 turn receipt 里记 candidates.offered)。卡只在拥有它的消息里渲染,不随最新消息漂移。
  2. 点"采用"后,这组卡原地变成结算态:05:06 "已采用",其余两张保留"改选为此时间";不再在后续消息里重复出卡。
  3. 采用后在会话顶部(或 composer 上方)出一条常驻状态条:"已采用 05:06 · 范围 04:53–05:06 · 改选 · 用这个时间看盘"。"用这个时间看盘"从列表末尾移进状态条;rectification-consult-handoff 删除。
  4. 核对题(reverse_verify / out_of_sample_check)的卡片不显示"先这样,先看当前范围",改为"这题跳过"(走现有 skipped)。
  5. 采用瞬间被 isStalePreAdoptFocus 关掉的采集题:对应消息里的题干保留、卡片灰掉并标"已跳过(已采用 05:06)",不能像现在这样无痕消失。

4. P1 · 采用后的续跑话术与 set-focus 失败

  1. 续跑(send("read_only",""))的 Agent 正文必须承认已采用并说明接下来是核对:method-followup.ts accepted 分支的 user_prompt_hint 已写"当前排盘已采用……",但 active_focus 承接分支的 hint 没有;两处统一,并在 agentic-rectification.ts 指令里加一条"case.accepted_time 非空时,正文第一句要说明已按该时间采用、现在在核对"。
  2. §2 修好后,在 rectification-question-ownership.test.ts 追加"采用后续跑:Agent 只传 spokenPrompt + 服务端 questionId → set-focus ok:true、focus id 不变、选项 4 个"。
  3. set-focus failed 的 receipt 目前只有 safeErrorCode;把 reasonyear_missing / probe_mismatch 等)也写进 tool_activities[].detail,后台可查。

5. P2 · 运行可观测与超时

178 秒的运行从回执上分不出时间花在哪(tool_activities 只有 tool/status/methods)。

  1. tool_activities[] 增加 started_at / elapsed_msrectification-compare-candidates 额外记引擎各段耗时(engine compare / vedastro-validate / persist),来源用 engine-client.ts 已有的调用边界,不改 Python。
  2. 前端时间线:某一步超过 45 秒时,状态文案从"读取校正记录/正在分析"切成"引擎在比较候选,可能需要一两分钟"(按当前活跃步骤,不是固定第一步);超过 RECTIFICATION_AGENT_ATTEMPT_TIMEOUT_MS 前 20 秒提示"即将超时,会自动重试或提示"。
  3. 采用后的续跑是 read_only:确认 compare-candidates 在该动作下不重复调用 vedastro-validate(上一轮 receipt 已有 vedastro_event_validationevidenceLedgerFingerprint 未变时直接沿用);若现有代码已如此,写一条测试固定它;若没有,加这条快路径。不得改确认门语义(沿用的结果仍是 failed 就仍 failed)。

6. 测试

  • rectification-decide-next-action.test.ts / rectification-confirmation-gate.test.tscollect_evidence + capability.canAdopt=truecan_adopt=falseselection_allowed 不变;provisional_range_user_stopped → trueadopt_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.tsaccepted + 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.tselapsed_ms 序列化与时间线行映射。
  • 用 §0 的两份快照做 fixture(脱敏后放 frontend/tests/fixtures/rectification-case-bf7a8de6-*.json)。

7. 硬红线

  1. 确认门恒 fail-closedcan_adopt 只能收紧不能放宽;不得用正文字符串判断状态。
  2. 选项、answer_class、计分仍全部服务端所有;Agent 只写 spokenPrompt
  3. 不恢复问题槽、不改 asked_turn_id 契约;不改 d9404976 / 8dfe457f 已定的消息形态。
  4. Python 引擎不动;Skill 10.0.14 不 bump。
  5. ./node_modules/.bin/tsc --noEmit 通过(不用 npx tsc);npm run lint --prefix frontend 0 错误;rectification-* / consultation-* / consult-* / chat-* 测试 fail=0;改动的既有断言逐条三栏说明(旧→新→保留语义)。
  6. 无凭据不得声称已真实环境验证。

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.mdTASK-rectification-question-ownership-fix-20260902.mddocs/BUG_HISTORY.mdBUG-313、BUG-323 及 BUG-492 起的链)、frontend/docs/VOICE.md。BUG 编号从 BUG-495 起(先 grep 确认最大号):495 = 采集阶段出采用卡(复发自 313/323),496 = 采用后核对题不可答,497 = 采用卡不沉淀/裸按钮,498 = 运行耗时不可观测。行号是线索,按符号名定位。

9. 验收标准

  1. 快照一形状:GET can_adopt=false,页面只有采集题 + 只读范围行,无"采用此时间"composer 旁有"先这样"入口,点后 session_outcome=provisional_range_user_stopped、三张卡出现。
  2. 快照二形状:GET choice_card 非空且 focus_id 与最新题一致;AD 可点,点后 answer_option 落库、候选比较更新。
  3. 采用后:卡片留在原消息且 05:06 标"已采用";新消息无卡;状态条含"改选"和"用这个时间看盘";列表末尾无裸按钮;被关掉的采集题标"已跳过"。
  4. 采用后续跑:正文第一句承认已采用;set-focus completed;核对题卡片无"先这样",有"这题跳过"。
  5. 回执含每步 elapsed_ms45 秒后文案切换。
  6. tsc + lint + 四组测试 fail=0BUG_HISTORY 追加 495498。
  7. 真实环境人工清单(部署后):走一遍"两件事 → 三道区分题 → 采集题阶段无卡 → 先这样 → 出卡 → 采用 → 核对题可答 → 看盘"。