docs: BUG-1051 record, progress, real-device checklist, changelog and board row

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-26 22:20:56 +08:00
co-authored by Claude Opus 5.5
parent 1530a0dd63
commit 1dd51f2d42
5 changed files with 168 additions and 0 deletions
+23
View File
@@ -14104,3 +14104,26 @@
- 相关记录:BUG-1047、BUG-1049、BUG-725。
- 复发自:无。
- 修复版本:分支 `codex/rectification-opening-plain-20260926`(本地提交,未推)。
## BUG-1051 | 普通咨询回答写到一半被掐断,仍按完成扣点、不提示未完成
- 状态:resolved(本地修复,真实 Mastra 流回归测试通过;未推送、未部署,真机清单 `docs/testing/consult-answer-truncation-20260926.md` 待走)
- 首次发现 / 最近更新:2026-09-26 / 2026-09-26
- 影响面:`POST /api/consult` 的三条 agentic 路径(本命、申报时段、无出生分钟);`frontend/src/lib/stream-agent-response.ts` 的 `consumeAttempt` / `continueCurrentAnswer` / `finishPass4` / 结算段;`frontend/src/mastra/consultation-tools.ts` 时间常数;`frontend/src/lib/agent-observability.ts`;`frontend/src/app/api/consult/route.ts` 的 `composeAnswer` / `continueAfterLength` / `retryForAnswer`。
- 用户现象:staging `e53052a2`(2026-09-26 21:24 CST)一次普通咨询,回答停在一个二级标题之后的半句上;活动区写「已完成 7 步」,没有出现「回答未完成,已保留现有内容;本次不会扣点。」,会话按完成保存并扣点。
- 触发条件:本命路径。工具循环(思考、读 Skill、约 31 秒/领域的计算,共 7 步)先用掉 110 秒闸门的大部分,写回答(compose)只剩几秒。
- 根因:三层。
1. 共用闸刀:`route.ts` 只建一个 `AbortSignal.timeout(AGENT_TIMEOUT_MS)`(110 秒),工具循环、`composeAnswer`、续写和回答重试全都展开同一个 `streamOptions`。BUG-944 的防复发写的是「每个 stream 自己的预算」,但那一轮只取消了分段写作,一次成文的 compose 仍然共用这把闸刀。
2. Mastra 1.50.1 超时不抛错:signal 触发时先入队 `{ type: "abort" }`,再发 `finish`(reason `tripwire`),然后正常关流(`@mastra/core/dist/chunk-OE4IEL7C.js` 约 27360 / 27452 行)。`stream-agent-response.ts` 的 `mapChunk` 忽略 `abort`,abort 运行步只在 `catch` 里记,`continueCurrentAnswer` 只认 `length`,于是流程走到 `onComplete`(扣点并按完成落库)和 `run.completed`;`finishPass4` 还把 Pass 4 缓冲里的半句当最后一句发了出去。
3. 同一缺口也覆盖其他非 `stop` 的结束:`content-filter`、`tool-calls`(compose 是 `toolChoice: "none"`、1 步)、`other`、`error`,以及供应商没发 finish(实测 Mastra 仍补一个 reason 为空的 `finish`,归一为 `unknown`)。只要有可见正文,都会被当成完成。
- 为什么 BUG-305 的测试没拦住:BUG-305 的超时回归(`consultation-agentic-runtime.test.ts`「a timeout after partial visible text…」)手工 `throw new DOMException("…", "TimeoutError")`。这不是 Mastra 的真实流形状(违反 AGENTS §7.4):真实超时根本不抛错,永远进不了 `catch`,所以测试一直绿,线上照样扣点。BUG-944 的防复发只写在文字里,没有测试锁住「compose 不与工具循环共用 signal」。BUG-612 在 staging 日志里已经见过同一形状(`modelFinishReason=tripwire`、墙钟约 110 秒),当时按「模型改领域、分段再调工具」处理,没有追到 Mastra 的 abort 不抛错。
- 修复(产品 2026-09-26 决策 D1–D3):
- D1 写回答阶段自有时钟:新增 `CONSULTATION_COMPOSE_TIMEOUT_MS = 70_000` 与 `createConsultationAnswerClock()`(第一次调用时才开始计时,同一轮后续的续写、Pass 4 重写、回答重试共用这一个 signal)。`composeAnswer`、三条路径的 `continueAfterLength` 与 `retryForAnswer` 都改用它;工具循环和工具本身仍用 110 秒的 `agentAbortSignal`。最坏总等待 110 + 70 = 180 秒;路由 `maxDuration` 120 → 240(自托管 `node server.js` 不执行该值,只作上限说明)。70 秒的依据见 PROGRESS。
- D2 结算只认 `stop`:每次 attempt 记录自己的 finish reason 与是否收到 Mastra `abort` 块;写回答的最后一次 attempt 不是 `stop`(abort/tripwire、content-filter、tool-calls、other/unknown/error、没有 finish)而正文非空时,不调用 `onComplete`、不发 `run.completed`,改为 `run.failed` / `answer_truncated`,保留已流出的正文,账务走 `cancel`,客户端沿用既有提示。Mastra `abort` 块记 `kind: "abort"` 运行步(工具循环 `tool-abort`、写回答 `compose-abort`),另记 `answer-truncated` 校验步,回执不再显示全部成功。`length` 仍先续写一次;续写本身也被掐或仍停在 `length` 时同样按截断处理。被掐的流不再把 Pass 4 缓冲里的半句冲出去。
- D3 观测:`[agent-observability]` 新增 `composeFinishReason`(封闭枚举 + `missing`)、`composeAborted`(布尔)、`answerVisibleChars`(计数),只有枚举和数字,不含正文。公开回执仍不带 `modelFinishReason`(BUG-305 规则)。
- 验证:新增 `frontend/tests/consult-answer-truncation-20260926.test.ts`(15 条,全部用真实 Mastra `Agent` + 假模型产生的流):共享闸刀在正文中途触发 → `answer_truncated`、`onComplete` 未调用、有 `compose-abort` 步、半句不外发;工具循环的 signal 已过期时 compose 用自有时钟照常完成(对照组:沿用过期 signal 则不完成);答案时钟首用才起算、全阶段共用;`content-filter` / `tool-calls` / `other` / `unknown` / `error`、供应商无 finish、流无 finish 块各一条 → 截断;`length` 续写后 `stop` → 完成并扣点;续写被掐 → 截断;正常 `stop` → 完成并扣点一次;观测字段通过严格 schema 且不含正文;路由源码合同(compose、3 处续写、3 处回答重试都用答案时钟,110 + 70 ≤ 180 且小于 `maxDuration`)。修复前 11 / 15 条失败。BUG-305 旧用例保留并加三栏说明、补 abort 步断言;11 条手造「无 finish」的既有 fixture 补上 Mastra 真实流必有的 `finish(stop)`(断言未改)。数字(全量、Python 门、构建、gzip)见 `docs/tasks/PROGRESS-consult-answer-truncation-20260926.md`。
- 未做:活动区标题「已完成 N 步」按时间线行数计,截断回复上仍会这样写(真正的信号是输入框上方的未完成提示);改它属于 UI 改动,未在本单范围。真实供应商下 compose 的实际耗时与 `composeFinishReason` 分布需部署后用新埋点复核。
- 防复发:结算只认 `finish = stop`,任何新的写回答流都要经过同一判定。流的超时 / 中止回归一律用真实 Mastra `Agent` + 假模型产生的流(abort 块 + `finish(tripwire)`),不得手工 `throw`。每个新加的模型流要有自己的时间预算,并用源码合同锁住它不与工具循环共用 signal。
- 相关记录:BUG-305(原记录)、BUG-944(「每个 stream 自己的预算」未落到 compose)、BUG-612(同一 tripwire / 110 秒形状)、BUG-280(回答重试没有独立时间预算,本单一并挂到答案时钟上)。
- 复发自:BUG-305(半截回答被当成功并扣点;原防线只拦「抛出的超时」与 `length`,没拦 Mastra 不抛错的 abort)。
- 修复版本:分支 `codex/consult-answer-truncation-20260926`(本地提交,未推送)。