Files
Jyotisha/docs/tasks/TASK-rectification-delivery-vs-collect-split-20260914.md
T
Jesse_ChenandClaude Fable 5 e59ef9fbfd docs(tasks): brief for delivery narrated while decision falls back to collect
tied_first 并列到顶时,persistNextInterviewIfIdle 因刷新尝试「落库失败仍回报
已尝试」而在第二次计算时判交付并播出结论,GET 重算又回到 collect_evidence 且
清零交付能力(BUG-680)。交付话术闸门认 completed_with_range/provisional_range,
交付卡闸门只认 ADOPT_OUTCOMES,能说不能画(BUG-681)。缺口状态机没有终态,
兜底印「没有拿到下一个问题」(BUG-682)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-09-14 07:00:55 +00:00

14 KiB
Raw Blame History

TASK · 播了交付结论却回到采集态:卡不出、题没有、只剩「没有拿到下一个问题」 — 2026-09-14

  • 基线:origin/staging @ b50a8f94staging 已部署该 SHA,含 BUG-673679 的修复)。
  • 分支:codex/rectification-delivery-vs-collect-20260914worktree .worktrees/rectification-delivery-vs-collect-20260914
  • 关联:BUG-674(同源:POST 与 GET 对同一状态判定不一致)、BUG-675、BUG-678、BUG-590、BUG-587。
  • 串行:改 refresh-discriminator-probes.tsrectification-decision.tsrectification-candidate-result.tsrectification-surface-state.ts。开工前 git log --oneline origin/staging -20 复核无同文件在途分支。

1. 事故实证(产品负责人 staging 真机,2026-09-14,部署 SHA b50a8f94

用户看到的

跳过最后一道定向补事题(家人)之后,助手播出完整的交付结论

再问下去也分不开 04:53 和 05:00,没有年份的分盘题不再问,所以这一轮不再往下问。
现在给的范围是 04:48 到 05:07,代表分钟为 04:53。采用后没有还能核对的前事,
之后新建对话即按此时间排盘,对不上可改选。
我按你说的经历认真分析过了,下面是这次的结果。

然后没有交付卡没有下一题,界面只剩:

目前范围 04:48–05:07,再说一件带年月的事就能继续
没有拿到下一个问题。

快照实证(同一会话 GET,产品负责人提供,已脱敏)

"interview": {
  "type": "ask_fact_collection",  "session_outcome": "collect_evidence",
  "can_offer_range": false, "can_adopt": false, "selection_allowed": false, "propose_allowed": false,
  "precision_stage": "collect_events", "stop_reason": "tied_first",
  "representative_time": "04:53", "credible_range": ["04:48","05:07"]
},
"current_question": null, "choice_card": null,
"next_user_action": { "id": "explain_current_window",  }

活跃候选 posterior04:53 = 16、05:00 = 16、05:06 = 15、04:51 = 14、04:59 = 13(前两名完全并列,probability 也都是 0.2592219508007118)。

这组字段只能由一条分支产生

evaluateCandidateSeparationlead = 16 16 = 0 → tiedForFirst = truesufficient = falseclassifyStopcore/rectification-decision.ts:237):if (separation.tiedForFirst) return { kind: "exhausted", reason: "tied_first" }decideRectification!separation.sufficient 分支:

if (stillNeedNarrowing(input)) {
  return collect(separation, holdout, range, probe, waitToNarrowCapability(capability), stopReason);
}
if (stopClass?.kind === "exhausted") {
  return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
}

collect() 给出 session_outcome: collect_evidenceprecision_stage: collect_eventscan_offer_range:falsewaitToNarrowCapability()canAdopt / selectionAllowed / proposeAllowed 全清零,stopReason 原样带出 tied_first —— 与快照逐字段吻合。结论:GET 侧 stillNeedNarrowing(input) === true,即 refreshExhausted === false || targetedCollectExhausted === false

为什么 POST 那一轮却交付了

交付话术出自 persistNextInterviewIfIdleanswer-choice.ts:1645 一带,由 agent-run.ts:621 在本轮没有持久化问题时调用)。它的算法是:

let decision = decideFromDossier(dossier, );                 // ① 第一次算:还要继续收窄
const refreshed = await refreshDatedDiscriminatorPoolIfNeeded({});
if (refreshed.refreshed || refreshed.attemptRecorded) {
  dossier = { dossier, latestResult: { latest, decisionReceipt: overlay.decisionReceipt ??  } };
  decision = decideFromDossier(dossier, );                   // ② 第二次算:refreshExhausted 变真 → 交付
}

refreshDatedDiscriminatorPoolIfNeeded 在写不进库时仍然回报"已尝试"

// frontend/src/lib/rectification-agentic/v9/refresh-discriminator-probes.ts:568
await persistRefreshAttempt({  });   // ← 返回值 boolean 被丢弃
return { dossier: , state: attempted, refreshed: false, attemptRecorded: true };

