diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 7f11a722..8b0172f6 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -44,7 +44,8 @@ | `TASK-rectification-adopt-flow-20260902.md` | — | 采用流程整治 | 已验收 | `35e5781e`(BUG-497~500) | | `TASK-rectification-adopt-flow-fix-20260903.md` | — | 采用流程修复单(含删「用这个时间看盘」) | 已验收 | `e8c98c37`(BUG-501/502) | | `TASK-rectification-uncertainty-stop-20260903.md` | — | 不确定度停止规则加样本下限 | 已验收 | `0c0df426`(BUG-503) | -| `TASK-rectification-adopt-narration-20260904.md` | `PROGRESS-rectification-adopt-narration-20260904.md` | 探针池耗尽时采用卡旁白改 Agent 生成 + 已丢弃探针绕过 BUG-472 早退 + 区分题答否关线 | 待验收 | 同步后基线 `cfa82499`,BUG-519 / BUG-520 | +| `TASK-rectification-adopt-narration-20260904.md` | `PROGRESS-rectification-adopt-narration-20260904.md` | 探针池耗尽时采用卡旁白改 Agent 生成 + 已丢弃探针绕过 BUG-472 早退 + 区分题答否关线 | 已验收(4.1.4 点选入口未通过,见修复单) | `0aaa0d70`(BUG-519/520) | +| `TASK-rectification-adopt-narration-fix-20260904.md` | — | 采用旁白 Agent 在点选入口从不运行(早退分支重算过期决策)+ 校验器/可观测/超时 | 待执行 | 基线 `0aaa0d70`,BUG-521 起 | | `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) | ### 聊天主链路与首页 diff --git a/docs/tasks/TASK-rectification-adopt-narration-fix-20260904.md b/docs/tasks/TASK-rectification-adopt-narration-fix-20260904.md new file mode 100644 index 00000000..fd77c532 --- /dev/null +++ b/docs/tasks/TASK-rectification-adopt-narration-fix-20260904.md @@ -0,0 +1,109 @@ +# 修复单 · 采用旁白 Agent 在点选入口从不运行(2026-09-04) + +基线:`origin/staging` `0aaa0d70`(`TASK-rectification-adopt-narration-20260904.md` 的实现)。本单只改 `answer-choice.ts` 与新测试,不动 Agent 提示词以外的决策逻辑。 + +## 0. 验收结论(对照原任务书) + +| 项 | 结论 | 证据 | +| --- | --- | --- | +| 4.1.1 nakshatra 计划/决策同口径,无 frame 的 distinguish 不进 `deferred_followup` | 通过 | `rectificationFollowupCatalog` 过 `inspectDiscriminatorProbes`;计划层兜底丢进 `dropped_probes(not_renderable)` | +| 4.1.2 `isRemainingDiscriminatorFollowup` / `interviewToPersist` 忽略无 frame distinguish | 通过 | 代码 + 14 探针 fixture | +| 4.1.3 `persistableFocusDomain` 非白名单返回 null,appearance 零 RPC;horary 断言三栏说明 | 通过 | `rectification-server-focus`、`rectification-collect-stall` | +| 4.1.4 采用旁白 Agent + 模板兜底,三条入口都接 | **未通过(P1)** | 见 §1;点选入口 Agent 从不运行,且模板改用了过期决策 | +| 4.1.5 采用轮 `current_question` 为 null、零次 set_focus | 通过 | 新测试 `assertNoFocusWrite` | +| 4.2 区分题答否不关整条领域 | 通过 | `declinedDomains` 按 intent;`dossierWithClosedFocus` 带 intent;BUG-520 | +| `rectification-collect-stall` live fixture 补 nakshatra + window_scan 后原断言仍过 | 通过 | 本地重跑 | +| tsc / lint / 定向套件 | 通过 | `tsc` 0 错;lint 0 error 74 warning(既有);`rectification-* / agentic-rectification-* / birth-time-*` 869 条 fail=0 | +| 全量 `tests/*.test.ts` | 环境缺口 | 2638 条,fail 25 全部为 `docker ENOENT` 的 DB / 部署 / 备份套件,与无 Docker 基线一致;无一条校正相关 | +| BUG-519 / BUG-520、CHANGELOG、docs/testing 清单 | 通过 | 无案例 ID、无用户资料 | + +## 1. 事故实证(P1 · 点选入口 Agent 从不运行) + +`answer-choice.ts` `persistNextInterviewAfterChoice` 的采用早退分支: + +```ts +const decision = decideFromDossier(input.dossier, { birthDate }); +const facts = adoptDeliveryFacts(decision, input.dossier); +const fallback = adoptHostNarration({ dossier: input.dossier, decision, ... }); +``` + +点选路径(`applyRectificationChoice` → `persistApplied`)传进来的 `input.dossier` 是**点选前**加载的 dossier,其 `latestResult.decisionReceipt.inference_state` 还是上一轮;本轮新状态在 `input.decisionState`,本轮决策在 `input.nextAction`(`persistApplied` 手里还有完整的 `nextDecision`)。函数开头已经把两者合成了 `latest`,但早退分支没有用它,而是从旧 dossier 重新 `decideFromDossier`: + +- 旧 dossier 的决策是「还有一道题要问」(`theme_refine` / `ask_candidate_discriminator`,`probe` 非空),`precisionStage !== "ready_to_adopt"` → `shouldWriteAdoptNarration` 为 false → **`narrateAdopt` 直接返回 fallback,模型一次都没调**。 +- fallback 模板的 `credibleRange / representativeTime / stopReason` 也来自这份旧决策,而不是本轮的 `nextAction`。改造前模板用的是 `nextAction.credible_range`,这是回归。 + +本地复现(用 14 探针 fixture,把 dossier 里的 receipt 回退成「最后一道题未答、分数 23/16/7」,`decisionState` 用答完后的状态,`nextAction` 用答完后的决策): + +``` +pre-click decision: theme_refine, probe=true +agent called: false +narration: <模板> +fresh nextAction: offer_provisional_range [05:00, 05:06] 05:00 +``` + +新测试「three adopt entry points call the model once」没拦住,是因为它给点选入口传的 `dossier` 已经是答完后的状态(receipt 与 `decisionState` 相同),与生产的点选路径不一致。 + +用户可感知:**点最后一道区分题**(采用卡最常见的出现方式)时,旁白仍是模板句;只有采集题答「没有」和空闲兜底两条入口才会走 Agent。 + +## 2. 根因 + +早退分支把「决策」当成可以从 dossier 随手再算一遍的东西,违反 BUG-440 防复发条款「同一次调用里出题层与决策层必须用同一个 sessionOutcome」——这里是决策层自己算了两遍、用了两份不同输入。 + +## 3. 决策记录 + +原任务书决策不变。本单不新增产品决策。 + +## 4. 硬红线 + +原任务书 §3 全部沿用。追加: + +1. `persistNextInterviewAfterChoice` 内不得再调用 `decideFromDossier`;决策由调用方传入,或用 `decideAfterInferenceChange({ dossier, state: input.decisionState })` 与 `persistApplied` 保持同一入口。 +2. 「三条入口」测试的点选用例必须用**点选前** dossier + 答完后 `decisionState` + 答完后 `nextAction` 的形状;不得再用已答完的 dossier 冒充。 + +## 5. 任务分解 + +### 5.1 P1 · 早退分支用本轮决策(BUG-521) + +1. `persistNextInterviewAfterChoice` 新增可选入参 `decision?: RectificationDecision`;三条调用方都传:`persistApplied` 传 `nextDecision`,`persistNextInterviewIfIdle` 传已算好的 `decision`,`applyCollectFocusDenial` 传它用来生成 `nextAction` 的那份决策。未传时用 `decideAfterInferenceChange({ dossier: input.dossier, state: input.decisionState, userStopped: false, birthDate })`,不得用 `decideFromDossier(input.dossier)`。 +2. `adoptDeliveryFacts` 与 `adoptHostNarration` 的 dossier 参数改传合成后的 `{ ...input.dossier, latestResult: latest }`(函数开头已有的 `latest`),保证活跃候选与相对支持度是本轮的。 +3. `persistApplied` 里 `adoptionFacts = adoptDeliveryFacts(nextDecision, input.dossier)` 同样改用合成 receipt。 + +验收: +- `rectification-adopt-narration-20260904.test.ts`「three adopt entry points」点选用例改成 §1 的形状:`dossier` receipt 为「最后一道题未答、分数 23/16/7、revision 5」,`decisionState = fourteenProbeState()`,`nextAction = publicNextAction(decideFromDossier(caseDossier()))`。断言:模型被调用 1 次;传给模型的 `facts.representative_minute === "05:00"`、`facts.active_candidates` 的分数为 21/18/9(不是 23/16/7)、`facts.precision_stage === "ready_to_adopt"`。 +- 同一形状下让 fake model 返回非法文本,断言 fallback 模板里的范围与代表分钟来自 `nextAction`(05:00–05:06 / 05:00),并含「分不开 05:00 和 05:06」。 +- 加一条源码级断言:`persistNextInterviewAfterChoice` 函数体内不出现 `decideFromDossier(`。 +- `docs/BUG_HISTORY.md` 新增 BUG-521,复发自 BUG-440(决策层重复计算)。 + +### 5.2 P2 · 校验器把「不是确认」也打回(BUG-522,可与 5.1 同批) + +`validateAdoptNarration` 的 `/确认|精确/` 是全文匹配;提示词只说「不得承诺确认或精确」,模型很自然会写「这不是确认的分钟」——会被整段打回、静默退到模板,Agent 路径在真实环境的命中率会被这条拉低而且没人知道。 + +1. 提示词改成「不要出现『确认』『精确』这两个词」(与校验器同口径)。 +2. 增加一条可观测:Agent 路径的结果写进本轮 run diagnostic / 服务端日志一个枚举字段 `adopt_narration: "agent" | "template:" | "template:model_error" | "template:not_ready"`,不写模型原文。没有这条,部署后没人能证明 Agent 到底有没有写。 +3. `agent.generate` 加超时:`AbortSignal.any([request.signal, AbortSignal.timeout(8000)])`,超时走模板。分类器没有超时是既有缺口,不在本单修,写进 `BLOCKED.md` 或进度记录。 + +验收:fake model 返回「这不是确认的分钟,先用 05:00。」→ 仍打回(校验器不变),但提示词测试断言含「不要出现」;诊断字段在三种结果下各有一条测试;超时用 fake `generateText` 挂起 + 短 timeout 断言走模板且不抛。 + +### 5.3 P3 · `stopFactsFromDropped` 的「婚恋题只覆盖窗口两端」标签 + +`insufficient_candidates || not_renderable` 一律标成婚恋题,而 `not_renderable` 现在也用于 nakshatra。改成按 `semantic_key` 前缀分桶(`relationship.` → 婚恋,其它 → 「有几道题渲染不出来」),或去掉领域词。可推迟。 + +## 6. 让步顺序 + +1. 5.1 必做,否则 4.1.4 在最常见入口上等于没做。 +2. 5.2 的可观测字段与超时优先于提示词改动。 +3. 5.3 可推迟到下一轮。 + +## 7. 开工前置命令 + +```bash +git fetch origin --prune +git worktree add -b codex/rectification-adopt-narration-fix-20260904 .worktrees/rectification-adopt-narration-fix-20260904 origin/staging +cd .worktrees/rectification-adopt-narration-fix-20260904/frontend +./node_modules/.bin/tsc --noEmit +npx tsx --test tests/rectification-adopt-narration-20260904.test.ts tests/rectification-answer-choice.test.ts tests/rectification-collect-stall.test.ts +``` + +## 8. BUG 编号起点 + +截至本单:BUG-520。本单从 **BUG-521** 起;开工时再核对。