Skip to content

Commit d6c880a

Browse files
authored
fix(link-understanding): honor global or largest model timeout for link fetches (#100660) (#100731)
1 parent e27202a commit d6c880a

2 files changed

Lines changed: 71 additions & 1 deletion

File tree

src/link-understanding/runner.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,59 @@ describe("runLinkUnderstanding", () => {
144144
expect(result.outputs).toEqual([]);
145145
expect(runCommandWithTimeout).not.toHaveBeenCalled();
146146
});
147+
148+
it("uses the global link-tools timeout for fetches when configured", async () => {
149+
mockGuardedFetch("page body", "https://example.com/final");
150+
mockCommand("summarized page");
151+
152+
await runLinkUnderstanding({
153+
cfg: {
154+
tools: {
155+
links: {
156+
enabled: true,
157+
timeoutSeconds: 15,
158+
models: [
159+
{ type: "cli", command: "summarize-fast", timeoutSeconds: 1 },
160+
{ type: "cli", command: "summarize-slow", timeoutSeconds: 9 },
161+
],
162+
},
163+
},
164+
} as OpenClawConfig,
165+
ctx: ctx("see https://example.com/page"),
166+
});
167+
168+
expect(fetchWithSsrFGuard).toHaveBeenCalledWith(
169+
expect.objectContaining({
170+
timeoutMs: 15000,
171+
url: "https://example.com/page",
172+
}),
173+
);
174+
});
175+
176+
it("falls back to the largest model timeout for fetches when no global timeout is set", async () => {
177+
mockGuardedFetch("page body", "https://example.com/final");
178+
mockCommand("summarized page");
179+
180+
await runLinkUnderstanding({
181+
cfg: {
182+
tools: {
183+
links: {
184+
enabled: true,
185+
models: [
186+
{ type: "cli", command: "summarize-fast", timeoutSeconds: 1 },
187+
{ type: "cli", command: "summarize-slow", timeoutSeconds: 9 },
188+
],
189+
},
190+
},
191+
} as OpenClawConfig,
192+
ctx: ctx("see https://example.com/page"),
193+
});
194+
195+
expect(fetchWithSsrFGuard).toHaveBeenCalledWith(
196+
expect.objectContaining({
197+
timeoutMs: 9000,
198+
url: "https://example.com/page",
199+
}),
200+
);
201+
});
147202
});

src/link-understanding/runner.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ function resolveTimeoutMsFromConfig(params: {
4141
return resolveTimeoutMs(configured, DEFAULT_LINK_TIMEOUT_SECONDS);
4242
}
4343

44+
function resolveFetchTimeoutMsFromConfig(params: {
45+
config?: LinkToolsConfig;
46+
entries: LinkModelConfig[];
47+
}): number {
48+
// The HTTP fetch phase is independent of any single CLI execution, so honor
49+
// an explicit global link-tools timeout first. Otherwise use the largest
50+
// per-entry timeout so slower entries are not capped by the first entry.
51+
if (params.config?.timeoutSeconds != null) {
52+
return resolveTimeoutMs(params.config.timeoutSeconds, DEFAULT_LINK_TIMEOUT_SECONDS);
53+
}
54+
return Math.max(
55+
...params.entries.map((entry) => resolveTimeoutMsFromConfig({ config: params.config, entry })),
56+
);
57+
}
58+
4459
function isLinkUrlTemplate(value: string): boolean {
4560
return value.includes("LinkUrl") || value.includes("LinkFinalUrl");
4661
}
@@ -220,8 +235,8 @@ export async function runLinkUnderstanding(params: {
220235
}
221236

222237
const outputs: string[] = [];
238+
const timeoutMs = resolveFetchTimeoutMsFromConfig({ config, entries });
223239
for (const url of links) {
224-
const timeoutMs = resolveTimeoutMsFromConfig({ config, entry: entries[0] });
225240
let fetched: Awaited<ReturnType<typeof fetchLinkContent>>;
226241
try {
227242
fetched = await fetchLinkContent({ url, timeoutMs });

0 commit comments

Comments
 (0)