Per-eval / per-call runtime knobs for the loop: Python-sandbox eval timeouts
(with clamping and a shell-timeout-aware widener), the svar/ask-code! stream
watchdog defaults, and the dynamic vars the loop binds per call.
Also the HOST half of the cancel contract: park-blocking-wall stops a
wall's clock while host code legitimately blocks.
A LEAF — depends on nothing else in the engine, so the loop and its tests read these settings from one place instead of carrying them in the loop namespace.
Per-eval / per-call runtime knobs for the loop: Python-sandbox eval timeouts (with clamping and a shell-timeout-aware widener), the `svar/ask-code!` stream watchdog defaults, and the dynamic vars the loop binds per call. Also the HOST half of the cancel contract: [[park-blocking-wall]] stops a wall's clock while host code legitimately blocks. A LEAF — depends on nothing else in the engine, so the loop and its tests read these settings from one place instead of carrying them in the loop namespace.
Hold hook installed by the innermost enclosing timeout wall: (fn [] release).
*blocking-wall-park* for work that ENTERS and LEAVES on separate calls.
A live view opens on one crossing of the Python bridge and closes on another
— there is no thunk to wrap around the watching — so a run SHOWING its work
takes a release token at open and spends it at close.
Hold hook installed by the innermost enclosing timeout wall: `(fn [] release)`. [[*blocking-wall-park*]] for work that ENTERS and LEAVES on separate calls. A live view opens on one crossing of the Python bridge and closes on another — there is no thunk to wrap around the watching — so a run SHOWING its work takes a release token at `open` and spends it at `close`.
Park hook installed by the innermost enclosing timeout wall: (fn [thunk]).
Extension dispatch and blocking user-input operations call
park-blocking-wall. Enclosing execution clocks stop for the complete
invocation, including hooks. The final concurrent or nested exit restores
the full execution budget; cancellation and the operation's own timeout
remain independent of this clock.
Park hook installed by the innermost enclosing timeout wall: `(fn [thunk])`. Extension dispatch and blocking user-input operations call [[park-blocking-wall]]. Enclosing execution clocks stop for the complete invocation, including hooks. The final concurrent or nested exit restores the full execution budget; cancellation and the operation's own timeout remain independent of this clock.
Dynamic timeout in milliseconds for Python code evaluation.
Dynamic timeout in milliseconds for Python code evaluation.
Dynamic context for RLM debug logging.
Dynamic context for RLM debug logging.
X-Initiator: agent — the header GitHub Copilot bills by.
Copilot charges a FULL premium interaction for every request it believes a
HUMAN initiated, and a MISSING X-Initiator counts as user. svar infers
the header from message roles, so any freshly built system+user prompt looks
exactly like something a person typed. Vis' background traffic — auto-titles,
extension helpers, one-shot ask-code!/llm-text! calls — is agent
activity, and without this header a cosmetic session title costs the same
premium interaction as the user's own turn. Only the foreground turn's FIRST
iteration is genuinely user initiated (loop/copilot-initiator-for-iteration).
`X-Initiator: agent` — the header GitHub Copilot bills by. Copilot charges a FULL premium interaction for every request it believes a HUMAN initiated, and a MISSING `X-Initiator` counts as `user`. svar infers the header from message roles, so any freshly built system+user prompt looks exactly like something a person typed. Vis' background traffic — auto-titles, extension helpers, one-shot `ask-code!`/`llm-text!` calls — is agent activity, and without this header a cosmetic session title costs the same premium interaction as the user's own turn. Only the foreground turn's FIRST iteration is genuinely user initiated (`loop/copilot-initiator-for-iteration`).
Host deadline for a provider attempt with no text, reasoning or tool-input output. It aborts only that request, allowing bounded retry without cancelling the turn. A provider-scoped network policy may widen this prefill window.
Host deadline for a provider attempt with no text, reasoning or tool-input output. It aborts only that request, allowing bounded retry without cancelling the turn. A provider-scoped network policy may widen this prefill window.
Default inter-chunk idle timeout for Vis svar/ask-code! calls (ms).
Fires when the transport itself goes silent — not one byte, not even an
SSE keepalive comment. 300s = CODEX PARITY with
DEFAULT_STREAM_IDLE_TIMEOUT_MS. Model-progress silence while keepalives
continue is different and does not time out by default; see
ASK_CODE_SEMANTIC_TIMEOUT_MS.
The FIRST byte has its own svar watchdog. Provider-scoped policy may widen that prefill window independently; this 300s default governs gaps after the first byte.
Default inter-chunk idle timeout for Vis `svar/ask-code!` calls (ms). Fires when the transport itself goes silent — not one byte, not even an SSE keepalive comment. 300s = CODEX PARITY with `DEFAULT_STREAM_IDLE_TIMEOUT_MS`. Model-progress silence while keepalives continue is different and does not time out by default; see `ASK_CODE_SEMANTIC_TIMEOUT_MS`. The FIRST byte has its own svar watchdog. Provider-scoped policy may widen that prefill window independently; this 300s default governs gaps after the first byte.
Default model/progress timeout for Vis svar/ask-code! streams (ms).
240 seconds without text, reasoning, tool-input progress or a terminal event means the model is stalled even if SSE keepalives still prove transport liveness. This exceeds Anthropic's documented ~185-second worst observed extended-thinking silence while bounding a live-but-unproductive stream.
Explicit nil disables it per call. OpenAI Responses sessions intentionally ignore this setting and retain their transport-specific idle policy.
Default model/progress timeout for Vis `svar/ask-code!` streams (ms). 240 seconds without text, reasoning, tool-input progress or a terminal event means the model is stalled even if SSE keepalives still prove transport liveness. This exceeds Anthropic's documented ~185-second worst observed extended-thinking silence while bounding a live-but-unproductive stream. Explicit nil disables it per call. OpenAI Responses sessions intentionally ignore this setting and retain their transport-specific idle policy.
Default time-to-first-token timeout for Vis svar/ask-code! calls (ms).
200s. The wait for the FIRST response header, and nothing else: no bytes arrived, no tool ran, no output was streamed, so the request can simply be made again.
Deliberately WIDER than svar's own router/DEFAULT_TTFT_TIMEOUT_MS (two
minutes). That number is tuned for a router holding a second candidate to
cross to; Vis pins the route to one provider+model, so the header a cold
start or a deep provider queue would have produced at 130s has nowhere
else to come from and must not be called dead at 120s. It still sits under
svar's router/DEFAULT_TIMEOUT_MS (the 300s whole-request cap Vis leaves
at its default), so this watchdog — not the HTTP client — is what names the
failure and the abort keeps its type (:svar.core/stream-ttft-timeout)
for the retry below to recognize. A project that lowers network.timeout_ms
under 200s reaches the client's own timeout first.
Measured: a pinned zai-coding-plan turn spent its last 120s waiting for
headers that never came and died with ten iterations of finished work
behind it, because svar's router had no second candidate to cross to. The
re-issue is what fixed that; this number only decides how long one try
waits. Because the abort is raised while no output exists,
loop/pre-output-stream-retryable? re-issues it seconds apart up to
MAX_PRE_OUTPUT_STREAM_RETRIES times, so a wedged endpoint is named by
three visible retries — about ten minutes at 200s, where 60s bought three —
instead of by one silent gap. Never shorten this without that retry in
place: alone it would only kill healthy slow-queue turns faster.
Model-progress silence while keepalives continue is a separate, opt-in semantic watchdog below.
Default time-to-first-token timeout for Vis `svar/ask-code!` calls (ms). 200s. The wait for the FIRST response header, and nothing else: no bytes arrived, no tool ran, no output was streamed, so the request can simply be made again. Deliberately WIDER than svar's own `router/DEFAULT_TTFT_TIMEOUT_MS` (two minutes). That number is tuned for a router holding a second candidate to cross to; Vis pins the route to one provider+model, so the header a cold start or a deep provider queue would have produced at 130s has nowhere else to come from and must not be called dead at 120s. It still sits under svar's `router/DEFAULT_TIMEOUT_MS` (the 300s whole-request cap Vis leaves at its default), so this watchdog — not the HTTP client — is what names the failure and the abort keeps its type (`:svar.core/stream-ttft-timeout`) for the retry below to recognize. A project that lowers `network.timeout_ms` under 200s reaches the client's own timeout first. Measured: a pinned zai-coding-plan turn spent its last 120s waiting for headers that never came and died with ten iterations of finished work behind it, because svar's router had no second candidate to cross to. The re-issue is what fixed that; this number only decides how long one try waits. Because the abort is raised while no output exists, `loop/pre-output-stream-retryable?` re-issues it seconds apart up to `MAX_PRE_OUTPUT_STREAM_RETRIES` times, so a wedged endpoint is named by three visible retries — about ten minutes at 200s, where 60s bought three — instead of by one silent gap. Never shorten this without that retry in place: alone it would only kill healthy slow-queue turns faster. Model-progress silence while keepalives continue is a separate, opt-in semantic watchdog below.
(await-wall fut deadline timeout-value)Wait for fut until the CURRENT value of deadline passes, re-reading the
atom on every wake so a park that MOVED the wall extends the wait instead of
expiring. Returns timeout-value once the wall is really reached.
A nil deadline disables this watchdog while an extension executes, a human is being asked, or a live view is watched. Polling observes the full budget restored after the final park or hold is released.
Wait for `fut` until the CURRENT value of `deadline` passes, re-reading the atom on every wake so a park that MOVED the wall extends the wait instead of expiring. Returns `timeout-value` once the wall is really reached. A nil deadline disables this watchdog while an extension executes, a human is being asked, or a live view is watched. Polling observes the full budget restored after the final park or hold is released.
(clamp-eval-timeout-ms candidate)Clamp a candidate eval timeout to [MIN_EVAL_TIMEOUT_MS, MAX_EVAL_TIMEOUT_MS].
Clamp a candidate eval timeout to [MIN_EVAL_TIMEOUT_MS, MAX_EVAL_TIMEOUT_MS].
Default wall-clock backstop around ONE python_execution block — five minutes.
A BACKSTOP for guest code that would never finish on its own (while True:, a
wedged frame), never a co-deadline for work that is progressing. Every BOUNDED
call a block makes — shell, run_tests, HTTP — is already floored ABOVE this
number by the widener below, so what this number really bounds is the in-sandbox
compute no scan can see: a large parse, an image pass, an analytic loop over
thousands of files. At two minutes that work was killed exactly where it got
expensive, and the model re-ran the whole block from zero.
The price is deliberate: a genuinely wedged block now costs five minutes of a
turn instead of two. It stays far under MAX_EVAL_TIMEOUT_MS and under every
bounded-call floor, so a block that makes such a call still gets that call's own
budget plus the widener's grace.
Default wall-clock backstop around ONE `python_execution` block — five minutes. A BACKSTOP for guest code that would never finish on its own (`while True:`, a wedged frame), never a co-deadline for work that is progressing. Every BOUNDED call a block makes — shell, `run_tests`, HTTP — is already floored ABOVE this number by the widener below, so what this number really bounds is the in-sandbox compute no scan can see: a large parse, an image pass, an analytic loop over thousands of files. At two minutes that work was killed exactly where it got expensive, and the model re-ran the whole block from zero. The price is deliberate: a genuinely wedged block now costs five minutes of a turn instead of two. It stays far under [[MAX_EVAL_TIMEOUT_MS]] and under every bounded-call floor, so a block that makes such a call still gets that call's own budget plus the widener's grace.
Default per-command / per-wait budget of the shell tool — declared here, and
read by foundation.shell, so ONE number governs both the tool and the
watchdog above it.
Thirty minutes, the same as MAX_SHELL_TIMEOUT_SECS. The budget is a
CEILING on the WAIT, never a delay: a fast command returns the instant it
exits, so the only calls this number touches are the long ones — a native
image, a cold Gradle / npm build, a full suite — and at two minutes those were
killed precisely where their output started to matter, with the work already
paid for.
It matters here because it used to EQUAL the eval watchdog's own default: both
were two minutes. A shell call that named no timeout therefore raced a
watchdog that had already started, and the watchdog always won — the turn got a
bare Timeout with NO output instead of shell's own envelope (partial stdout,
timed_out true, a killed process tree). The widener below now floors the
watchdog above this budget whenever an eval calls the shell at all.
Default per-command / per-wait budget of the `shell` tool — declared here, and read by `foundation.shell`, so ONE number governs both the tool and the watchdog above it. Thirty minutes, the same as [[MAX_SHELL_TIMEOUT_SECS]]. The budget is a CEILING on the WAIT, never a delay: a fast command returns the instant it exits, so the only calls this number touches are the long ones — a native image, a cold Gradle / npm build, a full suite — and at two minutes those were killed precisely where their output started to matter, with the work already paid for. It matters here because it used to EQUAL the eval watchdog's own default: both were two minutes. A `shell` call that named no timeout therefore raced a watchdog that had already started, and the watchdog always won — the turn got a bare `Timeout` with NO output instead of shell's own envelope (partial stdout, `timed_out` true, a killed process tree). The widener below now floors the watchdog above this budget whenever an eval calls the shell at all.
(eval-timeout-ms-for-code base-timeout-ms code)Eval watchdog for ONE Python block: the configured base, raised so it sits a grace period ABOVE the longest bounded call the block makes.
Eval watchdog for ONE Python block: the configured base, raised so it sits a grace period ABOVE the longest bounded call the block makes.
(explicit-shell-timeout-secs code)Best-effort scan for an EXPLICIT timeout override in Python code, in seconds.
Reads timeout_secs / timeout / secs (seconds) and timeout_ms
(milliseconds, rounded up) — repl_eval and MCP calls spell their budget in
ms, a shell handle's .wait(secs=…) in seconds, and leaving a spelling out let
a deliberately long call die at the default watchdog. The real tool still owns
validation/clamping; this only prevents the outer watchdog from preempting a
longer requested budget.
Best-effort scan for an EXPLICIT timeout override in Python code, in seconds. Reads `timeout_secs` / `timeout` / `secs` (seconds) and `timeout_ms` (milliseconds, rounded up) — `repl_eval` and MCP calls spell their budget in ms, a shell handle's `.wait(secs=…)` in seconds, and leaving a spelling out let a deliberately long call die at the default watchdog. The real tool still owns validation/clamping; this only prevents the outer watchdog from preempting a longer requested budget.
(hold-blocking-wall!)LIFT every enclosing wall until the returned thunk is called, and answer that thunk.
Not a bigger budget — no budget. The wall is the backstop for code that fell SILENT, and a run painting a picture a human is watching is the opposite of silent: it is billed nothing at all, for as long as it shows its work. The base budget comes back, measured from the release, when the last hold is spent.
Releasing twice is a no-op, so a finally releasing what a close already
released cannot collapse the clock under work that is still running. With no
wall installed (a host-side call, tests) both halves are no-ops.
LIFT every enclosing wall until the returned thunk is called, and answer that thunk. Not a bigger budget — no budget. The wall is the backstop for code that fell SILENT, and a run painting a picture a human is watching is the opposite of silent: it is billed nothing at all, for as long as it shows its work. The base budget comes back, measured from the release, when the last hold is spent. Releasing twice is a no-op, so a `finally` releasing what a close already released cannot collapse the clock under work that is still running. With no wall installed (a host-side call, tests) both halves are no-ops.
Floor for an eval that reaches the NETWORK through the HTTP shims (requests,
httpx, urllib3, urlopen).
A request's budget is invisible to a text scan in the way that matters: the
shim's own per-call default is 30s, the block usually loops over N hosts, and
the helper doing the fetching is typically defined in an EARLIER block. So a
perfectly ordinary crawl raced the watchdog — two minutes, then — and the
watchdog won: a bare Timeout for work that was progressing normally, which is
the defect DEFAULT_SHELL_TIMEOUT_SECS describes, spelled in sockets.
Unlike shell's, this floor does NOT drop when the block spells a literal
timeout=: that number bounds ONE request, never the loop around it.
Floor for an eval that reaches the NETWORK through the HTTP shims (`requests`, `httpx`, `urllib3`, `urlopen`). A request's budget is invisible to a text scan in the way that matters: the shim's own per-call default is 30s, the block usually loops over N hosts, and the helper doing the fetching is typically defined in an EARLIER block. So a perfectly ordinary crawl raced the watchdog — two minutes, then — and the watchdog won: a bare `Timeout` for work that was progressing normally, which is the defect `DEFAULT_SHELL_TIMEOUT_SECS` describes, spelled in sockets. Unlike shell's, this floor does NOT drop when the block spells a literal `timeout=`: that number bounds ONE request, never the loop around it.
(implicit-call-budget-secs code)Seconds an eval is entitled to purely from WHICH bounded calls it makes, when none of them names a number. A timeout that is a variable, an expression, or simply the tool's default is invisible to a text scan, so without this floor the watchdog silently preempts a call that owns a longer budget and answers timeouts itself.
A shell call may legally own the FULL cap, so that is its floor even when the
block DOES spell a literal budget somewhere: that literal bounds ONE call, and a
second, unannotated call in the same block still owns shell's default — which is
the cap.
An HTTP call keeps its floor for the same reason: a literal timeout= bounds
ONE request, and the block is almost always a LOOP of them.
Seconds an eval is entitled to purely from WHICH bounded calls it makes, when none of them names a number. A timeout that is a variable, an expression, or simply the tool's default is invisible to a text scan, so without this floor the watchdog silently preempts a call that owns a longer budget and answers timeouts itself. A `shell` call may legally own the FULL cap, so that is its floor even when the block DOES spell a literal budget somewhere: that literal bounds ONE call, and a second, unannotated call in the same block still owns shell's default — which is the cap. An HTTP call keeps its floor for the same reason: a literal `timeout=` bounds ONE request, and the block is almost always a LOOP of them.
Hard ceiling for :eval-timeout-ms — 35 minutes.
It must sit ABOVE the longest bounded call a block may own plus the widener's
grace: MAX_SHELL_TIMEOUT_SECS (30 min) + shell-timeout-eval-grace-ms. A
ceiling equal to the shell cap would clamp the widened watchdog 10s BELOW the
shell envelope, so a legal 30-minute wait would die at the watchdog with a bare
Timeout and no output instead of shell's own structured envelope — the exact
defect DEFAULT_SHELL_TIMEOUT_SECS describes. Lift the two together.
Hard ceiling for :eval-timeout-ms — 35 minutes. It must sit ABOVE the longest bounded call a block may own plus the widener's grace: [[MAX_SHELL_TIMEOUT_SECS]] (30 min) + `shell-timeout-eval-grace-ms`. A ceiling equal to the shell cap would clamp the widened watchdog 10s BELOW the shell envelope, so a legal 30-minute wait would die at the watchdog with a bare `Timeout` and no output instead of shell's own structured envelope — the exact defect [[DEFAULT_SHELL_TIMEOUT_SECS]] describes. Lift the two together.
Hard ceiling foundation.shell clamps EVERY run / wait budget to: thirty
minutes. Declared here so the widener below can floor the eval watchdog above
the LONGEST budget a shell call may legally own, and so
MAX_EVAL_TIMEOUT_MS can stay a grace period above THIS.
A budget that is a variable or an expression is invisible to a text scan, so
flooring at anything under the cap let the watchdog preempt a legal wait with a
bare Timeout instead of shell's own envelope — the same defect
DEFAULT_SHELL_TIMEOUT_SECS describes, one spelling further out.
Hard ceiling `foundation.shell` clamps EVERY `run` / `wait` budget to: thirty minutes. Declared here so the widener below can floor the eval watchdog above the LONGEST budget a shell call may legally own, and so [[MAX_EVAL_TIMEOUT_MS]] can stay a grace period above THIS. A budget that is a variable or an expression is invisible to a text scan, so flooring at anything under the cap let the watchdog preempt a legal wait with a bare `Timeout` instead of shell's own envelope — the same defect `DEFAULT_SHELL_TIMEOUT_SECS` describes, one spelling further out.
(park-blocking-wall thunk)Run thunk with every enclosing timeout wall parked. With no wall installed
(plain JVM call, tests) this is just (thunk).
Run `thunk` with every enclosing timeout wall parked. With no wall installed (plain JVM call, tests) this is just `(thunk)`.
(parkable-wall start timeout-ms)One MOVABLE wall clock for a bounded execution that began at start with
timeout-ms of budget.
Returns {:deadline <atom epoch-ms or nil> :park (fn [thunk]) :hold (fn [] release)},
where a NIL deadline is NO wall at all (see await-wall).
park is re-entrant and safe for concurrent calls. Only the final exit
restores the full timeout-ms budget, measured from that exit, even when
the call throws. Time spent executing extensions or awaiting user input
does not consume the enclosing execution budget.
It composes with *blocking-wall-park*, so parking an inner wall parks
every enclosing wall too. hold takes and releases the same clock on
separate calls (see *blocking-wall-hold*). Parks and holds share depth
and nest in either order.
One MOVABLE wall clock for a bounded execution that began at `start` with
`timeout-ms` of budget.
Returns `{:deadline <atom epoch-ms or nil> :park (fn [thunk]) :hold (fn [] release)}`,
where a NIL deadline is NO wall at all (see [[await-wall]]).
`park` is re-entrant and safe for concurrent calls. Only the final exit
restores the full `timeout-ms` budget, measured from that exit, even when
the call throws. Time spent executing extensions or awaiting user input
does not consume the enclosing execution budget.
It composes with [[*blocking-wall-park*]], so parking an inner wall parks
every enclosing wall too. `hold` takes and releases the same clock on
separate calls (see [[*blocking-wall-hold*]]). Parks and holds share depth
and nest in either order.Floor for an eval that calls run_tests: the run's OWN budget, in seconds.
A test run answers its own timeout with a structured test result, and nothing
preempts a direct tool call. Called from python_execution it must not die
earlier at the generic eval watchdog and lose that result, so the widener
floors the block's wall here and adds shell-timeout-eval-grace-ms on top.
Floor for an eval that calls `run_tests`: the run's OWN budget, in seconds. A test run answers its own timeout with a structured test result, and nothing preempts a direct tool call. Called from `python_execution` it must not die earlier at the generic eval watchdog and lose that result, so the widener floors the block's wall here and adds `shell-timeout-eval-grace-ms` on top.
The budget for ONE run_tests run, in every pack — ten minutes.
The Clojure pack uses this for both nREPL evaluation and owned CLI processes
(one deadline across shadow-cljs compilation and execution). The Python pack
waits this long on the project interpreter's pytest. RUN_TESTS_FLOOR_SECS
keeps the eval watchdog above all of them; no runner owns another literal.
Overrunning it is reported as a STRUCTURED result — a wedged nREPL, a killed pytest process, the output that did arrive — so the number says when we stop believing the suite will finish, never how much work a suite may legitimately do. Five minutes did not cover a cold full-suite run (JVM start, namespace loading, compilation), and such a run died with nothing to show for it.
Stays under MAX_EVAL_TIMEOUT_MS minus the widener's grace.
The budget for ONE `run_tests` run, in every pack — ten minutes. The Clojure pack uses this for both nREPL evaluation and owned CLI processes (one deadline across shadow-cljs compilation and execution). The Python pack waits this long on the project interpreter's pytest. [[RUN_TESTS_FLOOR_SECS]] keeps the eval watchdog above all of them; no runner owns another literal. Overrunning it is reported as a STRUCTURED result — a wedged nREPL, a killed pytest process, the output that did arrive — so the number says when we stop believing the suite will finish, never how much work a suite may legitimately do. Five minutes did not cover a cold full-suite run (JVM start, namespace loading, compilation), and such a run died with nothing to show for it. Stays under [[MAX_EVAL_TIMEOUT_MS]] minus the widener's grace.
(with-agent-initiator opts)Mark opts as agent-initiated Copilot traffic. Caller headers win, so a call
site that really is answering a human can still pin "user" itself.
Mark `opts` as agent-initiated Copilot traffic. Caller headers win, so a call site that really is answering a human can still pin `"user"` itself.
(with-default-ask-code-idle-timeout opts)(with-default-ask-code-idle-timeout opts provider-network)Merge streaming limits with precedence: explicit call opts, provider policy, Vis runtime defaults, then svar defaults for keys Vis leaves absent.
Merge streaming limits with precedence: explicit call opts, provider policy, Vis runtime defaults, then svar defaults for keys Vis leaves absent.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |