Files
Jyotisha/docs/tasks/TASK-consult-pass4-streaming-20260918.md
T
Jesse_ChenandClaude Opus 5 24a6fc9873 docs(tasks): e32ce624 验收通过;review 出 BUG-950~953 第三轮任务书
产品拍板:正文按句放行恢复流式(950);无出生分钟模式改按句丢弃、
全丢才用兜底句(951);该模式日期记 observe(952);校正流 token 级
thinking 是死链,按 P2 删除并把测试翻转成否定合同(953)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-18 05:39:05 +00:00

114 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK · Pass 4 按句放行与思考死链清理(2026-09-18 第三轮)
> 基线:`origin/staging` @ `1e553976`(含 `e32ce624`,BUG-945~949 已由 Claude 验收通过)。
> 前序:`TASK-consult-three-channels-20260918.md` → `TASK-consult-three-channels-fix-20260918.md`。
> BUG 编号起点:开工时 `docs/BUG_HISTORY.md` 最大号为 **BUG-949**,本单占 **BUG-950 ~ BUG-953**。
> 本单全部来自 `e32ce624` 的验收 review,不重开设计。
## 0. 决策记录(产品负责人 2026-09-18 授权)
1. **正文必须逐字流式**。「等 Pass 4 判完再整段发」不可接受。产品原话:「不能直接流式动画输出吗,那就按句放行」。→ BUG-950。
2. **无出生分钟模式的拒绝改成按句丢弃**,不再整段替换。产品已同意。→ BUG-951。
3. **无出生分钟模式下的具体日期要留痕**。→ BUG-952。
4. 校正流的 token 级 thinking 通道按 P2「provider reasoning 永不外发」**删除**,不是接上。→ BUG-953。
## 1. BUG-950(P1)正文不再逐字出现,整段一次性蹦出来
**实证**(`1e553976` 实跑,三个 `text-delta` 喂进 `streamAgentResponse`):
```
不设 pass4Mode → answer.delta 3 条:「第一句话。」「第二句话。」「第三句话。」
verified_chart → answer.delta 1 条:「第一句话。第二句话。第三句话。」
declared_birth_window → answer.delta 1 条:同上
```
根因:`frontend/src/lib/stream-agent-response.ts` 的 `holdAnswer` 一旦为真,`flushHeld` 就不再 `send`,全部攒进 `fullOutput`,等 `finishPass4` 一次性发出。而 `route.ts:1005 / 1105 / 1228` 三条路径**全部**设了 `pass4Mode`,所以本命、窗口、一般对话的正文都变成整段出现,首字延迟 = 整段写完的时间。`PROGRESS-consult-three-channels-fix-20260918.md` §948 让步 1 记了「正文 hold 到判定结束再发,避免闪两次」,但没记这等于关掉打字机。
**要求:按句放行。**
1. 只缓冲**当前这一句**(到 `。!?\n` 为止),句子闭合就对这一句跑 `classifyPass4`:
- 没有 `reject` → 立刻 `send` 这一句,恢复流式;
- 有 `reject` → 这一句从不发出(用户看不到闪动),按 §2 的规则处理。
2. `exact-timing` 是 `observe`,**不得**因此阻塞发送。
3. `methodology`(统一参数 / 技法审计表)是整篇结构问题,命中时仍可整篇退回重写——但重写只允许发生在**还没发出任何正文**之前;已经发出过句子就不再整篇重写,改为丢弃后续命中句。这条是「不闪两次」与「要流式」的分界线,必须写进代码注释。
4. 流末 `flush`:不以句号结尾的残句照样过一次 `classifyPass4` 再决定发不发。
5. 验收标准:
- 新增合同测试——三个 `text-delta` 在 `pass4Mode: "verified_chart"` 下产出 **≥3 条** `answer.delta`(现在是 1 条);
- 含保证句的那一句不出现在任何 `answer.delta` 里,且回执有 `pass4-reject:guarantee`;
- 含日期的句子照常逐句发出,回执有 `pass4-observe:exact-timing`;
- `e32ce624` 新加的三条 Pass 4 测试改成按句口径,写「原值 / 新值 / 原因」三栏。
## 2. BUG-951(P1)没有出生分钟时,整段回答被一句拒绝顶掉
`frontend/src/lib/timing-output-guard.ts` 的 `applyPass4Policy`:
```ts
if (options?.secondPass && mode === "general_no_birth_time" && rejects.some(...personal-chart)) {
next = GENERAL_NO_BIRTH_TIME_REFUSAL; // 整段丢弃
}
```
而 `route.ts:1005` 的一般路径**没有 `composeAnswer`**,`finishPass4` 里 `report.retry && options.composeAnswer` 不成立,于是直接走 `else if (report.retry)` 落二次——**没有重写机会**,一句个人盘断言就把整段回答换成「这部分需要具体出生分钟才能判断,我不会补造时间」。
旧实现(`createBirthTimeModeOutputGuard`)在这点上更好:只挖掉个人盘句、保留一般知识句。被 `e32ce624` 改掉的那条测试原名就是 `... while preserving general knowledge`。**这是我方修复单 §4c 第 2 条「兜底句替代整段」措辞造成的,执行方照做无过。**
**要求**:
1. 二次落地改成**按句丢弃**(`dropGuaranteeClauses` 已经是这个形状,抽成通用的 `dropRejectedClauses(text, mode)`):只丢命中 `guarantee` / `personal-chart` 的句子,其余原样保留。
2. **只有当所有句子都被丢掉、正文为空时**,才发 `GENERAL_NO_BIRTH_TIME_REFUSAL` 兜底句。
3. 保证句在无 `composeAnswer` 的路径上被静默删句——这一点保持(不加「有一句被省略」之类的提示),但回执必须有 `pass4-reject:guarantee` 可查。
4. 验收标准:把 `e32ce624` 删掉的那条断言**恢复**——混合文本(一般知识句 + 个人盘句 + 保证句)过 Pass 4 后,一般知识句仍在,个人盘句与保证句不在,且不出现 `。。` 这类残留标点;三栏说明写「原值 / 新值 / 原因」。
## 3. BUG-952(P2)没有出生时间时的具体日期既不记录也不拦
`classifyPass4` 对 `exact-timing` 的分支是:
```ts
if (mode !== "general_no_birth_time") steps.push({ action: "observe", ... });
continue;
```
即**恰恰在最没有依据给日期的模式下**,日期一条痕迹都不留。
**要求**:`general_no_birth_time` 下 `exact-timing` 记 `observe`(不拦,保持产品口径:日期不删字)。验收:该模式下含日期的正文原样通过,且回执有 `pass4-observe:exact-timing`。
## 4. BUG-953(P3)校正流的 token 级 thinking 是死链,按 P2 删掉
`toPublicThinkingDelta` / `mapStreamChunkToThinking`(`frontend/src/lib/rectification-agentic/v9/stream-mapping.ts:189-199`)**在生产代码里没有任何调用方**,只有测试在用。校正 v9 运行时对 `reasoning-delta` 的真实处理是 `step-answer.ts:165-168` 的 `return { kind: "none" }`——直接丢弃。`public-receipt.ts:8` 的模块注释写得很清楚:确定性 public phase「replaces token-level thinking on the browser stream」。
也就是说:`e32ce624` 的 BUG-947 修好的是一条没人走的路。修得对(测试该绿),但结论应当是**删除**而不是保留——上游任务书 P2 的原则是「provider 的 reasoning token 永不外发,展示给用户的思考是产品产物(Pass 2 条目),不是 CoT 抓取」,把 reasoning 分片推给浏览器本身就违反 P2。
**要求**:
1. 删除 `toPublicThinkingDelta`、`mapStreamChunkToThinking`、`InternalThinkingDeltaEvent`,以及 `think-step-gate.ts` 里只服务于它们的 `acceptThinkingFragment`、`createThinkingFragmentAssembler`。`acceptThinkStepText`(Pass 2 条目门)保留。
2. 把 `frontend/tests/rectification-step-answer.test.ts:44`(现在断言「reasoning 能变成 thinking 行」)翻转成**否定合同**:校正流对 `reasoning-delta` 的处理必须是丢弃,源码里不得出现把 `reasoning-delta` 映射成对外事件的函数。`rectification-v9-stream.test.ts` 的两条同类断言一并翻转。
3. 将来校正面板若要显示判断依据,走咨询流同一条路(Pass 2 产品化条目 `think.step`),任务书里另立单;本单不实现。
4. 验收:全仓 grep 不到这几个函数名;测试总数不得下降(翻转不是删除)。
## 5. 硬红线
1. `tsc --noEmit` 0 错、`npm run lint` 0 error、`npm test` 失败数**不得超过基线 `1e553976` 实测的 31 条**,且失败清单逐条一致。
2. 测试总数不得低于 3491。
3. 改既有断言写「原值 / 新值 / 原因」三栏;不得用弱化断言换绿。
4. 不得回到「先混流再过滤」:按句放行是**门**(整句发或整句不发),不许对句子内部动刀。
5. `next build` 后 `/` 仍 `○ Static`,首屏 js gzip 相对 468,388 B 变化在 ±2% 内。
## 6. 让步顺序
1. BUG-950 优先(用户每轮都感知)。
2. BUG-951 紧随(与 950 同在 `applyPass4Policy` / `finishPass4`,同一轮做,避免两次改同一函数)。
3. BUG-952 是 950/951 的一行分支,顺手做。
4. BUG-953 与前三条文件不重叠(只碰 `stream-mapping.ts` / `think-step-gate.ts` 与三个测试文件),可并行,也可放最后。
## 7. 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/consult-pass4-streaming-20260918 \
.worktrees/consult-pass4-streaming-20260918 origin/staging
cd .worktrees/consult-pass4-streaming-20260918/frontend
npm test 2>&1 | grep -E "^# (tests|pass|fail)" # 开工基线:tests 3491 / pass 3445 / fail 31
```
收工:`docs/tasks/PROGRESS-consult-pass4-streaming-20260918.md` + `docs/BUG_HISTORY.md`(BUG-950~953)+ `CHANGELOG.md`(正文恢复逐字出现属用户可感知),与代码同一批推 `staging`。