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
This commit is contained in:
co-authored by
Claude Fable 5
parent
b50a8f94ac
commit
e59ef9fbfd
@@ -184,6 +184,8 @@
|
||||
|
||||
| `TASK-rectification-targeted-card-dead-20260913.md` | `PROGRESS-rectification-targeted-card-dead-20260913.md` | **P0**:定向补事卡在快照投影里拿不到 `choice_card`(承接焦点分支不重建 `choice_frame`),卡片看得见点不动、流程停在采集等待态;模型还会把定向题改写成口述题(BUG-669~671)。先于 tie-break 修复单执行 | 待验收 | `codex/rectification-targeted-card-dead-20260913` |
|
||||
|
||||
| `TASK-rectification-delivery-vs-collect-split-20260914.md` | `PROGRESS-rectification-delivery-vs-collect-20260914.md` | **P0**:并列到顶(tied_first)时 POST 播了交付结论、GET 又算回 collect_evidence——刷新尝试落库失败仍回报「已尝试」+ `stillNeedNarrowing` 压过 exhausted(BUG-680);交付话术闸门与交付卡闸门不一致,`completed_with_range`/`provisional_range` 能说不能画(BUG-681);缺口状态机没有终态,兜底印「没有拿到下一个问题」(BUG-682) | 待执行 | `codex/rectification-delivery-vs-collect-20260914` |
|
||||
|
||||
| `TASK-rectification-spoken-orphan-and-engine-representative-20260914.md` | `PROGRESS-rectification-spoken-orphan-20260914.md` | **P0**:口述题(定向补事年份追问)没有 `askedTurnId` 时仍是无头像裸行——BUG-675 的挂回规则只覆盖选择题(BUG-678);同屏两个范围口径(旁白用活跃候选首尾、时间轴用 credible_range);引擎 `representative_time` 把已淘汰分钟的宫位表带进模型上下文(BUG-676 收敛为 resolved) | 待验收 | `codex/rectification-spoken-orphan-20260914` |
|
||||
|
||||
| `TASK-rectification-unstampable-probe-and-naked-card-20260914.md` | `PROGRESS-rectification-unstampable-probe-20260914.md` | **P0**:探针池空后判别题盖不上 probe → 焦点写不进 → 题干被念进正文、同一题反复问且答了不算数(BUG-674,推翻前一单 D3);快照已有 `choice_card` 但 `persisted_question` 仍只画一行裸题(BUG-675);同一轮 receipt 里 `representative_time` 取到 `eliminated_ids` 里的候选(BUG-676,investigating) | 待验收 | `codex/rectification-unstampable-probe-20260914` |
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
# TASK · 播了交付结论却回到采集态:卡不出、题没有、只剩「没有拿到下一个问题」 — 2026-09-14
|
||||
|
||||
- 基线:`origin/staging` @ `b50a8f94`(staging 已部署该 SHA,含 BUG-673~679 的修复)。
|
||||
- 分支:`codex/rectification-delivery-vs-collect-20260914`,worktree `.worktrees/rectification-delivery-vs-collect-20260914`。
|
||||
- 关联:BUG-674(同源:POST 与 GET 对同一状态判定不一致)、BUG-675、BUG-678、BUG-590、BUG-587。
|
||||
- 串行:改 `refresh-discriminator-probes.ts`、`rectification-decision.ts`、`rectification-candidate-result.ts`、`rectification-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,产品负责人提供,已脱敏)
|
||||
|
||||
```jsonc
|
||||
"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", … }
|
||||
```
|
||||
|
||||
活跃候选 posterior:04:53 = **16**、05:00 = **16**、05:06 = 15、04:51 = 14、04:59 = 13(前两名完全并列,probability 也都是 0.2592219508007118)。
|
||||
|
||||
### 这组字段只能由一条分支产生
|
||||
|
||||
`evaluateCandidateSeparation`:lead = 16 − 16 = 0 → `tiedForFirst = true`、`sufficient = false`。
|
||||
`classifyStop`(`core/rectification-decision.ts:237`):`if (separation.tiedForFirst) return { kind: "exhausted", reason: "tied_first" }`。
|
||||
`decideRectification` 的 `!separation.sufficient` 分支:
|
||||
|
||||
```ts
|
||||
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_evidence`、`precision_stage: collect_events`、`can_offer_range:false`,`waitToNarrowCapability()` 把 `canAdopt / selectionAllowed / proposeAllowed` 全清零,`stopReason` 原样带出 `tied_first` —— **与快照逐字段吻合**。结论:GET 侧 `stillNeedNarrowing(input) === true`,即 `refreshExhausted === false || targetedCollectExhausted === false`。
|
||||
|
||||
### 为什么 POST 那一轮却交付了
|
||||
|
||||
交付话术出自 `persistNextInterviewIfIdle`(`answer-choice.ts:1645` 一带,由 `agent-run.ts:621` 在本轮没有持久化问题时调用)。它的算法是:
|
||||
|
||||
```ts
|
||||
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` 在写不进库时**仍然回报"已尝试"**:
|
||||
|
||||
```ts
|
||||
// frontend/src/lib/rectification-agentic/v9/refresh-discriminator-probes.ts:568
|
||||
await persistRefreshAttempt({ … }); // ← 返回值 boolean 被丢弃
|
||||
return { dossier: …, state: attempted, refreshed: false, attemptRecorded: true };
|
||||
```
|
||||
|
||||
`persistRefreshAttempt` 用 `expectedRevision: previous.revision` 写 inference state;本轮的选择题回答刚刚把 revision 推进过,这里很容易撞版本冲突 → `catch` → `return false` → **落库失败**。但调用方不看返回值,`attemptRecorded: true` 照常让第 ② 次计算把 `refreshExhausted` 判为真 → `stillNeedNarrowing` 变假 → 走 `completeWithRange("exhausted", tied_first)` → `deliveryNarrationAllowed` 放行 → 播出交付结论。
|
||||
|
||||
下一次 GET 读的是库里的 receipt,没有这条 refresh attempt → `refreshExhausted` 回到 false → 又回到 `collect()`。**同一状态,POST 说交付,GET 说继续收集。**
|
||||
|
||||
### 第二层:就算 GET 判成交付,卡也不会出
|
||||
|
||||
```ts
|
||||
// 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_range` 与 `provisional_range` —— 也就是"问不下去了,给你区间"这两种正常收尾 —— **不在 `ADOPT_OUTCOMES` 里**,而话术闸门认这两种。区间交付卡只在 `rectification-agentic-chat.tsx:1844` 一处渲染,挂在 `showSelectionCards` 之下,没有第二条路径。所以只要 `canAdopt` 为假,就必然"有结论、没有卡"。
|
||||
|
||||
### 第三层:这个状态没有终态,兜底成一句报错
|
||||
|
||||
```ts
|
||||
// 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 | **落库失败不得当成已尝试。** `refreshDatedDiscriminatorPoolIfNeeded` 的 `attemptRecorded` 必须等于 `persistRefreshAttempt` 的真实返回值;写失败时按"未尝试"继续(本轮不交付、继续给定向补事或诚实说明),不得靠内存态播交付。 |
|
||||
| D2 | **并列到顶是终局,不被收窄手段压过。** `decideRectification` 的 `!separation.sufficient` 分支里,`stopClass?.kind === "exhausted" && reason === "tied_first"` 的判断提到 `stillNeedNarrowing` 之前:候选前两名分数完全并列时,再多的定向补事也分不开,应当直接交付区间。其余 `exhausted` 原因维持现有顺序(不扩大改动面)。 |
|
||||
| D3 | **能说就能画。** 把 `completed_with_range` 与 `provisional_range` 纳入交付卡可见集合(新增一个 `DELIVERY_OUTCOMES`,卡片闸门用它,`ADOPT_OUTCOMES` 仍只管"可采用"语义),使"播了交付话术"与"出了交付卡"永远同真同假。卡在这两种 outcome 下按只读区间卡呈现:可看候选、可选采用(若 `selection_allowed`),不得反过来把 `can_adopt` 提权。 |
|
||||
| D4 | **补终态。** 缺口状态机新增 `delivered`:`current_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 error;`npm test` 失败清单与基线逐条一致(本机基线 27 条,全是无 Docker / DB / rsync 缺口;工作树缺 `.venv` 软链会多一条 workflow YAML 假红);`next build` 通过且 `/` 仍 `○ Static`(Turbopack 拒软链 `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/16,`refresh_attempts` 为空),`persistNextInterviewIfIdle` 的决策与 `decideFromDossier` 的决策**必须一致**——要么都交付,要么都继续收集。这条是本单的防复发锚点。
|
||||
- 行为单测:并列到顶时 `decideRectification` 返回 `completeWithRange`,`stop_reason = tied_first`,`can_offer_range = true`。
|
||||
|
||||
### 任务 2 · 能说就能画(BUG-681,P0)
|
||||
|
||||
- `core/rectification-decision.ts`:新增 `DELIVERY_OUTCOMES`(= `ADOPT_OUTCOMES` ∪ `{ completed_with_range, provisional_range }`),导出。
|
||||
- `rectification-candidate-result.ts:426`:`canShowRectificationSelectionCards` 改用 `DELIVERY_OUTCOMES`;`canShowRectificationReadonlyRange` 相应排除交付 outcome(避免同屏既出卡又出"再说一件…")。
|
||||
- 验收标准:
|
||||
- 行为单测:outcome = `completed_with_range`、`selection_allowed = true`、`can_adopt = false` 时,`canShowRectificationSelectionCards` 为真,`canShowRectificationReadonlyRange` 为假。
|
||||
- 行为单测:`deliveryNarrationAllowed` 为真的每一种 outcome,`canShowRectificationSelectionCards` 都为真(用表驱动遍历所有 outcome,锁死"同真同假")。
|
||||
|
||||
### 任务 3 · 补终态(BUG-682,P1)
|
||||
|
||||
- `rectification-surface-state.ts`:`rectificationQuestionGapState` 新增 `delivered` 分支(排在 `collectWaiting` 之前),条件:无题 + outcome ∈ 交付集合,或 `stop_reason ∈ { tied_first, user_uncertainty_too_high }`。
|
||||
- 组件按该状态显示区间与出口说明(文案对照 `frontend/docs/VOICE.md`,不得出现「没有拿到下一个问题」「请稍候」)。
|
||||
- 验收标准:行为单测——该组合下 `rectificationQuestionGapState` 返回 `delivered`,不是 `unavailable`;普通"题没取到"仍返回 `unavailable`(不得误伤修复入口)。
|
||||
|
||||
### 任务 4 · 记录
|
||||
|
||||
- `docs/BUG_HISTORY.md`:BUG-680 / 681 / 682 三条 `resolved`。BUG-680 的防复发必须写明:**任何"是否已尝试/已耗尽"的标志,落库失败时一律按未完成处理;POST 与 GET 对同一状态的决策必须一致,并有一条对拍测试守着。**
|
||||
- `CHANGELOG.md` 一行;`PROGRESS-rectification-delivery-vs-collect-20260914.md`;`docs/testing/` 真机清单(要覆盖:并列到顶时给出区间交付卡;不再出现「没有拿到下一个问题」;同屏不再既出卡又出"再说一件带年月的事")。
|
||||
|
||||
## 6. 让步顺序
|
||||
|
||||
1. 任务 1、任务 2 必做——缺任一条用户都会看到"有结论没有卡"。
|
||||
2. 任务 3 若文案需要产品确认,可先用既有区间文案,把待确认项写进进度记录,但**不得**保留「没有拿到下一个问题」。
|
||||
3. D2 的顺序调整若牵动其他用例,最小改动只对 `tied_first` 生效,其余 `exhausted` 原因保持原样。
|
||||
|
||||
## 7. 开工前置命令
|
||||
|
||||
```bash
|
||||
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` 实际最大号为准)。
|
||||
Reference in New Issue
Block a user