Resolve conflicts from master's catalog/doc refactors landing alongside the tool-call timeout work: - knip.json: keep both new workspace entries (util/timeout + support/acp-snapshot). - tool-web/src/fetch.ts: keep the timeout_ms removal, adopt master's richer JSDoc @param/@returns style on parseFetchArgs/presentFetchCall. - tools/README.md: keep the tools/execute pipeline wording, adopt master's flattened docs/tool-catalog.md path. - Regenerate every generated doc (cordis-catalog, tool-catalog, config-catalog, doc-graphs, module-graph) so they carry both master's changes and the tools/execute event + timeout-policy package. - Add @param/@returns to toolTimeoutResult for master's new verify-export-jsdoc gate.
dsh-timeout-policy
Tool-call timeout policy: a single tools/execute around-dispatch listener that arms a per-call cooperative deadline on exec.signal for each configured tool and returns a structured TOOL_TIMEOUT result when that deadline wins. It is the reference tools/execute wrapper and the deployment-owned home for model-facing tool-call budgets (the timeout-library RFC's foreseen middleware).
Plugin (namespace: timeout-policy)
A function/namespace plugin (name / Config / apply), not a service. It registers no tool and injects nothing — it consumes ctx.tools's tools/execute waterfall, which the dsh-tools registry always provides.
Config
Per-tool policy, keyed by the model-facing tool name. There is deliberately no global default (a global budget would silently start failing any tool that runs long once the plugin loads) and no model-facing override (timeout is deployment policy, not prompt semantics) in this version.
- id: timeout-policy
name: '@deepseek-ai/dsh-timeout-policy'
config:
tools:
web_fetch:
timeoutMs: 30000
web_search:
timeoutMs: 30000
| Key | Type | Meaning |
|---|---|---|
tools |
Record<string, { timeoutMs }> |
Per-tool timeout policy; an unlisted tool gets no deadline. timeoutMs is required per configured tool and must be positive finite. |
Behavior
For a configured tool the listener:
- Arms
deadline(exec.signal, timeoutMs, 'TOOL_TIMEOUT')— one signal fusing the caller's abort with this plugin's timer (@deepseek-ai/dsh-timeout). - Swaps that derived signal onto
execfor the downstream dispatch, then restores the caller's own signal afterward (cordisnext()ignores passed arguments, so the wrapper mutates the sharedexecin place; restoring keepstools/post-executeseeing the caller's signal). - After dispatch, if
timeoutOf(d.signal, 'TOOL_TIMEOUT')matches — this plugin's own timer fired — replaces the result with a structuredTOOL_TIMEOUTtool result:{ isError: true, error: { name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' }, content: 'Error: tool call timed out after <ms>ms' }.
An unconfigured tool delegates untouched (no deadline).
The base next() of tools/execute is the registry's dispatch-with-normalization thunk, so when the timeout signal reaches a provider that throws its own upstream-abort error, dispatch first turns it into a normal error result, and this wrapper then replaces that with TOOL_TIMEOUT. That ordering is why the replacement is keyed off the signal (timeoutOf), not off the dispatched result's shape.
Cooperative, not a hard kill
The derived signal only notifies; termination stays with the tool and the capability it forwards exec.signal to (the dsh-timeout library owns no kill). "Configured" therefore means "cooperative with exec.signal": a tool that ignores the signal will not stop on timeout. A deployment must only configure tools that forward the signal to their implementation — the shipped web_fetch/web_search (which forward through ctx.web to providers) are the reference. TOOL_TIMEOUT needs no session event for reconstructability: it is the final model-facing tool/result, already logged by the loop.
Composing with other tools/execute wrappers
Multiple tools/execute listeners compose by cordis registration order. Combined with a future retry/sandbox/metrics wrapper, registration order chooses the semantics — "timeout covers the whole retry operation" (timeout registered outer) versus "timeout covers each attempt" (timeout registered inner).