# 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 完全不生效。 **要求**: 1. 降级正文按句过 Pass 4(复用 `releasePass4Sentences`,而不是另写一遍分句逻辑):命中 `reject` 的句子丢弃,其余交付;`observe` 照记。 2. 全部句子都被丢弃时:`general_no_birth_time` 用 `GENERAL_NO_BIRTH_TIME_REFUSAL`,其余模式维持 `runtime_contract_incomplete`(不扣点),不得发一句只有降级说明的空回答。 3. 降级说明句(`CONTRACT_DEGRADED_NOTE`)是服务端确定性文案,不过 Pass 4,仍然附在末尾。 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. 硬红线 1. `tsc --noEmit` 0 错、`npm run lint` 0 error、`npm test` 失败数不超过基线 `877128ce` 实测的 31 条且清单逐条一致;测试总数不低于 3501。 2. 不得为了让降级路径通过而放宽 `contractReady()`。 3. 降级交付仍然只在「模型写了字、但一次计算都没成功」时发生;模型既没调工具也没写字,维持 `runtime_contract_incomplete` 不扣点。 4. 不得在降级正文上做句内改写——按句丢弃是门,不是刀。 5. 改既有断言写「原值 / 新值 / 原因」三栏。 ## 5. 开工前置 ```bash 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`。