persistRefreshAttemptexpectedRevision: previous.revision 写 inference state;本轮的选择题回答刚刚把 revision 推进过,这里很容易撞版本冲突 → catchreturn false落库失败。但调用方不看返回值,attemptRecorded: true 照常让第 ② 次计算把 refreshExhausted 判为真 → stillNeedNarrowing 变假 → 走 completeWithRange("exhausted", tied_first)deliveryNarrationAllowed 放行 → 播出交付结论。

下一次 GET 读的是库里的 receipt,没有这条 refresh attempt → refreshExhausted 回到 false → 又回到 collect()同一状态,POST 说交付,GET 说继续收集。

第二层:就算 GET 判成交付,卡也不会出

// core/rectification-decision.ts:126
ADOPT_OUTCOMES = { adopt_representative, provisional_range_user_stopped, awaiting_confirmation, validated_range }

// rectification-candidate-result.ts:426
canShowRectificationSelectionCards = selectionAllowed && canAdopt
  && rectificationDecisionReceiptAllowsAdoption(receipt)
  && ADOPT_OUTCOMES.has(sessionOutcome);

// core/rectification-decision.ts:148
deliveryNarrationAllowed = publicCanAdopt(decision)
  || (selectionAllowed === true && nextAction === "offer_provisional_range");

completed_with_rangeprovisional_range —— 也就是"问不下去了,给你区间"这两种正常收尾 —— 不在 ADOPT_OUTCOMES,而话术闸门认这两种。区间交付卡只在 rectification-agentic-chat.tsx:1844 一处渲染,挂在 showSelectionCards 之下,没有第二条路径。所以只要 canAdopt 为假,就必然"有结论、没有卡"。

第三层:这个状态没有终态,兜底成一句报错

// rectification-surface-state.ts:381 interviewCollectWaiting
if (sessionOutcome === "collect_evidence" && !stopReason) return true;
return stopReason === "insufficient_dated_events" || "insufficient_domains" || "insufficient_events";

当前组合是「没有题 + collect_evidence + stopReason = tied_first」→ 三个条件都不满足 → 不是 collect_waiting、不是 persisted_question、不是 verified_idle → 掉到重试耗尽的 unavailable → 印出「没有拿到下一个问题」。

2. 根因

  1. BUG-680:刷新尝试落库失败仍按"已尝试"推进决策(attemptRecorded: true 无视 persistRefreshAttempt 的返回值),使 POST 与 GET 对同一状态给出相反结论;叠加 stillNeedNarrowing 排在 stopClass === "exhausted" 之前,"并列到顶(tied_first"这个终局信号被"还有收窄手段没用完"无条件压过。
  2. BUG-681:交付话术闸门与交付卡闸门不是同一套,completed_with_range / provisional_range 能说不能画。
  3. BUG-682:问题缺口状态机没有"已交付 / 没有更多可问"的终态,兜底文案是「没有拿到下一个问题」这种报错口吻。

3. 决策记录

