Stop the silent worker, but let a finished one leave
Two stops Mullet 2.3.0 puts on a coding worker: an idle limit for one that has gone quiet, and a short grace window for one that has finished but hasn’t exited. And what goes into the commit once the gates have passed its work.
A Mullet worker runs with nobody watching, so it needs limits that don’t depend on someone noticing. Until 2.3.0 there was one that mattered, the wall clock, and it was too blunt for two cases.
A Claude worker that stalled held the machine-wide Claude slot until its wall clock ran out, an hour by default, and every other Claude step in every job waited behind it. And a CLI that hung after printing its final result made the runner wait out the whole wall clock too, for work that was already done. The opencode worker had other stops, on its wall clock, 20 seconds after a fatal error and once past its spend, but not these two.
The idle limit
Both executors now watch their worker. Every line on its output stream and every byte it writes to stderr counts as output. A worker that prints nothing for the idle limit has its whole process group killed.
The attempt is recorded as an abort and a provider error, with the reason “stopped: no output for N minutes”. Like the spend and provider stops, it doesn’t count against the worker. A silent worker might be a hung CLI or a network that went away, and neither says anything about whether the model could do the step. On the delegation ladder the step moves up without using one of its attempts.
The limit is jobs.idle_minutes. When it’s unset, Mullet works it out from the repo’s profile: the larger of 15 minutes and 1.5 times the profile’s longest check or test timeout. A worker that runs the slowest check itself prints nothing until the check ends, and stopping it then would kill a worker for doing exactly what it was asked.
That has a cost, and the decision record says so. With the default profile, whose test timeout is an hour, the derived limit is 90 minutes, which is longer than the default wall clock. So with defaults, the idle stop never comes first. It only does with a profile that has shorter timeouts, or with jobs.idle_minutes set. A worker that thinks silently for longer than the limit is stopped, and raising the setting is the fix.
It also doesn’t catch everything. A worker that prints a line every few minutes while getting nowhere resets the clock each time. The wall clock and the spending limits are what bound that one.
Finished is a claim, not a fact
The second stop is for a worker that has said it’s finished. For Claude that’s its result event. For opencode it’s a last step that ends with stop, or one with text and no tool calls when opencode gives no reason. From then on, only the grace window counts.
The window is jobs.grace_seconds, 60 by default and never less than 5. The floor is there because Claude Code saves its session file after its final reply, and a later attempt that resumes the session needs that file. Once the window is up, the process group is killed and the attempt counts as finished.
Finished doesn’t mean passed. The gates judge the work either way, the same as after a timeout. A worker that exits cleanly with a broken change fails its gates, and one that hung after a good change passes them. What the worker said about its work decides nothing. When I read 101 attempts, workers said done 72 times and 30 of those failed a gate.
For a Claude worker, output after the final reply doesn’t buy more time: once the result event has arrived, the idle clock no longer applies and the grace window is all that’s left. An opencode worker that starts a new step after a reply that looked final has carried on working, so the idle clock takes over again.
All three settings sit under jobs: in the config file:
jobs:
idle_minutes: null # minutes of silence before a worker is stopped; null derives it
grace_seconds: 60 # seconds a worker may take to exit after its final reply (5 to 3600)
commit_body: full # full or minimal: what goes under each step commit's subject
A bad value, including YAML’s .inf and .nan, is reported by mullet config show and the default is kept. It never stops a job.
Hostile text in a commit message
When a step passes, Mullet commits it. The subject is the step’s goal. Under it, with commit_body: full, go the step, its kind, the gates it passed and the worker’s own summary. For one of the tests’ fixtures the message comes out like this:
add a sub function next to add
Mullet step n1 (mechanical) passed gates G0, G1.
----- worker's claim, not verified (status: done) -----
> added sub()
> and a test
----- end of worker's claim -----
The summary is text a model wrote, perhaps after reading something hostile, and a commit message isn’t only read by people. Tools parse trailers at the end of it: Signed-off-by:, Co-Authored-By:, Fixes:. A worker that could end its summary with one of those could forge a sign-off on a commit. So every line of the claim starts with > , and none can parse as a trailer. Control characters and escape sequences are stripped, and the claim is capped at 1,500 characters. A test feeds in a summary made of trailer lines and checks that git interpret-trailers finds none.
Each reply is also written to the ledger as a worker_claim event, with its status (done, blocked, split or unknown), whether the attempt was aborted and whether it was stopped for being idle. It’s a record only. That’s what lets me count the claims at all.
Subject-only commits
Some repos forbid tool or AI lines in commit messages. For those, jobs.commit_body: minimal commits each step with its subject alone. The gate results stay in the job’s evidence report either way, so nothing about the checking is lost.
What these stops don’t do
They don’t make a stalled worker’s work any good. They stop it holding a slot, or holding the runner, for longer than it needs. Whether the work was any good is still for the gates, and Gates, not a model’s word says how they decide. These are part of Mullet 2.3.0, released on 26 October 2026.
The code is at https://github.com/tomgould/mullet-public.
More on Mullet
-
Cheap models only for the work that is safe to be wrong about
Mullet tries free and cheap models first only on steps its gates can judge, and keeps risky and one-way steps for Claude and its reviewer. Why…
-
Decision records for a solo project
Mullet now keeps eleven short decision records beside its code, each a context, a decision and its consequences. Why a one-person project bothers…
-
Mullet from Python
Mullet 2.3.0 adds mullet.api, so a script or CI can plan, approve, run and merge coding jobs without the CLI. It was built by splitting the CLI’s…
-
A container backend stricter than the usual Docker recipe
Mullet 2.3.0 can run a job’s commands in Docker: one throwaway container per command, no network, no capabilities, secrets on stdin and a lifeline…
More writing
-
A Markdown version of every page, for AI agents
Why every page on this site now has a Markdown version for coding agents, the three ways an agent finds it, what goes into it, and how it stays out…
-
A TV remote from the command line
Pairing a Linux computer with a Google TV over the protocol phone remotes use, what the six-character code on the screen really checks, and why a…
-
The packages I kept on a Google TV
The other half of decluttering a TCL Google TV: a keep-list of 35 packages that debloat lists remove or that only look disposable, how each one got…
-
Letting an agent drive the TV
The rules a coding agent worked under while it changed a TV we were still watching: only changes that can be undone, small batches with a person…