Execution Limits
Three knobs bound every run โ deadline, stuck, cost_usd โ one schema at every scope, and one command that tells you what is in force and where it came from.
mur limits develop-rust # every bound for a fleet, with its source
mur limits rustsmith --json # the same for an agent, for tooling
mur fleet limits develop-rust --deadline 3h --stuck 20m
mur agent limits rustsmith --stuck off
mur limits --global --deadline 4h # config.yaml, edited as text
mur limits develop-rust --unset cost_usd
The three knobs
| knob | meaning | built-in default |
|---|---|---|
deadline | wall clock for the unit of work โ 30s, 5m, 2h, 1h30m, 1d | 30m for a single task, 1h for a fleet run |
stuck | how long with no real progress before the run stops (unattended) or warns (attended); off disables | 10m |
cost_usd | a spend cap โ only on a metered model | none |
They live in a limits: block with the same shape at three scopes, and the inner scope replaces a key it sets; nothing adds two caps together:
# ~/.mur/config.yaml # ~/.mur/fleets/<name>/fleet.yaml # ~/.mur/agents/<name>/profile.yaml
limits: limits: limits:
deadline: 4h deadline: 2h stuck: off
stuck: 20m
A per-call flag (mur fleet run --loop --deadline 15m) is the innermost scope of all.
Attended is not unattended
The axis is who is watching, not local versus cloud.
attended (murmur, Hub chat) | unattended (mur agent send, schedules, channel/delegate, fleet loops, daemon auto-run) | |
|---|---|---|
deadline | ignored | required โ built-in if nothing is set |
stuck | one warning in the live transcript: โ no progress for 10m โ Esc to stop | stops with stuck and the last three tool calls |
cost_usd | ignored | enforced when set and the model is metered |
| hard stop | Esc | deadline / stuck / cost / mur fleet stop |
Progress is a file write through any tool, a channel event authored by an agent, or a tool call whose name and arguments differ from the previous iteration's. A text-only model turn is not progress; the same call again is not progress. That is the "reasoning in circles" and the "retrying the same thing" shape, and those are exactly what the stuck clock exists to end.
The scope that launched owns the budget
A fleet delegating a task with twelve minutes left on its clock hands the member a twelve-minute deadline, not a fresh half hour. There is no per-task cap inside a fleet run; the fleet's limits: is the whole budget.
What no longer stops a run
- Iteration caps (
hitl.max_iterations,loop.max_iterations,--max-iterations) and token budgets (hitl.max_tokens) โ loaded, ignored, and named once at agent start and bymur limits:
stale: hitl.max_iterations: 800 in profile.yaml โ IGNORED since 2.79 โ remove it; the bounds are limits: deadline / stuck / cost_usd
loop.budget_usdstill works: it is read aslimits.cost_usduntil you rewrite the file;mur fleet limitswrites the new key.- The runtime keeps an iteration counter as a diagnostic, with a 10 000 ceiling that turns a runaway bug into a stop with a reason. It is not a setting.
Local models cost nothing
A cost cap on a fleet that runs on local or subscription models is reported as does not apply and never enforced โ that fleet is bounded by its deadline. A billable fleet with a deadline and no cost cap is allowed, but told so at start:
โ billable, no cost cap โ bound is the deadline (2h); spend is reported per iteration at $0.05/1k โ set --budget-usd to cap it
Mark a local model with billing: local in models.yaml when its provider is not one MUR already knows to be local.
Long work returns a handle
Work duration is not a timeout. fleet_run (the agent tool) and parallel_jobs (the MCP tool) answer within a second:
Poll mur_job_status <run_id> โ it reports the semantic state (running / blocked / done / failed / stopped, or abandoned once a dead run is past its grace window) and a liveness verdict (alive / stalled / dead) โ or mur fleet status <fleet>; the result lands in the fleet's channel, and the murmur rail and the deep-research panel show it live. mur fleet run --run-id <id> records a run under an id you chose, which is how the tools mint theirs. The MCP per-call timeout stays 120 s on purpose: a tool that needs longer must return a handle, and raising the timeout is the wrong fix for the next twenty-minute tool.
A parallel_jobs run lives inside the MCP server process that dispatched it; if that process exits, the run ends.
No clock ends a live reply
The same rule reaches the model connection: nothing cuts a reply off by elapsed time. There is no total request timeout, so a reply that is still arriving keeps arriving โ which matters most for a local model that spends a minute loading weights and evaluating a long prompt before its first token.
What is bounded is silence, and with two numbers rather than one, because cold start and a dead socket look identical from outside:
| Bound | Default | Covers |
|---|---|---|
MUR_LLM_FIRST_DELTA_TIMEOUT_SECS | 300 s | the wait for the first chunk โ a cold local model |
MUR_LLM_IDLE_TIMEOUT_SECS | 120 s | the gap between chunks, once it is emitting |
A stream that goes quiet past its bound does not fail the turn. Whatever arrived is kept and visibly marked, so a truncated reply never reads as a complete one:
The first half of the answer that did arrive
[output truncated: the model stopped sending]
and the settlement card names the bound that bit, not a generic error:
โ stopped at stream interrupted (0 iterations) โ output may be incomplete ยท
the model stopped sending mid-reply โ ask it again; if this repeats on a
slow local model, raise MUR_LLM_IDLE_TIMEOUT_SECS for that agent
Nothing usable assembled is a plain timeout instead, which is retried and then routed around by the fallback chain โ safe precisely because no answer text reached you to be duplicated. A tool call the model was still spelling out when the stream stopped is dropped, never run with half-finished arguments.
Raise either bound for a slow local box. A value of 0 is refused and the default kept: a bound a stray =0 can switch off is not a bound.
Requests that are not streamed have no socket clock at all โ before the first byte, "still thinking" and "hung" cannot be told apart โ so they are governed by the turn's deadline and by cancellation, exactly as above.
Liveness is a heartbeat
While a turn runs โ through model inference included โ the agent runtime sends a turn/heartbeat frame every 30 s on the connection that dialed it. The dial gives a beating peer 90 s of silence before failing with stopped responding โ no frame for 90s (last: <time> ยท turn/heartbeat); a router that thinks for ten minutes is alive the whole time. An agent still on an older runtime (before proto 2) keeps the old 600 s idle timeout, so a mixed-version fleet does not start failing mid-upgrade โ restart agents (mur update --restart-agents) to get them on the short one.
The beat does not depend on anything the caller sent. It used to be skipped when a caller omitted the optional task_id โ which mur agent send does โ so a healthy turn on that path emitted no proof of life at all and tripped the 90 s check while the agent sat waiting on the model. The runtime now mints the id itself, so liveness is never a caller courtesy.
Fail at dispatch, not after the budget
A fleet can declare what its work needs:
# ~/.mur/fleets/develop-rust/fleet.yaml
needs:
- write_file
- edit_file
- bash
Every delegate receives that list and checks it against its own tool inventory and policy โ the same tools: rules the approval gate reads โ before its first model call. A member missing one fails at once:
cannot start: rustsmith has no write_file โ mur agent perm tool-allow rustsmith write_file
No needs:, no preflight. And any authorization refusal (not authorized: โ the fleet_run allowlist, parallel_jobs.targets, a tool policy Deny) is told to the model once and that tool is withdrawn for the rest of the turn, so a turn cannot spin on the same refusal three times.
Every stop names itself
The settlement card and the fleet rail carry the reason and the remedy:
โ stopped at deadline (17 iterations) โ output may be incomplete ยท raise it: mur limits rustsmith --deadline <1h>
See Settlement and Fleet Loops.