决策 内容
D1 落库失败不得当成已尝试。 refreshDatedDiscriminatorPoolIfNeededattemptRecorded 必须等于 persistRefreshAttempt 的真实返回值;写失败时按"未尝试"继续(本轮不交付、继续给定向补事或诚实说明),不得靠内存态播交付。
D2 并列到顶是终局,不被收窄手段压过。 decideRectification!separation.sufficient 分支里,stopClass?.kind === "exhausted" && reason === "tied_first" 的判断提到 stillNeedNarrowing 之前:候选前两名分数完全并列时,再多的定向补事也分不开,应当直接交付区间。其余 exhausted 原因维持现有顺序(不扩大改动面)。
D3 能说就能画。completed_with_rangeprovisional_range 纳入交付卡可见集合(新增一个 DELIVERY_OUTCOMES,卡片闸门用它,ADOPT_OUTCOMES 仍只管"可采用"语义),使"播了交付话术"与"出了交付卡"永远同真同假。卡在这两种 outcome 下按只读区间卡呈现:可看候选、可选采用(若 selection_allowed),不得反过来把 can_adopt 提权。
D4 补终态。 缺口状态机新增 deliveredcurrent_question === null 且 outcome ∈ 交付集合(或 stop_reason 属于 tied_first / user_uncertainty_too_high 这类终局)时,显示区间与出口说明,禁止再出现「没有拿到下一个问题」。
D5 不动数据库结构与迁移、不动 deploy/** 与 workflow、不动 page.tsx、不新增依赖。不得为了让状态"看起来对"而放宽 can_adopt / confirmation_allowed 或任何置信度边界(§8 红线)。

4. 硬红线

  1. tsc --noEmit 0 错;npm run lint 0 errornpm test 失败清单与基线逐条一致(本机基线 27 条,全是无 Docker / DB / rsync 缺口;工作树缺 .venv 软链会多一条 workflow YAML 假红);next build 通过且 /○ StaticTurbopack 拒软链 node_modules);首屏 gzip ±2%。
  2. 新增断言必须是行为断言,不得用 readFileSync + 正则匹配源码。
  3. 不得新增第二套交付卡渲染路径。
  4. 交付话术与交付卡必须由同一个判据驱动;任何新增分支都要同时过这两处。

5. 任务分解

任务 1 · 刷新尝试落库失败不得当成已尝试(BUG-680,P0)

  • refresh-discriminator-probes.ts:568 一带:接住 persistRefreshAttempt 的返回值,attemptRecorded 用真实结果;失败时 console.warn(JSON.stringify({ event: "rectification_refresh_attempt_persist_failed", case_id, candidate_set_id, answer_count }))
  • core/rectification-decision.ts:按 D2 把 tied_first 的终局判断提到 stillNeedNarrowing 之前。
  • 验收标准:
    • 行为单测:persistRefreshAttempt 抛错(模拟版本冲突)时 refreshDatedDiscriminatorPoolIfNeeded 返回 attemptRecorded: false,且随后的决策不是交付。
    • 行为单测(回归核心):同一份 dossier + inference state(前两名 posterior 并列 16/16refresh_attempts 为空),persistNextInterviewIfIdle 的决策与 decideFromDossier 的决策必须一致——要么都交付,要么都继续收集。这条是本单的防复发锚点。
    • 行为单测:并列到顶时 decideRectification 返回 completeWithRangestop_reason = tied_firstcan_offer_range = true

任务 2 · 能说就能画(BUG-681,P0

  • core/rectification-decision.ts:新增 DELIVERY_OUTCOMES= ADOPT_OUTCOMES { completed_with_range, provisional_range }),导出。
  • rectification-candidate-result.ts:426canShowRectificationSelectionCards 改用 DELIVERY_OUTCOMEScanShowRectificationReadonlyRange 相应排除交付 outcome(避免同屏既出卡又出"再说一件…")。
  • 验收标准:
    • 行为单测:outcome = completed_with_rangeselection_allowed = truecan_adopt = false 时,canShowRectificationSelectionCards 为真,canShowRectificationReadonlyRange 为假。
    • 行为单测:deliveryNarrationAllowed 为真的每一种 outcomecanShowRectificationSelectionCards 都为真(用表驱动遍历所有 outcome,锁死"同真同假")。

任务 3 · 补终态(BUG-682P1

  • rectification-surface-state.tsrectificationQuestionGapState 新增 delivered 分支(排在 collectWaiting 之前),条件:无题 + outcome ∈ 交付集合,或 stop_reason ∈ { tied_first, user_uncertainty_too_high }
  • 组件按该状态显示区间与出口说明(文案对照 frontend/docs/VOICE.md,不得出现「没有拿到下一个问题」「请稍候」)。
  • 验收标准:行为单测——该组合下 rectificationQuestionGapState 返回 delivered,不是 unavailable;普通"题没取到"仍返回 unavailable(不得误伤修复入口)。

任务 4 · 记录

  • docs/BUG_HISTORY.mdBUG-680 / 681 / 682 三条 resolved。BUG-680 的防复发必须写明:任何"是否已尝试/已耗尽"的标志,落库失败时一律按未完成处理;POST 与 GET 对同一状态的决策必须一致,并有一条对拍测试守着。
  • CHANGELOG.md 一行;PROGRESS-rectification-delivery-vs-collect-20260914.mddocs/testing/ 真机清单(要覆盖:并列到顶时给出区间交付卡;不再出现「没有拿到下一个问题」;同屏不再既出卡又出"再说一件带年月的事")。

6. 让步顺序

  1. 任务 1、任务 2 必做——缺任一条用户都会看到"有结论没有卡"。
  2. 任务 3 若文案需要产品确认,可先用既有区间文案,把待确认项写进进度记录,但不得保留「没有拿到下一个问题」。
  3. D2 的顺序调整若牵动其他用例,最小改动只对 tied_first 生效,其余 exhausted 原因保持原样。

7. 开工前置命令

git fetch origin --prune
git worktree add -b codex/rectification-delivery-vs-collect-20260914 \
  .worktrees/rectification-delivery-vs-collect-20260914 origin/staging
cd .worktrees/rectification-delivery-vs-collect-20260914
ln -s /workspace/Jyotisha/.venv .venv
cd frontend && npm ci

AGENTS.md §5,开工前用「tied_first」「没有拿到下一个问题」「交付卡」「refresh attempt」「stillNeedNarrowing」检索 docs/BUG_HISTORY.md,至少读完 BUG-674、BUG-678、BUG-590、BUG-587 四条。

8. BUG 编号起点

  • 起点 BUG-680(当前最大号 679,开工时以 docs/BUG_HISTORY.md 实际最大号为准)。