Files
Jyotisha/docs/tasks/TASK-contract-degraded-pass4-20260918.md
T
Jesse_ChenandClaude Opus 5 895a85bcbe docs(tasks): 5b6abc23 验收通过;降级路径 review 出 BUG-959~961
降级正文直接 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
2026-09-18 09:10:48 +00:00

4.9 KiB
Raw Blame History

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. 开工前置

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。