Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
74 lines
12 KiB
Markdown
74 lines
12 KiB
Markdown
# TASK · 定向补事题落不了库、探针刷新是空转:六题答完又停在「没有拿到下一个问题」(2026-09-11)
|
||
|
||
- 基线:`origin/staging` @ `6c9a0896`(已部署;含 BUG-653/654,Skill 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 的 `persistRefreshedInference`,`reason=supersede`、`rawAnswer=refresh_probes`)让 `authoritativeCandidateProjection` 判为不一致或取不到候选 |
|
||
| `step_state` | 第 1 步·收集经历 | 与决策层「出区分题」自相矛盾 |
|
||
|
||
这说明 §2.3 之外还有第二条死路:刷新之后决策层拿到了一道「可问」的题(最可能是 `eventProbeFromInference` 把 `inference_state.probes` 里已答过或同域同年的题重新并入 `eventProbes`,或刷新并入的探针 `id` 与已答 `probe_id` 不一致而被当成未答),持久化层按 `probe_already_answered` / 同年去重拒绝落焦点,于是「决策有题、库里无题」——与 BUG-651 前的漏洞同型。T0 必须同时打印 GET 侧 `decision.probe.semanticKey` 与 `persistServerOwnedFocus` 的拒绝状态,并核对刷新写入的 `inference_state.candidates` 与 `candidate_set_id`。
|
||
|
||
## 2. 根因(按 6c9a0896 源码定位,三段同时成立)
|
||
|
||
1. **定向补事焦点写不进库(BUG-655)**。`method-followup.ts` `followupFromPoolItem` 给定向题 `kind_hint = "targeted:<domain>"`(锚定题 `anchor:<kind>:<year>`、通用题 `generic:<domain>` 同理)。答题事务里的服务端落焦点 `server-focus.ts` `persistCollectFocus` 把它原样写成 `p_target_kind`;`agentic_rectification_conversation_focuses.target_kind` 的 CHECK(`frontend/supabase/migrations/20260814020000_rectification_v10_runtime.sql` L124–131)只允许证据种类枚举,插入抛错 → 不是幂等冲突 → 返回 `skipped` → 重试一次仍 `skipped`。对照:Agent 工具路径 `rectification-v9-tools.ts` `rectification-set-focus` 用 `evidenceKindSchema` 校验并对 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_times` 是 `grid_times = built.candidate_times`(整个 31 分钟网格),`column_times` 只用于三列对照。于是 `_discriminating_event_probe_lists` 里 `remaining = _remaining_contexts(built, 全网格)` = 全部,簇还是最初 9 簇,生成的仍是那 7 道已答题;`mergeConflictProbes` 并入 0 道。`refresh_count` 记成 1,刷新被判「已用尽」。
|
||
3. **决策层把「定向补事池非空」当成「还没问完」,形成无载体死循环(BUG-657)**。`decision-from-dossier.ts` `narrowingExhaustion` 用 `targetedCollectPool(...).length === 0 || isTargetedCollectDeclined(...)` 判 `targetedCollectExhausted`。定向题因 1 落不了库,既不会被答也不会被拒 → 池永远非空 → `stillNeedNarrowing` 恒真 → `decideRectification` 走 `collect(..., waitToNarrowCapability)`:`nextAction=ask_fact_collection`、`canAdopt=false`、`canOfferRange=false`、`stopReason=null`。GET 无活动焦点、无卡;客户端 `interviewCollectWaiting` 要求 `stopReason ∈ insufficient_*`,不满足 → `questionGap=unavailable` → 「没有拿到下一个问题」;「接着问」走同一条路,同样失败。BUG-652 的载体不变量只覆盖了 `persistNextInterviewAfterChoice` 末尾的空正文,没覆盖「决策要求等待、载体却写不进去」这一种。
|
||
4. **待核实(T0)**:`persistExhaustionCollect` 在焦点 `skipped` 时会把定向题干作为 `hostNarration` 返回,`persistApplied` 应把它拼在「已记录…」之后写进确定性 turn;真机正文里没有这句。怀疑 `persistV9DeterministicTurn(requestId = actionId)` 与选择动作共用 request id 被幂等吞掉,或 `spoken` 为空。用 staging 容器日志核实:`[rectification-v9] persist next focus failed`、`refresh discriminator probes failed`、`persist 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_times` 取 `column_times`;刷新后若公开探针与已答集合无差集,回执写 `refresh_result="no_new_probes"`。
|
||
3. 「等待收窄」的决策不得没有载体:`targetedCollectExhausted` 必须由**已问过**(focus 存在且 resolved/declined/skipped)来判定,不是由池非空判定;定向题落库失败时,同一事务内把 `collect:targeted:*` 记为 `skipped` 话题并直接交付卡(BUG-651 口径),不得停在 `ask_fact_collection` 且无焦点。
|
||
4. 客户端:`stopReason=null` 且 `sessionOutcome=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_transitions`(reason / raw_answer / inference_state.candidates 数量 / candidate_set_id / refresh_count)与 `latest_result` 的候选投影为何为空;用 §1 序列写回放测试,断言第六题答完后 `persistServerOwnedFocus` 的 status、`refresh` 返回的新探针数、决策 `nextAction / canOfferRange / stopReason`,把三者写进进度记录第一段。
|
||
- **T1 采集焦点 kind 映射(BUG-655)**:`server-focus.ts` `persistCollectFocus` 与 `persistSpokenChoiceFallback` 写 `targetKind` 前经 `collectFocusTargetKind(followup)`(枚举内或 null),`kind_hint` 原值写入 `schema.collect_kind`;`answer-choice.ts` / `agent-run.ts` 读 `collect_kind` 判定 invite / anchor / generic / targeted。验收:定向、锚定、通用三类 followup 在 `rectification-server-focus.test.ts` 里用真实 CHECK 列表断言 `targetKind` 合法且 `status=created`;`npm run test:db` 若有 Docker 则跑焦点插入。
|
||
- **T2 引擎按剩余候选刷新(BUG-656)**:`api_service.py` / `refinement_packet.py` 在 `refresh_probes` 为真时把 `column_times` 作为探针生成器的 `candidate_times`;`event_probes.py` 的 `refresh 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` 与同域同年去重;`narrowingExhaustion` 的 `targetedCollectExhausted` 改为「`collect:targeted:*` 焦点已存在且非 active」或池空;`persistNextInterviewAfterChoice` / `persistNextInterviewIfIdle` 在定向题落库 `skipped` 时写 `collect:targeted:<domain>` 为 `skipped` 话题并按 BUG-651 出卡;`decideRectification` 加断言:`collect` 且 `waitToNarrowCapability` 只允许在 `targetedCollectExhausted=false` 且**本事务已成功落下焦点**时返回。客户端 `rectificationQuestionGapState`:`sessionOutcome=collect_evidence && !stopReason && 无焦点无卡` → `collect_waiting`。验收:§1 序列第六题答完 → 出定向题「还有 N 条线能把 04:48 和 05:07 分开:…」;答一件家人事 → 重算回 S2;答「没有了」→ 出「目前范围」卡。模拟落库失败 → 同一事务出卡,不出现「没有拿到下一个问题」。
|
||
- **T4 记录**:BUG-655~657 入历史(655 关联 BUG-648、BUG-442「覆盖判定不得依赖模型 domain」;656 关联 BUG-653;657 关联 BUG-651/652/646);CHANGELOG;PROGRESS;`docs/testing/rectification-scenarios-20260907.md` 场景 0b 补「定向题必须出现」一条。
|
||
|
||
## 6. 让步顺序
|
||
|
||
T2 若引擎侧改动大,可先只做 T1 + T3(定向补事能问、问完必出卡),刷新留下一单;T1 + T3 不可省。
|
||
|
||
## 7. 开工前置命令
|
||
|
||
```bash
|
||
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 序列第六题答完必须看到定向补事题;说「没有了」后看到「目前范围」卡;全程不出现「没有拿到下一个问题」。
|