Files
Jyotisha/docs/tasks/TASK-rectification-other-collect-fallback-20260908.md
T

11 KiB
Raw Blame History

TASK · 生时校正修复单:采集问完落到「也可以再说一件你记得大概时间的事」,职业题从未出现、没有自动出卡(2026-09-08)

  • 基线:origin/staging @ 3973b6c3(代码头 a00b069dstaging 已部署 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.tspersistableFocusDomainstableFollowupQuestionId)、v9/method-followup.tsactive-focus 承接 followup、collectQuestionDomainotherCollectFollowup 死代码)、frontend/src/mastra/rectification-v9-tools.tsset-focus 的 invalid_spoken_prompt 兜底 L10851100)、frontend/src/lib/rectification-agentic/user-copy.tsUSER_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 分钟。最后一轮:

  1. 助手气泡两段旁白(BUG-584);
  2. 「本轮对照了 …」方法句;
  3. 问题块:「也可以再说一件你记得大概时间的事。」——这是 USER_COLLECT_QUESTION.other 逐字;
  4. 没有区间交付卡、没有职业题「你平时主要做什么工作?」、没有停止按钮以外的出口。receipt 显示该轮 6 步(此前每轮 3 步)。

按当前设计,这一步应当是:职业口述题 → (若有未问领域则 holdout)→ adopt_representative 自动出区间交付卡。职业题一次都没出现,而「other」这句取代了它。

2. 已由代码确认的事实

2.1 这句固定文案在 a00b069d 仍有三条活路

spokenFollowupForUserdomain="other" 的采集 followup:只有「source=method_coveragemethod_id=dasha_events 且非 retry 且账本无带年月事件」才给开场句 GENERIC;其余一律查表落到 USER_COLLECT_QUESTION.other。BUG-558 只堵了 exhaustionSpokenCollectFollowup 这一条(otherCollectFollowup 现已无人调用,是死代码),但 domain="other" 还能从下面三处产生:

