diff --git a/CHANGELOG.md b/CHANGELOG.md index 77df54ef..06b8758a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 印度占星 Skill 更新日志 +## 2026-10-01 — 普通对话写回答最多可以等 5 分钟(原来 70 秒) + +- 推理较重的模型(例如 deepseek-v4-pro)写长回答不再在最后一节被截断;5 分钟只用来防模型卡死,不限制回答长度(BUG-1142)。 +- 没填出生分钟的档案,回答也按 5 分钟算,不再被 110 秒的计算时限一起截断。 +- 计算阶段仍是 110 秒,算几个领域不变;生时校正不受影响。Skill 不 bump,不改数据库。 + ## 2026-10-01 — 生时校正:每道题只出现一次,复述变短,回复不再挂「已完成 N 步」(待验收) - 同一道题被重新出题时,以前会出现「题干、题干、选项」,或者只剩一行没选项的题干加「没有拿到下一个问题」;现在屏幕上只有当前这道题出一次题干,旧题什么都不显示(BUG-1135)。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 553ee806..65fe75c1 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -15332,3 +15332,18 @@ - 复发自:无 - 修复版本:研究分支 `codex/rectification-angle-timing-research-20261001`。 + +## BUG-1142 | 普通对话答题时钟 70 s 容不下推理模型;无出生分钟路线只有 110 s 一道闸 + +- 状态:resolved(分支已验收;部署后以 `/api/health` 版本为准) +- 首次发现 / 最近更新:2026-10-01 / 2026-10-01 +- 影响面:普通对话(`/api/consult`,agentic 运行时)写回答的阶段。生时校正有自己的预算(`RECTIFICATION_RUN_BUDGET_MS`),不受影响。 +- 用户现象:选推理较重的模型时,回答写到最后一节被截断,显示「回答未完成,已保留现有内容;本次不会扣点」。 +- 触发条件:10-01 真实模型对比(`docs/testing/consult-plain-answer-20261001-model-runs.md`)中,`deepseek-v4-pro` 父母题从发出到写完 77 s,超过 70 s 答题时钟;`deepseek-flash` 30–41 s,其中约 85% 是推理 token。同日取消了回答字数上限(TASK-consult-plain-answer-20261001 D8),回答平均变长约 50%。另一条:无出生分钟 / 公开日历路线(`usesPublicDailyGeneralAgent`)没有工具,`answerReady` 永远为 false,答题时钟只在续写和重试时才启动,整段模型循环被 110 s 工具时钟管着。 +- 根因:70 s 按非推理写作模型的吞吐(约 90–100 tok/s)定,而回答步骤开着 provider thinking,推理 token 也从同一只钟里扣;无工具的路线没有「拿到计算结果」这个交接点。 +- 修复:`CONSULTATION_ANSWER_TIMEOUT_MS` 70_000 → 300_000(产品 10-01 定 5 分钟,只防卡死、不限长度);`createConsultationRunClock` 加 `answerFromStart`,route 对 `usesPublicDailyGeneralAgent` 的路线从第一步起走答题时钟;`maxDuration` 240 → 480(110 + 300 + 准备与结算)。工具阶段 110 s、领域预算(65 s / 2 个领域,由工具时钟与 45 s 预留推出,不读答题时钟)不变。截断语义不变:答题时钟到点仍是 `answer_truncated`、不扣点(BUG-1051)。 +- 验证:`frontend/tests/consult-answer-clock-20261001.test.ts`(答题时钟 300 s / 工具 110 s / maxDuration 留余量;领域预算仍 65 s、2 个且公式不读答题时钟;`answerFromStart` 时工具钟到点不掐循环、答题钟到点仍掐;route 对无分钟路线接线);两条既有断言按三栏改;全量测试失败名单与基线逐条一致。 +- 防复发:答题时钟的注释写明它只防卡死;下游各层(Caddy 无响应超时、Node 无自定义超时、undici bodyTimeout 300 s 为「两块数据之间」的空闲时间、推理 token 也是流式下发、预留扣点租约 15 分钟)均在 410 s 以上或不按总时长计。 +- 相关记录:BUG-1051、BUG-1053、BUG-944。 +- 复发自:无 +- 修复版本:`codex/consult-answer-clock-20261001` diff --git a/docs/tasks/PROGRESS-consult-answer-clock-20261001.md b/docs/tasks/PROGRESS-consult-answer-clock-20261001.md new file mode 100644 index 00000000..446ddf05 --- /dev/null +++ b/docs/tasks/PROGRESS-consult-answer-clock-20261001.md @@ -0,0 +1,35 @@ +# PROGRESS · 普通对话答题时钟 5 分钟(2026-10-01) + +## 改动 + +- `frontend/src/mastra/consultation-tools.ts`:`CONSULTATION_ANSWER_TIMEOUT_MS` 70_000 → 300_000,注释改写;`createConsultationRunClock` 新增 `answerFromStart`(创建时即启动答题时钟,工具钟到点因 `if (answer) return` 不再掐循环)。 +- `frontend/src/app/api/consult/route.ts`:`maxDuration` 240 → 480,注释改 410 s;run clock 传 `answerFromStart: usesPublicDailyGeneralAgent(consultationMode, generalDailyContext)`。 +- 申报时段路线:有窗口工具,拿到窗口计算结果时经 `onAnswerPhase` 切答题时钟(原有行为不变);预计算在 warmup 里用工具钟。 +- 新测试 `frontend/tests/consult-answer-clock-20261001.test.ts`(4 条)。 + +## 改动的既有断言 + +| 文件 | 原值 | 新值 | 原因 | +|---|---|---|---| +| `tests/consult-answer-truncation-20260926.test.ts` | `CONSULTATION_ANSWER_TIMEOUT_MS = 70_000`;总和 ≤ 180_000 | `= 300_000`;总和 ≤ 410_000 | 产品 10-01 定 5 分钟防卡死 | +| `tests/consult-single-pass-answer-20260927.test.ts` | 总和 ≤ 180_000 | 总和 ≤ 410_000 | 同上 | + +`tests/consult-evidence-lookup-20260927.test.ts` 的测试名与注释里仍写「70 s」,测试本身用自己的短时钟、不读常量,未改(描述性文字,记在此处)。 + +## 门禁(Node 22.14) + +| 项 | 基线 `8912ba68` | 本分支 | +|---|---|---| +| tsc | — | 0 | +| lint | — | 0 error / 126 warning | +| npm test | 4,842 条,fail 24 | 4,846 条,fail 24,与基线逐条同名 | +| next build | — | `/` Static | + +## 下游时限核对(T5,只报告) + +- Caddy(`deploy/Caddyfile*`):反代未设置读写超时,默认不限响应时长。 +- Next standalone / Node:无自定义 server;Node `requestTimeout` 300 s 管的是接收请求体,不管流式响应。 +- Provider(undici 默认):`bodyTimeout` 300 s 是两块数据之间的空闲时间;DeepSeek 推理内容也是流式下发(10-01 实测推理 5,200 token 在 30 s 内持续到达),正常不会空闲到 300 s。 +- 浏览器:无停滞检测;断线后服务端继续生成,客户端每 1.75 s 轮询状态。 +- 预留扣点:`/api/consult/status` 15 分钟租约后才懒取消,> 410 s。 +- 输出 token 上限 16,384 + 思考 8,192(`agent-generation-settings.ts`,与校正共用,未动)。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index c63e674d..a95dc110 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -398,3 +398,4 @@ - 任务书里的行号会随代码漂移,定位以符号名为准。 - **实现合入 `staging` 的同一次推送里,必须同时把状态板那一行改掉。** 2026-09-16 的对账发现 10 份早已合入的单仍写着「待领取 / 待验收」,会导致重复派活。 | `TASK-consult-plain-answer-20261001.md` | `PROGRESS-consult-plain-answer-20261001.md` | 普通对话「还是废话」:09-17 四步开场形状(格局名 → 谁推谁修 → 扮演哪个象)逼出谜语,问父母时爸妈被揉成一段;产品授权推翻该形状,改成先答 + 按问题里的对象分段 + 人话自检 + 空宫不单独下结论 + 父母卡标 mother/father(BUG-1132~1134) | **已实现,待 Claude 验收**(fork 子代理直接执行);未推 staging、未部署;模型对比为环境缺口,真机清单 `docs/testing/consult-plain-answer-20261001.md` | 分支 `codex/consult-plain-answer-20261001` | +| `TASK-consult-answer-clock-20261001.md` | `PROGRESS-consult-answer-clock-20261001.md` | 普通对话答题时钟 70 s 容不下推理模型(v4-pro 77 s),无出生分钟路线只受 110 s 工具钟管;产品定 5 分钟防卡死(BUG-1142) | **已实现,待 Claude 验收**(直接执行) | 分支 `codex/consult-answer-clock-20261001` | diff --git a/docs/tasks/TASK-consult-answer-clock-20261001.md b/docs/tasks/TASK-consult-answer-clock-20261001.md new file mode 100644 index 00000000..b2f782e3 --- /dev/null +++ b/docs/tasks/TASK-consult-answer-clock-20261001.md @@ -0,0 +1,31 @@ +# TASK · 普通对话答题时钟 70 s → 5 分钟(2026-10-01) + +- 基线:`origin/staging` `8912ba68` +- 分支 / worktree:`codex/consult-answer-clock-20261001` / `.worktrees/consult-answer-clock-20261001` +- 模式:直接执行(产品 10-01 选择「直接执行」),Claude 派子代理实现、独立验收 +- BUG:BUG-1142 + +## 事故实证 + +10-01 真实模型对比(`docs/testing/consult-plain-answer-20261001-model-runs.md`):`deepseek-v4-pro` 父母题 77 s 写完,超过 `CONSULTATION_ANSWER_TIMEOUT_MS`(`frontend/src/mastra/consultation-tools.ts`)的 70 s,线上会以 `answer_truncated` 截断。时限全链路排查:从浏览器、Caddy、Node 到 provider,能在写作中截断回答的只有本应用的两只钟(工具 110 s、答题 70 s),其余层都 ≥ 240 s 或不按总时长计。另发现无出生分钟路线(`usesPublicDailyGeneralAgent`)不切答题时钟,整段循环受 110 s 工具钟管。 + +## 决策记录 + +- 产品 2026-10-01:答题时长「可以延长、别定 70 秒这个上限」;在「3 / 5 / 10 分钟」中选 **5 分钟**,定位为防卡死,不是长度限制。推翻 BUG-1051 / 1053 记录中「110 + 70 = 180 s,产品接受三分钟」的口径,新上限 110 + 300 = 410 s。 +- 无出生分钟路线一并走答题时钟。 +- 工具阶段 110 s 与领域预算(2 个领域)不变。 + +## 硬红线 + +不动生时校正(`rectification-*`、`RECTIFICATION_RUN_BUDGET_MS`)、`agent-generation-settings.ts`、Caddy / deploy / workflow;截断仍记 `answer_truncated` 且不扣点。 + +## 任务与验收 + +| # | 内容 | 验收 | +|---|---|---| +| T1 | 答题时钟 300_000,注释写清原因 | 单测锁 300 s,工具钟仍 110 s | +| T2 | 领域预算不随之变化 | 单测锁 65 s / 2 个领域,公式不读答题时钟 | +| T3 | 无出生分钟路线从第一步起走答题时钟(`answerFromStart`) | 单测:工具钟到点不掐循环,答题钟到点仍掐;route 接线断言 | +| T4 | `maxDuration` 240 → 480 | 单测:110 + 300 + 60 s 以内 | +| T5 | 下游时限核对(只报告) | 见 PROGRESS | +| T6 | 记录:BUG-1142、CHANGELOG、README、真机清单加一步 | — | diff --git a/docs/testing/consult-plain-answer-20261001.md b/docs/testing/consult-plain-answer-20261001.md index b5e9249c..17abf379 100644 --- a/docs/testing/consult-plain-answer-20261001.md +++ b/docs/testing/consult-plain-answer-20261001.md @@ -11,3 +11,5 @@ | 5 | 新开对话,打「你好」 | 只回一句寒暄 | | 6 | 用没填出生分钟的人物档案问事业 | 不点个人大运,不写月份 | | 7 | 第 1 步的回答如果明显被截断(最后一节没写完就停了) | 记下时间和模型名报给 Claude——这是取消字数上限后要盯的风险(见进度记录「长度副作用核对」) | +| 8 | (BUG-1142,答题时钟改 5 分钟后)选 `deepseek-v4-pro` 新开对话,问「我和父母关系如何,他们怎么对待我」 | 回答完整写到「这周可以做的一件事」,不出现「回答未完成」;记下从发出到写完大约多少秒 | +| 9 | 用没填出生分钟的人物档案、选 `deepseek-v4-pro` 问一个长问题(例如「完整讲讲我今年的事业和感情」) | 回答写完,不出现「回答未完成」 | diff --git a/frontend/src/app/api/consult/route.ts b/frontend/src/app/api/consult/route.ts index 544414fd..f1e2092a 100644 --- a/frontend/src/app/api/consult/route.ts +++ b/frontend/src/app/api/consult/route.ts @@ -115,14 +115,14 @@ import { generateSessionTitle, shouldGenerateSessionTitle } from "@/lib/session- import { z } from "zod"; export const runtime = "nodejs"; -export const maxDuration = 240; +export const maxDuration = 480; // The step budget, the wall-clock budget and the domain cap all bound this same // run, so they are declared as one group in @/mastra/consultation-tools with the // reasoning that ties them together. maxDuration above is the ceiling they must // stay under: the tool phase (AGENT_TIMEOUT_MS) plus the answer phase's own -// clock (CONSULTATION_ANSWER_TIMEOUT_MS), 180s, plus setup and settlement -// (BUG-1051, BUG-1053). Self-hosted `node server.js` does not enforce it; it +// clock (CONSULTATION_ANSWER_TIMEOUT_MS), 410s, plus setup and settlement +// (BUG-1051, BUG-1053, TASK-consult-answer-clock-20261001). Self-hosted `node server.js` does not enforce it; it // documents the ceiling. const chatRequestMetadataSchema = z.object({ @@ -1103,10 +1103,13 @@ export async function POST(request: Request) { // that deadline until the calculation result is in hand, then on the answer // clock (CONSULTATION_ANSWER_TIMEOUT_MS), because its next step writes the // answer. Continuation and answer retries share the same answer clock. + // The general / no-birth-minute agent has no tools, so its loop is the + // answer from the first step and runs on the answer clock from the start. const runClock = createConsultationRunClock({ toolPhaseMs: AGENT_TIMEOUT_MS, answerMs: CONSULTATION_ANSWER_TIMEOUT_MS, answerReady: () => state.consultationToolCompleted, + answerFromStart: usesPublicDailyGeneralAgent(consultationMode, generalDailyContext), }); const agentAbortSignal = runClock.toolSignal; const streamOptions = { diff --git a/frontend/src/mastra/consultation-tools.ts b/frontend/src/mastra/consultation-tools.ts index 3512e2a2..6d0112d5 100644 --- a/frontend/src/mastra/consultation-tools.ts +++ b/frontend/src/mastra/consultation-tools.ts @@ -78,19 +78,24 @@ export const AGENT_TIMEOUT_MS = 110_000; * covers the rest of the loop plus any length continuation or empty-answer * retry. * - * 70s is measured, not guessed: staging writer calls on the default model - * produced 560-1240 output tokens in 5.7-13.6s end to end (about 90-100 tok/s - * including first-token latency, PROGRESS-report-writer-failure-20260902). 70s - * therefore holds about 6,300-7,000 output tokens, three to four times a - * typical four-heading answer; at half that throughput it still holds about - * 3,000 tokens. The answer step keeps provider thinking on (it is the loop's - * step), so thinking tokens now come out of the same 70s; the loop's step - * after the tool result used to think and write inside the 110s as well, and - * its text was thrown away. The calculation must finish inside the 110s tool - * phase, so the worst case is 110 + 70 = 180s, the three minutes the product - * accepted. The route's maxDuration must stay above the sum. + * 300s is a hang guard, not a length limit (product decision 2026-10-01, + * TASK-consult-answer-clock-20261001). The old 70s was sized for a + * non-reasoning writer (about 90-100 tok/s, PROGRESS-report-writer-failure-20260902). + * The answer step keeps provider thinking on, so thinking tokens come out of + * this clock too, and a reasoning model spends most of its time there: in the + * 10-01 real-model run (docs/testing/consult-plain-answer-20261001-model-runs.md) + * a parents answer on deepseek-v4-pro took 77s end to end and would have been + * cut, while deepseek-flash took 30-41s with about 85% of its tokens in + * thinking. The answer shape no longer has a word cap either, so the clock + * only has to stop a provider that hangs. A run that does hit it still ends + * as answer_truncated and is not charged (BUG-1051). + * + * The calculation must finish inside the 110s tool phase, so the worst case is + * 110 + 300 = 410s; the route's maxDuration must stay above the sum. The + * domain budget below is derived from the tool phase and its own reserve, not + * from this clock, so raising it does not change how many domains run. */ -export const CONSULTATION_ANSWER_TIMEOUT_MS = 70_000; +export const CONSULTATION_ANSWER_TIMEOUT_MS = 300_000; export type ConsultationRunClock = Readonly<{ /** The tool phase's deadline. Tools and precompute run under it, unchanged. */ @@ -114,11 +119,17 @@ export type ConsultationRunClock = Readonly<{ * where the calculation settles just before the tool-phase timer fires but * the stream consumer has not seen the result yet: the loop is then handed to * the answer clock instead of being cut. + * + * `answerFromStart` is for a loop with no calculation to wait for (the + * general / no-birth-minute agent has no tools): its first step already writes + * the answer, so the loop runs on the answer clock from the start instead of + * being cut by the tool phase's timer (TASK-consult-answer-clock-20261001). */ export function createConsultationRunClock(options: { toolPhaseMs?: number; answerMs?: number; answerReady?: () => boolean; + answerFromStart?: boolean; } = {}): ConsultationRunClock { const toolPhaseMs = options.toolPhaseMs ?? AGENT_TIMEOUT_MS; const answerMs = options.answerMs ?? CONSULTATION_ANSWER_TIMEOUT_MS; @@ -141,6 +152,7 @@ export function createConsultationRunClock(options: { } loop.abort(toolSignal.reason); }, { once: true }); + if (options.answerFromStart) answerSignal(); return Object.freeze({ toolSignal, loopSignal: loop.signal, answerSignal }); } export const CONSULTATION_NATAL_CALC_TOOL_ID = "run-jyotish-consultation"; diff --git a/frontend/tests/consult-answer-clock-20261001.test.ts b/frontend/tests/consult-answer-clock-20261001.test.ts new file mode 100644 index 00000000..4e567a49 --- /dev/null +++ b/frontend/tests/consult-answer-clock-20261001.test.ts @@ -0,0 +1,55 @@ +// TASK-consult-answer-clock-20261001: the answer clock is a five-minute hang +// guard, the general / no-birth-minute loop runs on it from the start, and the +// domain budget does not move with it. +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +import { + AGENT_TIMEOUT_MS, + CONSULTATION_ANSWER_TIMEOUT_MS, + CONSULTATION_DOMAIN_WALL_CLOCK_MS, + MAX_CONSULTATION_DOMAINS, + createConsultationRunClock, +} from "../src/mastra/consultation-tools.ts"; + +const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8"); +const tools = readFileSync(new URL("../src/mastra/consultation-tools.ts", import.meta.url), "utf8"); + +test("the answer clock is five minutes and the tool phase stays 110s", () => { + assert.equal(CONSULTATION_ANSWER_TIMEOUT_MS, 300_000); + assert.equal(AGENT_TIMEOUT_MS, 110_000); + const maxDuration = Number(route.match(/export const maxDuration = (\d+);/)?.[1]); + assert.ok(AGENT_TIMEOUT_MS + CONSULTATION_ANSWER_TIMEOUT_MS + 60_000 <= maxDuration * 1000, "setup and settlement still fit"); +}); + +test("the domain budget is unchanged by the answer clock", () => { + // Same numbers as before the change: 110s tool phase minus a 45s reserve, + // 31s per domain, so two domains run. + assert.equal(CONSULTATION_DOMAIN_WALL_CLOCK_MS, 65_000); + assert.equal(MAX_CONSULTATION_DOMAINS, 2); + const formula = tools.match(/export const CONSULTATION_DOMAIN_WALL_CLOCK_MS = [^;]+;/)?.[0] ?? ""; + assert.match(formula, /AGENT_TIMEOUT_MS - CONSULTATION_ANSWER_RESERVE_MS/); + assert.doesNotMatch(formula, /CONSULTATION_ANSWER_TIMEOUT_MS/); +}); + +test("a loop with no calculation runs on the answer clock from the start", async () => { + const clock = createConsultationRunClock({ toolPhaseMs: 30, answerMs: 200, answerFromStart: true }); + await new Promise((resolve) => setTimeout(resolve, 60)); + assert.equal(clock.toolSignal.aborted, true, "the tool phase's timer still fires"); + assert.equal(clock.loopSignal.aborted, false, "but it does not cut a loop that is already writing the answer"); + await new Promise((resolve) => setTimeout(resolve, 220)); + assert.equal(clock.loopSignal.aborted, true, "the answer clock still bounds it"); + assert.equal(clock.answerSignal(), clock.answerSignal(), "retries and continuations share the same clock"); +}); + +test("the consult route starts the general / no-birth-minute loop on the answer clock", () => { + assert.match( + route, + /const runClock = createConsultationRunClock\(\{[\s\S]*?answerFromStart: usesPublicDailyGeneralAgent\(consultationMode, generalDailyContext\),\s*\}\);/, + ); + // That branch still streams on the shared loop signal and keeps its traces. + const general = route.match(/if \(usesPublicDailyGeneralAgent\(consultationMode, generalDailyContext\)\) \{[\s\S]*?\n {4}\}\n/)?.[0] ?? ""; + assert.match(general, /await streamWithOverflowRetry\(agent\)/); + assert.match(general, /onError: \(error\) => settleRun\(\s*cancel,/); +}); diff --git a/frontend/tests/consult-answer-truncation-20260926.test.ts b/frontend/tests/consult-answer-truncation-20260926.test.ts index f3a5603c..4ce1110b 100644 --- a/frontend/tests/consult-answer-truncation-20260926.test.ts +++ b/frontend/tests/consult-answer-truncation-20260926.test.ts @@ -356,7 +356,11 @@ test("the consult route gives the answer phase its own clock inside maxDuration" // 原因: BUG-1053 删除 compose 流;「写回答有自己的时钟、不与工具阶段共用闸刀」这一性质不变 const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8"); const tools = readFileSync(new URL("../src/mastra/consultation-tools.ts", import.meta.url), "utf8"); - assert.match(tools, /export const CONSULTATION_ANSWER_TIMEOUT_MS = 70_000;/); + // 原值: CONSULTATION_ANSWER_TIMEOUT_MS = 70_000;总上限 AGENT_TIMEOUT_MS + 答题时钟 <= 180_000 + // 新值: CONSULTATION_ANSWER_TIMEOUT_MS = 300_000;总上限 <= 410_000(110s 工具 + 300s 答题) + // 原因: 产品 2026-10-01 决定答题时钟只防卡死、不限长度(TASK-consult-answer-clock-20261001); + // 推理模型 deepseek-v4-pro 父母题 77s 写完,70s 会截断 + assert.match(tools, /export const CONSULTATION_ANSWER_TIMEOUT_MS = 300_000;/); assert.match(route, /const runClock = createConsultationRunClock\(\{\s+toolPhaseMs: AGENT_TIMEOUT_MS,\s+answerMs: CONSULTATION_ANSWER_TIMEOUT_MS,/); assert.match(route, /const answerPhaseSignal = runClock\.answerSignal;/); assert.equal(route.match(/onAnswerPhase: startAnswerPhase,/g)?.length, 2, "natal and window hand the loop over"); @@ -371,5 +375,5 @@ test("the consult route gives the answer phase its own clock inside maxDuration" assert.match(route, /const agentAbortSignal = runClock\.toolSignal;/); const maxDuration = Number(route.match(/export const maxDuration = (\d+);/)?.[1]); assert.ok(AGENT_TIMEOUT_MS + CONSULTATION_ANSWER_TIMEOUT_MS < maxDuration * 1000); - assert.ok(AGENT_TIMEOUT_MS + CONSULTATION_ANSWER_TIMEOUT_MS <= 180_000, "product accepted about three minutes"); + assert.ok(AGENT_TIMEOUT_MS + CONSULTATION_ANSWER_TIMEOUT_MS <= 410_000, "product accepted a five-minute answer guard (2026-10-01)"); }); diff --git a/frontend/tests/consult-single-pass-answer-20260927.test.ts b/frontend/tests/consult-single-pass-answer-20260927.test.ts index d12aeb2e..68ade33e 100644 --- a/frontend/tests/consult-single-pass-answer-20260927.test.ts +++ b/frontend/tests/consult-single-pass-answer-20260927.test.ts @@ -446,5 +446,8 @@ test("the consult route has no separate compose stream and wires the single-pass assert.match(route, /const answerPhaseSignal = runClock\.answerSignal;/); const maxDuration = Number(route.match(/export const maxDuration = (\d+);/)?.[1]); assert.ok(AGENT_TIMEOUT_MS + CONSULTATION_ANSWER_TIMEOUT_MS < maxDuration * 1000); - assert.ok(AGENT_TIMEOUT_MS + CONSULTATION_ANSWER_TIMEOUT_MS <= 180_000, "product accepted about three minutes"); + // 原值: AGENT_TIMEOUT_MS + CONSULTATION_ANSWER_TIMEOUT_MS <= 180_000(110s + 70s) + // 新值: <= 410_000(110s + 300s) + // 原因: 产品 2026-10-01 把答题时钟改成 5 分钟防卡死(TASK-consult-answer-clock-20261001) + assert.ok(AGENT_TIMEOUT_MS + CONSULTATION_ANSWER_TIMEOUT_MS <= 410_000, "product accepted a five-minute answer guard (2026-10-01)"); });