Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
11 KiB
TASK · 生时校正修复单:采集问完落到「也可以再说一件你记得大概时间的事」,职业题从未出现、没有自动出卡(2026-09-08)
- 基线:
origin/staging@3973b6c3(代码头a00b069d,staging 已部署a00b069d;事故转录来自 2026-09-07 晚间实测,跑在84a29754还是a00b069d上待用户确认,下文两版都对过) - 分支:
codex/rectification-other-collect-fallback-20260908,基于origin/staging - 执行方:coding agent;验收:Claude
- 涉及文件:
frontend/src/lib/rectification-agentic/v9/server-focus.ts(persistableFocusDomain、stableFollowupQuestionId)、v9/method-followup.ts(active-focus 承接 followup、collectQuestionDomain、otherCollectFollowup死代码)、frontend/src/mastra/rectification-v9-tools.ts(set-focus 的invalid_spoken_prompt兜底 L1085–1100)、frontend/src/lib/rectification-agentic/user-copy.ts(USER_COLLECT_QUESTION.other/_RETRY.other) - BUG 编号起点:BUG-586(584/585 已由重复旁白修复单占用;开工时
grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1复核) - 优先级:P1(采集走完后无出口,第四次实测复现;BUG-558 同现象、不同来路)
1. 事故实证(只写结构)
七个带年月领域全部问过或拒过(学业、感情、事业、家人拒答、财务、迁居、健康),中间答了 5 张点选卡,范围从 30 分钟收到 8 分钟。最后一轮:
- 助手气泡两段旁白(BUG-584);
- 「本轮对照了 …」方法句;
- 问题块:「也可以再说一件你记得大概时间的事。」——这是
USER_COLLECT_QUESTION.other逐字; - 没有区间交付卡、没有职业题「你平时主要做什么工作?」、没有停止按钮以外的出口。receipt 显示该轮 6 步(此前每轮 3 步)。
按当前设计,这一步应当是:职业口述题 → (若有未问领域则 holdout)→ adopt_representative 自动出区间交付卡。职业题一次都没出现,而「other」这句取代了它。
2. 已由代码确认的事实
2.1 这句固定文案在 a00b069d 仍有三条活路
spokenFollowupForUser 对 domain="other" 的采集 followup:只有「source=method_coverage 且 method_id=dasha_events 且非 retry 且账本无带年月事件」才给开场句 GENERIC;其余一律查表落到 USER_COLLECT_QUESTION.other。BUG-558 只堵了 exhaustionSpokenCollectFollowup 这一条(otherCollectFollowup 现已无人调用,是死代码),但 domain="other" 还能从下面三处产生:
| # | 来路 | 位置 |
|---|---|---|
| A | 职业焦点落库时领域被压成 other:DB 约束 target_domain in (education, career, relationship, relocation, finance, health, family, other) 没有 occupation,persistableFocusDomain("occupation") 返回 "other"(测试 rectification-server-focus.test.ts L716/L775 锁定)。随后 buildMethodFollowupPlan 有 active focus 时用 domain: focus.targetDomain 重建「承接 followup」(method-followup.ts L2016–2025,source="active_focus"),职业焦点被重建成 domain=other, intent=collect,spokenFollowupForUser 给出「也可以再说一件」。stableFollowupQuestionId 对它算出 collect:other:collect_method_evidence,与原职业焦点 collect:occupation:collect_method_evidence 不同,因此不会被判 already_open,会 supersede 掉职业题 |
server-focus.ts L201–206、L52–53;method-followup.ts L2016 |
| B | set-focus 工具的兜底:模型 spokenPrompt 两次 invalid_spoken_prompt 后,USER_COLLECT_QUESTION[nextFollowup.domain] 直接落库;nextFollowup.domain="other" 时就是这句 |
rectification-v9-tools.ts L1085–1100 |
| C | spokenCollectFallbackFollowup / `collectQuestionDomain(null |
"unknown" |
2.2 尚未确认的触发链(investigating)
最符合转录的链:该轮模型先用 set-focus 写出职业题(焦点 target_domain=other),同一轮再次调用 set-focus(6 步);第二次的 next_followup 是承接 followup(domain=other),模型的 spokenPrompt 因 domain_mismatch(模型填 targetDomain="career",persistableFocusDomain 后 ≠ "other")或词表问题两次被拒,走 2.1-B 兜底,把职业题覆盖成「也可以再说一件」。这一段需要事故 Case 的 receipt 证实(预期能看到两条 rectification-set-focus / intent.classified / failed / invalid_spoken_prompt 和一条 completed,以及 collect:occupation:* 焦点被 collect:other:* supersede)。执行方拿不到 staging 数据时,把这一节保留为 investigating,不得编造。
无论触发链是哪条,2.1 的三条活路都是确定性缺陷,修掉后这句话在代码里不再存在。
2.3 为什么没有自动出卡
出卡的服务端路径是 persistNextInterviewIfIdle → persistExhaustionCollect:exhaustionSpokenCollectFollowup 为空且 acceptanceAllowed && trainingGate.open && canAdopt 时写 adopt_representative 旁白 + terminalNote,客户端据此渲染区间交付卡。它只在没有 active focus 时运行(L1158 提前返回)。一个 collect:other:* 焦点挂着,出卡永远轮不到;用户答了这句「再说一件」之后,若职业仍未覆盖,还会再来一遍。
3. 决策记录
- 删掉
USER_COLLECT_QUESTION.other与USER_COLLECT_QUESTION_RETRY.other(产品口径:多余入口宁可删除也不修)。spokenFollowupForUser对domain=other只剩开场 GENERIC 一条路;非开场的 other 返回null,上游按「无可问」处理。otherCollectFollowup一并删除。 - 职业焦点不再丢领域。 不改表(本轮不动迁移):
target_domain仍写other,但questionId已是collect:occupation:collect_method_evidence;承接 followup 的domain改为「先从questionId解析collect:<domain>:段,再退回focus.targetDomain」。新增parseCollectFocusQuestionId。persistableFocusDomain("occupation") === "other"的两条测试保留(DB 约束未变)。 - set-focus 兜底只允许表内真实领域。 L1085–1100:
fallbackDomain必须是USER_COLLECT_QUESTION的键且 ≠other(删表后自然满足),否则返回invalid_spoken_prompt不落地;且兜底题干必须与当前next_followup同领域。 - 同一轮第二次 set-focus 必须承接、不得覆盖。 若 active focus 的
askedTurnId === turnId(本轮刚写的),set-focus 返回该焦点投影并标idempotent: true,不重建、不 supersede。 - 有
collect:other:*非开场焦点的存量 Case:persistNextInterviewIfIdle开头把这种焦点判为 stale 并skipped(复用isStalePreAdoptFocus旁边加一条isOrphanOtherCollectFocus:questionId以collect:other:开头且账本已有带年月事件),让出卡路径能跑起来。 - 不动采用门、确认门、
MIN_SEPARATION_LEAD、_relative_support、minute_step=1指纹;不动page.tsx;Skill 不 bump。
4. 任务分解
4.1 BUG-586:三条活路 + 出卡
- 决策 1–5 全部落地。
- 验收(形状用例,
rectification-collect-stall.test.ts/rectification-occupation-coverage-exit.test.ts扩):- (a) 7 个带年月领域已覆盖或拒答、职业未覆盖、active focus 为
collect:occupation:*(target_domain=other)→buildMethodFollowupPlan的承接 followupdomain === "occupation",spokenFollowupForUser给「你平时主要做什么工作?」,stableFollowupQuestionId等于原焦点 id(already_open,不 supersede)。 - (b) 同 (a),模型
spokenPrompt两次无效 → set-focus 返回invalid_spoken_prompt,不落库任何collect:other:*。 - (c) 同一轮 set-focus 两次(第二次
next_followup为承接 followup)→ 第二次返回idempotent: true,焦点 id 不变。 - (d) 职业已答、无剩余采集、
acceptanceAllowed && trainingGate.open && canAdopt→persistNextInterviewIfIdle返回terminalNote: true的采用旁白,agent-run写 exhaustion gate turn,客户端canOfferCards为真(区间交付卡)。 - (e) 存量
collect:other:*焦点 + 账本有带年月事件 → 进入persistNextInterviewIfIdle时被skipped,随后走 (d)。 - (f)
agent-voice-copy-contract:源码与user-copy.ts不再含「也可以再说一件」;rectification-server-focus.test.tsL517/L727、rectification-adopt-flow-*.test.ts里以collect:other:collect_method_evidence为期望值的断言逐条写「原值 / 新值 / 原因」。
- (a) 7 个带年月领域已覆盖或拒答、职业未覆盖、active focus 为
4.2 记录
docs/BUG_HISTORY.mdBUG-586:状态先写investigating(2.2 未证实)或resolved(若执行方能从 staging receipt 证实触发链);复发自 BUG-558(同现象),关联 BUG-580、BUG-582;CHANGELOG.md;PROGRESS-rectification-other-collect-fallback-20260908.md;docs/testing/rectification-scenarios-20260907.md加「七领域问完后下一问必须是职业题或直接出区间卡,不得出现『再说一件』」。
5. 让步顺序
决策 1、2、3 必做且同一提交;决策 4 若 set-focus 工具改动牵扯太广可后置为 P2 但要写进度记录;决策 5 必做(否则存量 Case 卡死);4.2 不可省。
6. 开工前置命令
git fetch origin --prune
git worktree add -b codex/rectification-other-collect-fallback-20260908 .worktrees/rectification-other-collect-fallback-20260908 origin/staging
cd .worktrees/rectification-other-collect-fallback-20260908
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 tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"
grep -rn "也可以再说一件" src tests # 收工时必须为空
与 codex/rectification-duplicate-narration-20260907(BUG-584/585,改 step-answer.ts / agent-run.ts / collect-prompt.ts)无文件重叠;谁后合谁 rebase。
验收(Claude,2026-09-08,origin/staging @ df182c16,staging 已部署同 SHA)
| 门 | 结果 |
|---|---|
| tsc | 0 错 |
| lint | 0 error / 102 warning |
| 前端 rectification + consultation + voice + skill-registry(非 DB) | 1253 / 0 |
| Python v5_services + growth contract | 全绿(执行方记的 15s 壁钟用例本机未复现超时) |
| Docker DB 套件、浏览器走查 | 环境缺口,同执行方记录 |
| 项 | 结论 |
|---|---|
| 决策 1–5 | 通过。grep 也可以再说一件 为空;parseCollectFocusQuestionId 让职业承接不再压成 other;同轮二次 set-focus 幂等;isOrphanOtherCollectFocus 放行出卡 |
| 2.2 触发链 | 仍 investigating(执行方按事实写 mitigated,接受) |
| 真实 Case(2026-09-08) | 职业题正常出现、其后不再出 other 句;但暴露下一道题(BUG-590,见 TASK-rectification-covered-domain-recollect-20260908.md) |
结论:通过。