# 来路 位置
A 职业焦点落库时领域被压成 otherDB 约束 target_domain in (education, career, relationship, relocation, finance, health, family, other) 没有 occupationpersistableFocusDomain("occupation") 返回 "other"(测试 rectification-server-focus.test.ts L716/L775 锁定)。随后 buildMethodFollowupPlan 有 active focus 时用 domain: focus.targetDomain 重建「承接 followup」(method-followup.ts L20162025source="active_focus"),职业焦点被重建成 domain=other, intent=collectspokenFollowupForUser 给出「也可以再说一件」。stableFollowupQuestionId 对它算出 collect:other:collect_method_evidence,与原职业焦点 collect:occupation:collect_method_evidence 不同,因此不会被判 already_open,会 supersede 掉职业题 server-focus.ts L201206、L5253method-followup.ts L2016
B set-focus 工具的兜底:模型 spokenPrompt 两次 invalid_spoken_prompt 后,USER_COLLECT_QUESTION[nextFollowup.domain] 直接落库;nextFollowup.domain="other" 时就是这句 rectification-v9-tools.ts L10851100
C spokenCollectFallbackFollowup / `collectQuestionDomain(null "unknown"

2.2 尚未确认的触发链(investigating

最符合转录的链:该轮模型先用 set-focus 写出职业题(焦点 target_domain=other),同一轮再次调用 set-focus(6 步);第二次的 next_followup 是承接 followupdomain=other),模型的 spokenPromptdomain_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 → persistExhaustionCollectexhaustionSpokenCollectFollowup 为空且 acceptanceAllowed && trainingGate.open && canAdopt 时写 adopt_representative 旁白 + terminalNote,客户端据此渲染区间交付卡。它只在没有 active focus 时运行(L1158 提前返回)。一个 collect:other:* 焦点挂着,出卡永远轮不到;用户答了这句「再说一件」之后,若职业仍未覆盖,还会再来一遍。

3. 决策记录

  1. 删掉 USER_COLLECT_QUESTION.otherUSER_COLLECT_QUESTION_RETRY.other(产品口径:多余入口宁可删除也不修)。spokenFollowupForUserdomain=other 只剩开场 GENERIC 一条路;非开场的 other 返回 null,上游按「无可问」处理。otherCollectFollowup 一并删除。
  2. 职业焦点不再丢领域。 不改表(本轮不动迁移):target_domain 仍写 other,但 questionId 已是 collect:occupation:collect_method_evidence;承接 followup 的 domain 改为「先从 questionId 解析 collect:<domain>: 段,再退回 focus.targetDomain」。新增 parseCollectFocusQuestionIdpersistableFocusDomain("occupation") === "other" 的两条测试保留(DB 约束未变)。
  3. set-focus 兜底只允许表内真实领域。 L10851100fallbackDomain 必须是 USER_COLLECT_QUESTION 的键且 ≠ other(删表后自然满足),否则返回 invalid_spoken_prompt 不落地;且兜底题干必须与当前 next_followup 同领域。
  4. 同一轮第二次 set-focus 必须承接、不得覆盖。 若 active focus 的 askedTurnId === turnId(本轮刚写的),set-focus 返回该焦点投影并标 idempotent: true,不重建、不 supersede。
  5. collect:other:* 非开场焦点的存量 CasepersistNextInterviewIfIdle 开头把这种焦点判为 stale 并 skipped(复用 isStalePreAdoptFocus 旁边加一条 isOrphanOtherCollectFocusquestionIdcollect:other: 开头且账本已有带年月事件),让出卡路径能跑起来。
  6. 不动采用门、确认门、MIN_SEPARATION_LEAD_relative_supportminute_step=1 指纹;不动 page.tsxSkill 不 bump。

4. 任务分解

4.1 BUG-586:三条活路 + 出卡

  • 决策 15 全部落地。
  • 验收(形状用例,rectification-collect-stall.test.ts / rectification-occupation-coverage-exit.test.ts 扩):
    • (a) 7 个带年月领域已覆盖或拒答、职业未覆盖、active focus 为 collect:occupation:*target_domain=other)→ buildMethodFollowupPlan 的承接 followup domain === "occupation"spokenFollowupForUser 给「你平时主要做什么工作?」,stableFollowupQuestionId 等于原焦点 idalready_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 && canAdoptpersistNextInterviewIfIdle 返回 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.ts L517/L727、rectification-adopt-flow-*.test.ts 里以 collect:other:collect_method_evidence 为期望值的断言逐条写「原值 / 新值 / 原因」。

4.2 记录

  • docs/BUG_HISTORY.md BUG-586:状态先写 investigating2.2 未证实)或 resolved(若执行方能从 staging receipt 证实触发链);复发自 BUG-558(同现象),关联 BUG-580、BUG-582CHANGELOG.mdPROGRESS-rectification-other-collect-fallback-20260908.mddocs/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-20260907BUG-584/585,改 step-answer.ts / agent-run.ts / collect-prompt.ts)无文件重叠;谁后合谁 rebase。

验收(Claude2026-09-08origin/staging @ df182c16staging 已部署同 SHA

结果
tsc 0 错
lint 0 error / 102 warning
前端 rectification + consultation + voice + skill-registry(非 DB 1253 / 0
Python v5_services + growth contract 全绿(执行方记的 15s 壁钟用例本机未复现超时)
Docker DB 套件、浏览器走查 环境缺口,同执行方记录
结论
决策 15 通过。grep 也可以再说一件 为空;parseCollectFocusQuestionId 让职业承接不再压成 other;同轮二次 set-focus 幂等;isOrphanOtherCollectFocus 放行出卡
2.2 触发链 仍 investigating(执行方按事实写 mitigated,接受)
真实 Case2026-09-08 职业题正常出现、其后不再出 other 句;但暴露下一道题(BUG-590,见 TASK-rectification-covered-domain-recollect-20260908.md

结论:通过。