Files
Jyotisha/docs/tasks/TASK-rectification-targeted-collect-persist-20260911.md
T

12 KiB
Raw Blame History

TASK · 定向补事题落不了库、探针刷新是空转:六题答完又停在「没有拿到下一个问题」(2026-09-11)

  • 基线:origin/staging @ 6c9a0896(已部署;含 BUG-653/654Skill 10.0.24
  • 分支:codex/rectification-targeted-collect-persist-20260911,基于 origin/staging
  • 执行方:coding agent;验收:Claude
  • BUG 编号起点:BUG-655(开工时 grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1 核对,应为 654
  • 不改确认门、不改 SCORE_DELTA、不改 MIN_ACCEPTANCE_*;不升 Skill

1. 事故实证(2026-09-11 staging 真机,代码 6c9a0896

同一序列(两件学业 + 实习入职/离职 + 六道带年月选择题 C/C/C/B/A/C)。第六题答完,助手只回「已记录,范围收到 04:48–05:07。」,没有新题、没有定向补事句、没有卡;界面「没有拿到下一个问题。」。BUG-653/654 想做的「先刷新探针 → 再定向补事 → 再出卡」三步一步都没有在用户面前出现,而且比 BUG-651 之前更糟:连卡也不出了。

1b. 同一 Case 的 GET 快照(产品负责人 2026-09-11 晚补充,折叠视图)

字段 含义
interview.type / session_outcome ask_candidate_discriminator / discriminate_candidates GET 的决策层认为还有一道可问的区分题
next_user_action.id ask_candidate_discriminator 同上
current_question / choice_card null / null 库里没有活动焦点:这道题从未落下
latest_result.candidates [] 公开候选投影为空。答第六题前同一 Case 的快照里候选是 5 个,说明第六题之后写入的那一行推理状态(BUG-653 的 persistRefreshedInferencereason=supersederawAnswer=refresh_probes)让 authoritativeCandidateProjection 判为不一致或取不到候选
step_state 第 1 步·收集经历 与决策层「出区分题」自相矛盾

这说明 §2.3 之外还有第二条死路:刷新之后决策层拿到了一道「可问」的题(最可能是 eventProbeFromInferenceinference_state.probes 里已答过或同域同年的题重新并入 eventProbes,或刷新并入的探针 id 与已答 probe_id 不一致而被当成未答),持久化层按 probe_already_answered / 同年去重拒绝落焦点,于是「决策有题、库里无题」——与 BUG-651 前的漏洞同型。T0 必须同时打印 GET 侧 decision.probe.semanticKeypersistServerOwnedFocus 的拒绝状态,并核对刷新写入的 inference_state.candidatescandidate_set_id

2. 根因(按 6c9a0896 源码定位,三段同时成立)

  1. 定向补事焦点写不进库(BUG-655method-followup.ts followupFromPoolItem 给定向题 kind_hint = "targeted:<domain>"(锚定题 anchor:<kind>:<year>、通用题 generic:<domain> 同理)。答题事务里的服务端落焦点 server-focus.ts persistCollectFocus 把它原样写成 p_target_kindagentic_rectification_conversation_focuses.target_kind 的 CHECKfrontend/supabase/migrations/20260814020000_rectification_v10_runtime.sql L124–131)只允许证据种类枚举,插入抛错 → 不是幂等冲突 → 返回 skipped → 重试一次仍 skipped。对照:Agent 工具路径 rectification-v9-tools.ts rectification-set-focusevidenceKindSchema 校验并对 collect 焦点写 targetKind: null,所以开场邀请题(collect:invite:more)能落,而答题事务之后的定向题永远落不了。dd8f35f7 的锚定 / 通用题在真机同样落不了,只是之前训练门一开就跳过了它们,没暴露。
  2. 探针刷新在引擎侧是空转(BUG-656)refresh-discriminator-probes.ts 把剩余 5 个候选放在 columnTimes 传给引擎;api_service.py 只把 refresh_probes 加进白名单;refinement_packet.py L707 给探针生成器的 candidate_timesgrid_times = built.candidate_times(整个 31 分钟网格),column_times 只用于三列对照。于是 _discriminating_event_probe_listsremaining = _remaining_contexts(built, 全网格) = 全部,簇还是最初 9 簇,生成的仍是那 7 道已答题;mergeConflictProbes 并入 0 道。refresh_count 记成 1,刷新被判「已用尽」。
  3. 决策层把「定向补事池非空」当成「还没问完」,形成无载体死循环(BUG-657)decision-from-dossier.ts narrowingExhaustiontargetedCollectPool(...).length === 0 || isTargetedCollectDeclined(...)targetedCollectExhausted。定向题因 1 落不了库,既不会被答也不会被拒 → 池永远非空 → stillNeedNarrowing 恒真 → decideRectificationcollect(..., waitToNarrowCapability)nextAction=ask_fact_collectioncanAdopt=falsecanOfferRange=falsestopReason=null。GET 无活动焦点、无卡;客户端 interviewCollectWaiting 要求 stopReason ∈ insufficient_*,不满足 → questionGap=unavailable → 「没有拿到下一个问题」;「接着问」走同一条路,同样失败。BUG-652 的载体不变量只覆盖了 persistNextInterviewAfterChoice 末尾的空正文,没覆盖「决策要求等待、载体却写不进去」这一种。
  4. 待核实(T0persistExhaustionCollect 在焦点 skipped 时会把定向题干作为 hostNarration 返回,persistApplied 应把它拼在「已记录…」之后写进确定性 turn;真机正文里没有这句。怀疑 persistV9DeterministicTurn(requestId = actionId) 与选择动作共用 request id 被幂等吞掉,或 spoken 为空。用 staging 容器日志核实:[rectification-v9] persist next focus failedrefresh discriminator probes failedpersist refreshed probes failed 三条 warn 的 reason

3. 决策记录(产品负责人 2026-09-11)

  1. 定向补事 / 锚定 / 通用三类采集题必须能在答题事务里落库:target_kind 只写枚举内的证据种类(按领域映射:family→family_event、finance→finance_gain、relocation→relocation、relationship→relationship_change、career→career_change、education→education_milestone、health→self_health_event),采集种类提示(targeted: / anchor: / generic:)放进 expected_answer_schema.collect_kind,不改表结构、不加迁移。
  2. 探针刷新必须真的按剩余候选生成:refresh_probes=true 时引擎探针生成器的 candidate_timescolumn_times;刷新后若公开探针与已答集合无差集,回执写 refresh_result="no_new_probes"
  3. 「等待收窄」的决策不得没有载体:targetedCollectExhausted 必须由已问过focus 存在且 resolved/declined/skipped)来判定,不是由池非空判定;定向题落库失败时,同一事务内把 collect:targeted:* 记为 skipped 话题并直接交付卡(BUG-651 口径),不得停在 ask_fact_collection 且无焦点。
  4. 客户端:stopReason=nullsessionOutcome=collect_evidence 且无焦点无卡时,不再显示「没有拿到下一个问题」,显示缺口句并保持输入框可用(与 collect_waiting 同形)。

4. 硬红线

  • 不加数据库迁移;不放宽 target_kind CHECK。
  • 刷新不改 candidate_set_id / 已答题(BUG-587 / BUG-594);同域同年去重照旧(BUG-559)。
  • 任何 nextAction 在事务结束时必须满足:有活动焦点,或 can_offer_range=true 并带 range_delivery,或正文含精确缺口句(BUG-646 / 652 / 657 合并不变量,写成一条合同测试)。
  • 既有测试总数不降;改断言写原值 / 新值 / 原因;记录只用脱敏领域 + 年月。

5. 任务分解

  • T0 取证staging 容器日志抓 §2.4 三条 warn 的 reason;对 §1b 那个 Case 直接查库:最后一行 agentic_rectification_inference_transitionsreason / raw_answer / inference_state.candidates 数量 / candidate_set_id / refresh_count)与 latest_result 的候选投影为何为空;用 §1 序列写回放测试,断言第六题答完后 persistServerOwnedFocus 的 status、refresh 返回的新探针数、决策 nextAction / canOfferRange / stopReason,把三者写进进度记录第一段。
  • T1 采集焦点 kind 映射(BUG-655server-focus.ts persistCollectFocuspersistSpokenChoiceFallbacktargetKind 前经 collectFocusTargetKind(followup)(枚举内或 null),kind_hint 原值写入 schema.collect_kindanswer-choice.ts / agent-run.tscollect_kind 判定 invite / anchor / generic / targeted。验收:定向、锚定、通用三类 followup 在 rectification-server-focus.test.ts 里用真实 CHECK 列表断言 targetKind 合法且 status=creatednpm run test:db 若有 Docker 则跑焦点插入。
  • T2 引擎按剩余候选刷新(BUG-656)api_service.py / refinement_packet.pyrefresh_probes 为真时把 column_times 作为探针生成器的 candidate_timesevent_probes.pyrefresh and remaining 分支才真正命中。refresh-discriminator-probes.ts 若引擎返回的公开探针键全在已答集合内,refresh_result="no_new_probes" 并记 refresh_count。验收:tests/test_candidate_discriminator_contract.py 加「31 格 + column_times=5 个」夹具,断言簇按 5 个候选重算、至少出现一道家人或财务月级边界题(若确无边界则断言 no_new_probes);TS 合同测试断言刷新后 probes 增量且 candidate_set_id 不变。
  • T3 等待收窄必须有载体(BUG-657)persistRefreshedInference 只在引擎真的返回新探针时写库,写入前断言 candidates 非空且 candidate_set_id 不变,否则不写;刷新并入的探针 id 必须与 answered_probes[].probe_id 同一命名规则(带 split hash 后缀),并入前按 semantic_key 与同域同年去重;narrowingExhaustiontargetedCollectExhausted 改为「collect:targeted:* 焦点已存在且非 active」或池空;persistNextInterviewAfterChoice / persistNextInterviewIfIdle 在定向题落库 skipped 时写 collect:targeted:<domain>skipped 话题并按 BUG-651 出卡;decideRectification 加断言:collectwaitToNarrowCapability 只允许在 targetedCollectExhausted=false本事务已成功落下焦点时返回。客户端 rectificationQuestionGapStatesessionOutcome=collect_evidence && !stopReason && 无焦点无卡collect_waiting。验收:§1 序列第六题答完 → 出定向题「还有 N 条线能把 04:48 和 05:07 分开:…」;答一件家人事 → 重算回 S2;答「没有了」→ 出「目前范围」卡。模拟落库失败 → 同一事务出卡,不出现「没有拿到下一个问题」。
  • T4 记录BUG-655657 入历史(655 关联 BUG-648、BUG-442「覆盖判定不得依赖模型 domain」;656 关联 BUG-653657 关联 BUG-651/652/646);CHANGELOGPROGRESSdocs/testing/rectification-scenarios-20260907.md 场景 0b 补「定向题必须出现」一条。

6. 让步顺序

T2 若引擎侧改动大,可先只做 T1 + T3(定向补事能问、问完必出卡),刷新留下一单;T1 + T3 不可省。

7. 开工前置命令

git fetch origin --prune
git worktree add -b codex/rectification-targeted-collect-persist-20260911 .worktrees/rectification-targeted-collect-persist-20260911 origin/staging
grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1   # 应为 654
grep -n "target_kind" frontend/supabase/migrations/20260814020000_rectification_v10_runtime.sql | head -3
cd frontend && npm test -- tests/rectification-server-focus.test.ts tests/rectification-probe-pool-exhausted-20260911.test.ts tests/rectification-replay-20260911.test.ts tests/rectification-collection-question-pool.test.ts
cd .. && .venv/bin/python -m pytest tests/test_candidate_discriminator_contract.py tests/test_rectification_event_probes.py -q

8. 验收口径

  • 前端 tsc --noEmit 0 错、npm run lint 0 error、相关套件 fail=0、测试总数 ≥ 基线;next build/ 仍 Static、首屏 gzip ±2%;有 Docker 则 npm run test:db
  • Python:上述两个测试文件 + run_quality_gate.py --profile quick
  • 部署后真机:§1 序列第六题答完必须看到定向补事题;说「没有了」后看到「目前范围」卡;全程不出现「没有拿到下一个问题」。