docs(tasks): skipped-health holdout dead end + exit gate fix brief (BUG-626/627)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
6008c07c81
commit
350fd13426
@@ -0,0 +1,60 @@
|
||||
# TASK · 健康题答「记不清」后职业题一答完就断:holdout 把跳过的健康线换个名字再问,撞焦点 id 静默失败;出口闸门把"穷尽"当成已交付(2026-09-09)
|
||||
|
||||
- 基线:`origin/staging` @ `6008c07c`(BUG-623~625 已合入)
|
||||
- 分支:`codex/rectification-skipped-health-deadend-20260909`,基于 `origin/staging`
|
||||
- 执行方:coding agent;验收:Claude
|
||||
- 涉及文件:`frontend/src/lib/rectification-agentic/v9/method-followup.ts`(`holdoutFollowupFor` L1676–1697、`declinedDomains` L509)、`v9/answer-choice.ts`(`persistNextInterviewAfterChoice` 的 `duplicate_focus` 分支、`inspectNonTerminalTurnExit` L1739–1753、`ensureNonTerminalTurnExit`)、`v9/agent-run.ts`(idle 异常吞掉后的处理 L555–575)、`frontend/src/components/rectification-agentic-chat.tsx`("没有拿到下一个问题 / 重新加载")、`frontend/src/app/api/rectification/cases/[caseId]/...`(重载改为修复)
|
||||
- BUG 编号起点:**BUG-626**(`docs/BUG_HISTORY.md` 当前最大 BUG-625)
|
||||
- 优先级:**P1**(真实用户,流程在最后一步断掉,只剩"没有拿到下一个问题"和一个无效的重新加载)
|
||||
|
||||
## 1. 事故实证(2026-09-09 真实用户截图 + 转录;只写结构)
|
||||
|
||||
顺序:学业 → 感情 → D9 风格 → 2018.04 事业 → 2024.08 迁居 → 家人「没有」→ 事业事件 → D10 风格 → 财务事件 → 2024.08 财务 → 2025.09 财务 → 健康题点「记不清」→ 职业题答完("已完成 2 步",正文一句复述)→ **"没有拿到下一个问题。[重新加载]"**,没有卡、没有旁白、没有题。时间轴 14:33–14:57,三个实心点。
|
||||
|
||||
## 2. 根因
|
||||
|
||||
### 2.1 BUG-626(P1):跳过的健康线在 holdout 里没被识别,再问一次时撞上刚跳过的焦点 id
|
||||
|
||||
1. 「记不清」把健康采集焦点标 `skipped`,`declined_skipped_topics` 里这条的 `target_domain` 是 **`health`**(DB 约束只有 `health`,没有 `health_pressure`;`persistableFocusDomain` 把 `health_pressure` 映成 `health`)。
|
||||
2. 职业答完后计划层走 `pendingHoldout = holdoutFollowupFor(input, declined)`:`declined` 由 `declinedDomains(topics)` 得到 `{family, health}`;holdout 候选来自 `oos_blind_prompts`,其 `domain` 是 **`health_pressure`**;`!declined.has("health_pressure")` 为真 → 把刚跳过的健康线当"没用过的线"再问一遍。BUG-590 只给 `occupied` 做了 health / health_pressure 归并,没给 `declined` 做。
|
||||
3. 这道 holdout 采集题的 `questionId` 是 `collect:health_pressure:collect_method_evidence`,与刚被跳过的焦点同 id → `focus_idempotency_conflict` → BUG-591 后非 retry 不再 `:next`,返回 `duplicate_focus`。
|
||||
4. `persistNextInterviewAfterChoice` 对 collect 类 followup:状态不是 `created / already_open` 就转 `persistExhaustionCollect`;那里 `exhaustionSpokenCollectFollowup` 为空(健康按 `declinedHealth` 已封)、`canAdopt` 为假(决策仍是 holdout / 区分)→ 只剩门槛句路径。截图里连门槛句都没有,说明这条链上某处返回了空旁白或抛了异常被 `agent-run` L565 的 `catch` 吞掉(日志有 `persist interview before collect attach failed`)。执行方开工先从 staging 日志确认是哪一种,两种都要修。
|
||||
|
||||
### 2.2 BUG-627(P1):出口闸门把"穷尽"当成"已交付"
|
||||
|
||||
`inspectNonTerminalTurnExit` 的 `satisfied` 包含 `exhausted`(`!remainingCollect && blockingMethodsCovered && !canAdopt && !accepted`)。穷尽只是一个状态,不是用户能看到的出口;本例正是穷尽为真、却没有任何问题 / 卡 / 句子落库,闸门照样放行,`ensureNonTerminalTurnExit` 不修复。BUG-558 的防复发写的是"穷尽必须有 `rectification_exhaustion_collect` 日志且写出 terminalNote",这个闸门把它绕过去了。
|
||||
|
||||
### 2.3 P2:客户端"重新加载"只重取快照
|
||||
|
||||
`questionGap === "unavailable"` 的按钮只 `refetchQuestion()`;服务端没有新东西,点多少次都一样。
|
||||
|
||||
## 3. 决策记录
|
||||
|
||||
1. **holdout 的 `declined` 与 `occupied` 一样做 health / health_pressure 归并**(`declinedHealth` 口径),跳过的领域本会话不再作 holdout 问。
|
||||
2. **`duplicate_focus` 不得静默。** `persistNextInterviewAfterChoice` 遇 `duplicate_focus` 时记 `rectification_focus_duplicate` 日志,并**必定**走 `persistExhaustionCollect`;`persistExhaustionCollect` 在三条分支(继续采集 / 采用 / 门槛句)之外不得返回空旁白——门槛句为空时用 `RECTIFICATION_USER_COPY.noCandidatesGate` 或范围句兜底,`terminalNote: true`。
|
||||
3. **出口闸门只认可见载体。** `satisfied` = 有活动问题 / 已采用 / 已确认 / 本轮已落库带 `terminalNote` 的 host turn / `publicCanAdopt`;`exhausted` 从 `satisfied` 里删掉。穷尽且无载体 → `ensureNonTerminalTurnExit` 调 `persistExhaustionCollect` 并把结果写成 host turn(现有 `persistExhaustionGateTurn`)。
|
||||
4. **idle 异常不再吞。** `agent-run` L565 的 `catch` 记日志后,仍调用 `ensureNonTerminalTurnExit`(闸门已在路由里,但要保证异常路径也到得了它)。
|
||||
5. **"重新加载"改为"修复"。** 客户端按钮改调 `POST /api/rectification/cases/[caseId]/repair-exit`(新路由,只做 `ensureNonTerminalTurnExit` + 返回快照),文案"接着问";连续两次仍无载体才显示"暂时接不上,请新建一次校正"。不新增迁移。
|
||||
6. 不动采用门、确认门、各道门槛、四选项合同;Skill 不 bump。
|
||||
|
||||
## 4. 任务分解
|
||||
|
||||
- 4.1 BUG-626:决策 1、2。用例——账本七领域 + 健康 `skipped`(topic `target_domain=health`)+ oos 含 `health_pressure` → `holdoutFollowupFor` 为 null;同形状进 `persistNextInterviewIfIdle` → 返回非空 `hostNarration` 且(`canAdopt` 时)`terminalNote`;mock `persistCollectFocus` 返回 `duplicate_focus` → 仍有非空旁白。
|
||||
- 4.2 BUG-627:决策 3、4。用例——穷尽为真、无活动焦点、无 host turn → `ensureNonTerminalTurnExit` 写出 host turn;有 terminalNote turn 时不重复写。
|
||||
- 4.3 决策 5:路由 + 客户端;`rectification-agentic-entry` 合同:unavailable 态按钮文案与请求路径。
|
||||
- 4.4 记录:`docs/BUG_HISTORY.md` BUG-626(复发关联 BUG-558 / BUG-590 决策 4 / BUG-591 / BUG-605)、BUG-627(关联 BUG-558 防复发条款);`CHANGELOG.md`;`PROGRESS-…`;`docs/testing/rectification-scenarios-20260907.md` 加"健康题点『记不清』后职业答完必须出卡或门槛句,不得出现『没有拿到下一个问题』"。
|
||||
|
||||
## 5. 让步顺序
|
||||
|
||||
4.1 决策 1 一行改动先部署;4.1 决策 2、4.2 必做;4.3 可后置为 P2;4.4 不可省。
|
||||
|
||||
## 6. 开工前置命令
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/rectification-skipped-health-deadend-20260909 .worktrees/rectification-skipped-health-deadend-20260909 origin/staging
|
||||
cd .worktrees/rectification-skipped-health-deadend-20260909
|
||||
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
|
||||
cd frontend && ./node_modules/.bin/tsc --noEmit; npm run lint 2>&1 | tail -1
|
||||
ls tests/rectification-*.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"
|
||||
```
|
||||
Reference in New Issue
Block a user