fix(consult): five-minute answer clock as a hang guard; general mode runs on it from the start (BUG-1142)
The 70s answer clock was sized for a non-reasoning writer; thinking tokens come out of the same clock and deepseek-v4-pro took 77s on a parents answer. Product 2026-10-01 chose five minutes. The general / no-birth-minute loop has no tools, so it now starts on the answer clock instead of the 110s tool clock. Tool phase and domain budget unchanged; maxDuration 240 -> 480. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8912ba685a
commit
04c09cbf4d
@@ -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,/);
|
||||
});
|
||||
@@ -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)");
|
||||
});
|
||||
|
||||
@@ -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)");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user