降级正文直接 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
68 lines
4.9 KiB
Markdown
68 lines
4.9 KiB
Markdown
# 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`。
|