降级正文直接 send,不过 Pass 4:实跑「我保证你一定会升职」原样送达、 回执只有 contract-degraded(959)。uncontractedText 跨 attempt 不清零, 两次 attempt 的正文拼一起送出(960)。降级后仍空跑一轮 compose(961)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
4.9 KiB
TASK · 降级交付路径补 Pass 4 与去重(2026-09-18 第五轮)
基线:
origin/staging@877128ce(含5b6abc23,BUG-954~956/958 已由 Claude 验收通过)。 前序:TASK-window-consult-contract-20260918.md。 BUG 编号起点:开工时最大号为 BUG-958,本单占 BUG-959 ~ BUG-961。 全部来自5b6abc23的验收 review,都落在 BUG-956 新增的降级交付路径上,不重开设计。
0. 决策记录
BUG-956 的原始要求是「合同未绿但模型写了字,就把字交付出去,别让用户白等一场只看到系统错误」。这条不变。本单补的是它漏掉的两件事:降级正文同样要过 Pass 4,以及两次 attempt 的正文不许拼在一起送出。
TASK-window-consult-contract-20260918.md §4 只写了「交付该正文 + 服务端说明 + 回执追加 contract-degraded」,没写「先过 Pass 4」,实现照做无过。本单把口径补齐。
1. BUG-959(P1)降级正文绕过 Pass 4
实证(877128ce 实跑 streamAgentResponse,requireTool: true、pass4Mode: "verified_chart"、模型不调工具只写字):
输入:「我保证你一定会升职。」「方向上可以推进。」
送达 answer.delta:「我保证你一定会升职。方向上可以推进。」+ 降级说明句
state.steps 里的 pass4 相关:只有 contract-degraded,没有 pass4-reject:guarantee
根因:stream-agent-response.ts:717-731 的降级分支直接 send(controller, { type: "answer.delta", text: uncontractedText }),没有走 releasePass4Sentences / classifyPass4。
风险等级高于普通路径:降级正文是模型在没有任何计算依据时自己写的,保证性结论、无出生分钟的个人盘断言恰恰最容易出现在这里,而这条路径上 Pass 4 完全不生效。
要求:
- 降级正文按句过 Pass 4(复用
releasePass4Sentences,而不是另写一遍分句逻辑):命中reject的句子丢弃,其余交付;observe照记。 - 全部句子都被丢弃时:
general_no_birth_time用GENERAL_NO_BIRTH_TIME_REFUSAL,其余模式维持runtime_contract_incomplete(不扣点),不得发一句只有降级说明的空回答。 - 降级说明句(
CONTRACT_DEGRADED_NOTE)是服务端确定性文案,不过 Pass 4,仍然附在末尾。 - 验收标准:新增测试——降级路径 +
pass4Mode+ 含保证句的正文 → 保证句不出现在任何answer.delta,回执同时有contract-degraded与pass4-reject:guarantee;e32ce624之后的既有降级测试补上pass4Mode参数。
2. BUG-960(P1)两次 attempt 的正文被拼起来一起送出
实证:同一次运行里,第一次 attempt 与合同 retry 的正文都进了 uncontractedText,降级时一起送出,用户看到同一轮的内容被说了两遍。
根因:stream-agent-response.ts 的 uncontractedText += text 只在 contractReady 为真时清零(uncontractedText = "")。合同一直红就一路累加,attempt 之间没有边界。
要求:每次 attempt 开始时清空 uncontractedText,降级只交付最后一次 attempt 的正文。验收:两次 attempt 各写一段不同文字的假流 → 降级正文只含第二段。
3. BUG-961(P2)降级之后还会空跑一轮 compose
实证:降级分支执行后,流程继续走到 publishFindings / composeOnce;本命形状的事件序列里出现 activity → phase.started → phase.completed,但该轮 compose 的产出没有送达用户。等于白花一次模型调用与一段墙钟。
要求:降级交付后直接收口(发 run.completed 前不再进入 Pass 2 / Pass 3)。验收:降级路径的事件序列里不再出现 phase.started{phase:"compose"};composeAnswer 在该路径上零次调用。
4. 硬红线
tsc --noEmit0 错、npm run lint0 error、npm test失败数不超过基线877128ce实测的 31 条且清单逐条一致;测试总数不低于 3501。- 不得为了让降级路径通过而放宽
contractReady()。 - 降级交付仍然只在「模型写了字、但一次计算都没成功」时发生;模型既没调工具也没写字,维持
runtime_contract_incomplete不扣点。 - 不得在降级正文上做句内改写——按句丢弃是门,不是刀。
- 改既有断言写「原值 / 新值 / 原因」三栏。
5. 开工前置
git fetch origin --prune
git worktree add -b codex/contract-degraded-pass4-20260918 \
.worktrees/contract-degraded-pass4-20260918 origin/staging
cd .worktrees/contract-degraded-pass4-20260918/frontend
npm test 2>&1 | grep -E "^# (tests|pass|fail)" # 开工基线:tests 3501 / pass 3455 / fail 31
收工:docs/tasks/PROGRESS-contract-degraded-pass4-20260918.md + docs/BUG_HISTORY.md(BUG-959~961)+ CHANGELOG.md(若降级文案有变),与代码同一批推 staging。