fix(rectification): persist refresh attempts and map collect focus kinds (BUG-656/657/658)
Independent Staging Quality Gate / validate (push) Failing after 13m28s
Independent Staging Quality Gate / publish (push) Skipped

Empty engine refresh now records an already_answered attempt so GET can leave wait-to-narrow; collect kinds map onto the table CHECK; remaining candidates drive probe refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-12 06:09:20 +08:00
co-authored by Cursor
parent 2b6d28e7a7
commit eae049d0ea
17 changed files with 747 additions and 62 deletions
+51 -3
View File
@@ -10191,8 +10191,56 @@
- 触发条件:训练门已开、带年月池空、刷新调用引擎后没有可并入的新题,或并入探针 id 与已答 id 命名不一致,或写入时候选被清空 / 候选集被改掉。
- 根因:刷新在引擎没有新题时仍写 `reason=supersede` 的推理记录;GET 侧可能选中未映射进 `inference_state.probes` 的引擎探针,`expectedAnswerSchemaFor` 盖不上 `probe_id` 后 persist 返回 `invalid_choice_schema` / `skipped`
- 修复:只在引擎映射出可问新题时写库;写入前断言候选非空且 `candidate_set_id` 不变,候选沿用刷新前集合;并入探针 id 与已答同一规则(`probe:<semantic_key>` 或带 split hash);刷新只把可问探针写进 `inference_state.probes` 和 receiptGET 选中的 key 必须能 stamp 上 `probe_id`
- 验证:`frontend/tests/rectification-probe-pool-exhausted-20260911.test.ts` T0 打印最后一行推理记录与 GET 探针 key / persist 状态;T3 空刷新与候选集变更不写库
- 防复发:新写库必须同时满足新题、候选非空、候选集不变、探针 id 与已答同名。不得靠客户端「接着问」补救(BUG-652)。
- 相关记录:BUG-653、BUG-654、BUG-652、BUG-587、BUG-594
- 验证:`frontend/tests/rectification-probe-pool-exhausted-20260911.test.ts` T0 打印最后一行推理记录与 GET 探针 key / persist 状态;有新题时 `supersede`,空刷新改由 BUG-658 写尝试记录
- 防复发:新探针写库必须同时满足新题、候选非空、候选集不变、探针 id 与已答同名。空刷新不得写 `supersede`不得靠客户端「接着问」补救(BUG-652)。
- 相关记录:BUG-653、BUG-654、BUG-652、BUG-587、BUG-594、BUG-656、BUG-657、BUG-658
- 复发自:BUG-653(刷新总会写库,未校验新题与候选集)
- 修复版本:`512be9b7`
## BUG-656 | 采集焦点 kind 必须映射到表约束枚举
- 状态:resolved
- 首次发现:2026-09-11
- 最近更新:2026-09-12
- 影响面:`persistCollectFocus``persistSpokenChoiceFallback``agentic_rectification_conversation_focuses.target_kind`
- 用户现象:带年月题问完后应出现定向补事口述题,但焦点写不进库,界面停在没有下一问。
- 触发条件:下一问是定向 / 锚定 / 通用采集,`kind_hint``targeted:<domain>``anchor:…``generic:…`
- 根因:`targetKind` 直接写入 `kind_hint`,违反 `target_kind` CHECK;插入失败后 persist 返回 `skipped`,定向题从未成为活动焦点。
- 修复:写库前经 `collectFocusTargetKind` 映射到 CHECK 枚举(产品别名 `relationship_change``relationship_start``education_milestone``education_start`);原 `kind_hint` 写入 `expected_answer_schema.collect_kind`;读取侧先读 `collect_kind` 再回退旧字段。不放宽 CHECK、不加迁移。
- 验证:`frontend/tests/rectification-server-focus.test.ts` 用真实 CHECK 列表断言三类 followup 的 `targetKind` 合法且 `status=created``rectification-probe-pool-exhausted-20260911.test.ts` 第六题后定向题成为活动焦点。
- 防复发:采集焦点 `target_kind` 必须落在 CHECK 列表内;`kind_hint` 原值只进 schema,不进列。
- 相关记录:BUG-648、BUG-442、BUG-658
- 复发自:BUG-648(采集题号可重试,但 kind 仍未映射到表约束)
- 修复版本:待发布
## BUG-657 | 引擎刷新必须按剩余候选生成探针
- 状态:resolved
- 首次发现:2026-09-11
- 最近更新:2026-09-12
- 影响面:`scripts/rectification/refinement_packet.py``event_probes.py` `refresh and remaining` 分支
- 用户现象:第六题后刷新仍按整段约 31 分钟网格出题,剩余几分钟上没有新边界时永远 `no_new_probes`
- 触发条件:`refresh_probes=true` 且请求带非空 `column_times`(剩余活跃候选)。
- 根因:`build_refinement_packet``grid_times` 交给探针生成,`column_times` 只喂三列对照;`refresh and remaining` 看不到剩余分钟。
- 修复:刷新且 `column_times` 非空时,用它们作为 `_discriminating_event_probe_lists``candidate_times`;回执写 `refresh_result=new_probes|no_new_probes`
- 验证:`tests/test_candidate_discriminator_contract.py`「31 格 + column_times=5」:探针 `candidate_ids` 只含这 5 个;无边界则 `refresh_result=no_new_probes`
- 防复发:刷新探针生成不得默认整网格;`column_times` 非空时必须作为剩余候选。
- 相关记录:BUG-653、BUG-658
- 复发自:BUG-653(刷新调用了引擎,但探针仍按全网格算)
- 修复版本:待发布
## BUG-658 | 刷新尝试必须落库;等待收窄按已问过判定;定向题落库失败同一事务出卡
- 状态:resolved
- 首次发现:2026-09-12
- 最近更新:2026-09-12
- 影响面:`refreshDatedDiscriminatorPoolIfNeeded``narrowingExhaustion``persistExhaustionCollect``interviewCollectWaiting`
- 用户现象:引擎没有新题时界面永久「等待收窄」,每次答题再调一次引擎(约 20s),并出现「没有拿到下一个问题」。
- 触发条件:训练门已开、带年月池空、刷新无新题;或定向补事焦点因 CHECK/落库失败。
- 根因:512be9b7 空刷新什么都不写,`refresh_count` 恒 0GET 不调刷新,`refreshExhausted` 永假,`waitToNarrowCapability` 无题无卡。`shouldRefreshDatedPool` 只看 `refresh_count`,同一已答数会反复打引擎。定向题 persist `skipped` 时仍停留在无焦点的口述。
- 修复:空刷新把 `inference_state.refresh_attempts[]``reason=already_answered` 写入(幂等键 `refresh_attempt:<set>:<count>`);有新题仍走 `supersede` 并附带尝试记录。`refreshExhausted` 由同一候选集、同一已答数下已有尝试记录(或剩余换升层为空)判定;`shouldRefreshDatedPool` 同源,至多一次引擎刷新。定向题 persist `skipped` 时同一事务把 `collect:targeted:<domain>` 记为 skipped 并出「目前范围」。客户端 `collect_evidence` 且无 stopReason、无焦点无卡时走 `collect_waiting`
- 验证:`frontend/tests/rectification-probe-pool-exhausted-20260911.test.ts` 空刷新写 `already_answered`、第二次不调引擎、GET 进定向补事;定向 persist 失败同一事务出「目前范围」、不得出现「没有拿到下一个问题」;`rectification-surface-state.test.ts` `collect_waiting``insufficient_*` 也可成立。
- 防复发:空刷新不得只打日志;GET 决策不得依赖只存在于内存的刷新结果。新探针仍禁止空 `supersede`BUG-655)。
- 相关记录:BUG-655、BUG-651、BUG-652、BUG-656、BUG-657
- 复发自:BUG-655(堵住空 `supersede` 后未留下「已刷新」事实)
- 修复版本:待发布
@@ -0,0 +1,29 @@
# 进度 · 修复单:采集焦点 kind、剩余候选刷新、刷新尝试落库(2026-09-12)
## 范围
- 分支:`codex/rectification-targeted-collect-persist-fix-20260911`(基于 `origin/staging` @ `2b6d28e7`
- 任务单:`docs/tasks/TASK-rectification-targeted-collect-persist-fix-20260911.md`
- BUG-656:采集焦点 `kind` 映射到表 CHECK
- BUG-657:引擎刷新按剩余候选
- BUG-658:刷新尝试落库、等待收窄按已问过判定、落库失败同一事务出卡
- T1 与 T3 不可省;T2 已一并做完
## 完成
- T1 `collectFocusTargetKind``targeted:` / `anchor:` / `generic:` 写入 CHECK 枚举;原 hint 进 `collect_kind`
- T2 `build_refinement_packet``refresh_probes``column_times` 非空时用剩余分钟生成探针;回执 `refresh_result`
- T3 `refresh_attempts``already_answered` 持久化;`refreshExhausted` / `shouldRefreshDatedPool` 同源;定向 persist `skipped` 出「目前范围」;`collect_waiting` 覆盖无 stopReason 的采集缺口
- T4 BUG-656658、CHANGELOG、本文件、场景 0c
## 验收(本机,未部署)
- `frontend` `tsc --noEmit` 0 错
- eslint 变更文件 0 error(既有 unused-import warning 仍在)
- `rectification-server-focus.test.ts` + `rectification-probe-pool-exhausted-20260911.test.ts` + `rectification-surface-state.test.ts` + `rectification-replay-20260911.test.ts`**51 / 51**
- Python`test_candidate_discriminator_contract.py` + `test_rectification_event_probes.py`fail=0`.venv/bin/python -m pytest`
## 未做
- 未 commit / 未 push
- 未部署后按场景 0c 真机走一遍
@@ -41,9 +41,12 @@
期望:
- 引擎若没有新的带年月题,不得新写推理记录,界面走定向补事或已有载体
- 第六题答完必须看到定向补事口述题(例如「还有 N 条线能把两端分开」);题干种类必须能写入焦点表,不得静默失败
- 引擎若没有新的带年月题,界面走定向补事或已有载体;同一候选集、同一已答数下引擎刷新只调用一次,不得反复打约 20 秒的刷新
- 引擎若有新题,下一问必须出现;不得「GET 已有探针、界面没有下一问」
- 刷新不得把候选集改空或换成另一套 `candidate_set_id`
- 说「没有了」后看到「目前范围」卡;定向题若写不进库,同一轮也必须出这张卡
- 全程不出现「没有拿到下一个问题」
## 1. 范围:家人说两点到四点