Skip to content

Commit 2530a9f

Browse files
mpstatonclaude
andcommitted
fix(prompt-runner): crawl requests get a real deadline — 4 minutes per round-trip, not the SDK's silent 10
A carnegie-foundation team crawl held one non-streaming connection for the SDK's full default 10-minute timeout before dying with "Request timed out" — the operator watched a spinner for 10 minutes to learn nothing. Crawl model calls now pass an explicit per-request budget (240s, 1 retry — ~480s worst case, inside the 600s dispatch ceiling) so a stuck request fails fast with a real error while normal crawls (observed ~211s total) are untouched. Each pause_turn continuation gets the same per-request budget. runPrompt grows optional timeoutMs/maxRetries per-request overrides; enrichment runs (run.ts, drafter.ts) keep SDK defaults deliberately — uncapped pack crawls have legitimately run 15+ minutes. Env dial: CRAWL_REQUEST_TIMEOUT_MS. Files changed: - services/prompt-runner/src/anthropic.ts - services/prompt-runner/src/crawl.ts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW28dw3kQAKXr2ZNefCukE
1 parent cde8cf4 commit 2530a9f

2 files changed

Lines changed: 30 additions & 5 deletions

File tree

services/prompt-runner/src/anthropic.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,33 @@ function extractText(content: Anthropic.ContentBlock[]): string {
5656
*/
5757
export async function runPrompt(
5858
request: Anthropic.MessageCreateParamsNonStreaming,
59-
options?: { signal?: AbortSignal },
59+
options?: {
60+
signal?: AbortSignal;
61+
// Per-REQUEST deadline + retry override. Without it the SDK defaults
62+
// apply (10-minute timeout, 2 retries) — observed live 2026-07-28: a
63+
// carnegie-foundation team crawl held one connection the full 10
64+
// minutes before "Request timed out". Callers with their own dispatch
65+
// ceilings (crawls: 600s) pass a tighter budget so a stuck request
66+
// fails fast and localizes. Each pause_turn continuation gets the same
67+
// per-request budget — the cap is per round-trip, not per crawl.
68+
timeoutMs?: number;
69+
maxRetries?: number;
70+
},
6071
): Promise<string> {
61-
const signal = options?.signal;
72+
const { signal, timeoutMs, maxRetries } = options ?? {};
73+
const reqOpts = {
74+
signal,
75+
...(timeoutMs !== undefined ? { timeout: timeoutMs } : {}),
76+
...(maxRetries !== undefined ? { maxRetries } : {}),
77+
};
6278
let messages: Anthropic.MessageParam[] = request.messages;
63-
let response = await getClient().messages.create(request, { signal });
79+
let response = await getClient().messages.create(request, reqOpts);
6480

6581
let continuations = 0;
6682
while (response.stop_reason === 'pause_turn' && continuations < MAX_PAUSE_CONTINUATIONS) {
6783
continuations += 1;
6884
messages = [...messages, { role: 'assistant', content: response.content }];
69-
response = await getClient().messages.create({ ...request, messages }, { signal });
85+
response = await getClient().messages.create({ ...request, messages }, reqOpts);
7086
}
7187

7288
return extractText(response.content);

services/prompt-runner/src/crawl.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ const CRAWL_MAX_TOKENS = Number(process.env.CRAWL_MAX_TOKENS ?? 4096);
2525
// (SearXNG/Exa, no Anthropic tokens). Uncapped, a crawl of a huge publisher
2626
// searched open-endedly (see request.ts).
2727
const CRAWL_MAX_WEB_SEARCHES = Number(process.env.CRAWL_MAX_WEB_SEARCHES ?? 5);
28+
// Per-request deadline for crawl model calls. The SDK default (10 min ×
29+
// 2 retries) let a stuck request eat the whole 600s dispatch ceiling and
30+
// then time out anyway (carnegie-foundation team crawl, 2026-07-28).
31+
// 240s × (1 retry + 1) ≈ 480s worst case — inside the ceiling, so the
32+
// operator sees a real error instead of a silent 10-minute hang.
33+
const CRAWL_REQUEST_TIMEOUT_MS = Number(process.env.CRAWL_REQUEST_TIMEOUT_MS ?? 240_000);
2834

2935
export type CrawlTarget = 'links' | 'streams' | 'team';
3036

@@ -220,7 +226,10 @@ async function handleCrawl(nc: NatsConnection, msg: CrawlMsg): Promise<void> {
220226
tools: ['web_search'],
221227
webSearchMaxUses: CRAWL_MAX_WEB_SEARCHES,
222228
});
223-
const text = await runPrompt(request);
229+
const text = await runPrompt(request, {
230+
timeoutMs: CRAWL_REQUEST_TIMEOUT_MS,
231+
maxRetries: 1,
232+
});
224233
const parsed = extractJson(text);
225234

226235
if (args.target === 'team') {

0 commit comments

Comments
 (0)