skip to content
Jobs

Salvage the Work a Timeout Would Have Thrown Away

Kill a coding agent for stalling, not for being slow — and keep the work it already finished

Gantry milestones
Kill a coding agent for stalling, not for being slow — and keep the work it already finished

Gantry gives each coding agent a limit and kills it if it runs too long. That limit used to be a fixed amount of wall-clock time, and it was the wrong signal in two ways. It killed agents that were still working — a long job and a stuck job looked identical from the outside — and when it fired at the end of the coding step, it discarded whatever the agent had built and stopped the run with "this is too big, split it up." The trigger for that had already happened once for real: an agent left a worktree whose tests all passed, hit the ceiling, and had its finished, passing work thrown away.

This job fixed both halves. The limit is now a stall guard: it fires only when an agent has produced nothing at all for a long, generous window, so a slow-but-active agent is left alone. A far larger absolute ceiling stays behind it as a runaway backstop for agents whose harness gives no live signal to watch. And when a ceiling does fire on the coding step, Gantry no longer throws the work out on sight — it commits what is there and runs the test suite over it. If the tests pass, the work is kept and the run moves on; only genuinely incomplete work still stops the run.

Build

The plan split the wrong-signal fix from the salvage fix, and made the core change a reuse of machinery Gantry already trusted — a truncated coding stream was already allowed to fall through to the tests rather than stop the run.

The decomposition separated two changes that read as one feature. The first sprint changed what "too long" means — a stall rather than elapsed time — while deliberately preserving the existing "oversized" outcome that the kill produces. That preservation is what let the change stand on its own: the decision was factored into a pure helper that takes the elapsed-since-progress, the total elapsed, the stall window, the absolute ceiling, and whether the harness streams a live signal, and returns whether to reap. That helper could be unit-tested exhaustively — stalled reaps, busy never reaps however long it runs, non-streaming reaps only at the ceiling — with no salvage logic existing anywhere yet.

The second sprint was the core fix, and its architecture is why the whole run stayed green with no gate sending a sprint back and no review forcing a re-plan. It did not build a new mechanism to decide the fate of a timed-out chunk. Gantry already had one: a truncated coding stream, whose verdict could not be read, was already routed *past* the stop and down to ground truth — check the worktree, run the tests, repair once if red. This sprint made the "oversized" outcome from a fired ceiling reach that same path, but only for the coding step. Every other step — planner, test-builder, fix, review — keeps the terminal arm, because none of them leaves a tested worktree to salvage against. Pointing one more outcome at proven machinery is a small, well-grounded change, which is the shape of a cut that dissolves its difficulty in advance rather than fighting it at a boundary.

The third sprint followed from the first two: once a ceiling could end in kept, passing work, the detector that halts a run on repeated timeouts had to stop counting a salvaged-green chunk as a timeout, or a legitimately long job could trip the systemic halt. The last sprint was documentation. The order let each piece lean only on what the piece before it had already established.

Feature

The stall guard, the per-harness selection between it and an absolute ceiling, the per-unit overrides, and the salvage path for a timed-out coding step are all present at today's HEAD — living in a module that has since been reorganized around them.

Before this job, a coding agent's time limit was terminal and lossy. Hitting it stopped the run and discarded the work, whether or not that work was complete and passing — the incident that motivated the job was exactly this, gate-passing work thrown away at the ceiling.

What stands now is a two-part limit selected by what the harness can tell Gantry. The reaping decision lives in a pure `should_reap` helper in the agent poll loop: for a harness that streams usage, it watches the stall window; for one that does not, it falls back to the large absolute ceiling. The config carries a `no_progress_secs` stall window alongside the repurposed absolute-ceiling setting, and a sprint can override either through `timeout_secs:` and `no_progress_secs:` front-matter — both still parsed, and now surfaced in the plan module's documentation where before they were silent. Each harness declares whether it streams usage through a one-line capability flag, which is what the guard selects on.

The salvage path is intact and checkable. When the coding step returns an oversized outcome, the failure handler returns "no stop" for it exactly as it does for a truncated stream, and the pipeline emits an honest note that the guard fired rather than claiming the work was implemented — then runs the tests to judge. The map driver carries a comment marking where its per-unit path differs from the incident's milestone-build path. The one visible drift is structural, not a loss: the single large `build.rs` the job edited no longer exists at HEAD — it has been split into a `build/` module directory, and the salvage routing now lives in that module's agent and sprint-context files. The README prose the job added has likewise moved into the `docs/` tree. The behaviour the job built is still the behaviour that ships.

unavailable sessions
unavailable context
51,164 in / 122,964 out tokens
$18.50 cost
0 x 4 milestones x sprints
unavailable edits
unavailable commands
37m duration
unavailable roles
0 x 0 fixes x replans
- harness