产品拍板:正文按句放行恢复流式(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
8.7 KiB
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 授权)
- 正文必须逐字流式。「等 Pass 4 判完再整段发」不可接受。产品原话:「不能直接流式动画输出吗,那就按句放行」。→ BUG-950。
- 无出生分钟模式的拒绝改成按句丢弃,不再整段替换。产品已同意。→ BUG-951。
- 无出生分钟模式下的具体日期要留痕。→ BUG-952。
- 校正流的 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 到判定结束再发,避免闪两次」,但没记这等于关掉打字机。
要求:按句放行。
- 只缓冲当前这一句(到
。!?\n为止),句子闭合就对这一句跑classifyPass4:- 没有
reject→ 立刻send这一句,恢复流式; - 有
reject→ 这一句从不发出(用户看不到闪动),按 §2 的规则处理。
- 没有
exact-timing是observe,不得因此阻塞发送。methodology(统一参数 / 技法审计表)是整篇结构问题,命中时仍可整篇退回重写——但重写只允许发生在还没发出任何正文之前;已经发出过句子就不再整篇重写,改为丢弃后续命中句。这条是「不闪两次」与「要流式」的分界线,必须写进代码注释。- 流末
flush:不以句号结尾的残句照样过一次classifyPass4再决定发不发。 - 验收标准:
- 新增合同测试——三个
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:
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 条「兜底句替代整段」措辞造成的,执行方照做无过。
要求:
- 二次落地改成按句丢弃(
dropGuaranteeClauses已经是这个形状,抽成通用的dropRejectedClauses(text, mode)):只丢命中guarantee/personal-chart的句子,其余原样保留。 - 只有当所有句子都被丢掉、正文为空时,才发
GENERAL_NO_BIRTH_TIME_REFUSAL兜底句。 - 保证句在无
composeAnswer的路径上被静默删句——这一点保持(不加「有一句被省略」之类的提示),但回执必须有pass4-reject:guarantee可查。 - 验收标准:把
e32ce624删掉的那条断言恢复——混合文本(一般知识句 + 个人盘句 + 保证句)过 Pass 4 后,一般知识句仍在,个人盘句与保证句不在,且不出现。。这类残留标点;三栏说明写「原值 / 新值 / 原因」。
3. BUG-952(P2)没有出生时间时的具体日期既不记录也不拦
classifyPass4 对 exact-timing 的分支是:
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。
要求:
- 删除
toPublicThinkingDelta、mapStreamChunkToThinking、InternalThinkingDeltaEvent,以及think-step-gate.ts里只服务于它们的acceptThinkingFragment、createThinkingFragmentAssembler。acceptThinkStepText(Pass 2 条目门)保留。 - 把
frontend/tests/rectification-step-answer.test.ts:44(现在断言「reasoning 能变成 thinking 行」)翻转成否定合同:校正流对reasoning-delta的处理必须是丢弃,源码里不得出现把reasoning-delta映射成对外事件的函数。rectification-v9-stream.test.ts的两条同类断言一并翻转。 - 将来校正面板若要显示判断依据,走咨询流同一条路(Pass 2 产品化条目
think.step),任务书里另立单;本单不实现。 - 验收:全仓 grep 不到这几个函数名;测试总数不得下降(翻转不是删除)。
5. 硬红线
tsc --noEmit0 错、npm run lint0 error、npm test失败数不得超过基线1e553976实测的 31 条,且失败清单逐条一致。- 测试总数不得低于 3491。
- 改既有断言写「原值 / 新值 / 原因」三栏;不得用弱化断言换绿。
- 不得回到「先混流再过滤」:按句放行是门(整句发或整句不发),不许对句子内部动刀。
next build后/仍○ Static,首屏 js gzip 相对 468,388 B 变化在 ±2% 内。
6. 让步顺序
- BUG-950 优先(用户每轮都感知)。
- BUG-951 紧随(与 950 同在
applyPass4Policy/finishPass4,同一轮做,避免两次改同一函数)。 - BUG-952 是 950/951 的一行分支,顺手做。
- BUG-953 与前三条文件不重叠(只碰
stream-mapping.ts/think-step-gate.ts与三个测试文件),可并行,也可放最后。
7. 开工前置
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。