Trace the loop and flag the stall - #70
Merged
Merged
Conversation
A long autonomous run can stop converging without anything erroring. Tests stick, weed stays dirty, obligations don't shrink, and the loop grinds tick after tick until a cap trips. Nothing surfaces it while it happens. The loop now appends a trace entry each tick to .allium-loop/<slug>.trace.jsonl, a snapshot of the tick's convergence state drawn from the phases' typed records. When a tick makes no progress on any convergence metric, the run says so out loud instead of waiting for the no-progress cap. The trace makes the run's trajectory observable, and the end report summarises how the metrics moved. The stall rule is simple counting over a short log, so the orchestrator applies it directly, with no tool to install. driving-the-loop gains section 13; the per-tick append, the stall surfacing and the trajectory report thread through sections 2, 4, 8 and 10. The 6 / 2 caps are unchanged.
A trace group pins the contract offline: trace entries validate against their schema, malformed ones are caught, and the stall rule (detectStall) is proven over trajectories -- a converging run stays quiet, a flat run alarms at the threshold, a recovered run does not, and the report points at where it flattened. The rule the live loop applies by hand is the rule tested here, so it cannot drift, and it is ready to move into a script or the CLI later.
The trace so far showed the loop's convergence trajectory, which the model can
see, but not two things it can't: how long each subagent call took, and whether
the calls it made were the right ones.
Timing has to be captured outside the model — a subagent call isn't a Bash call
to wrap in `date`, and the model can't read its own latency. A new loop-trace
hook, registered on the subagent tool for PreToolUse and PostToolUse, stamps each
call's start and end and appends {agent, duration_ms} to .allium-loop/timings.jsonl.
It only records while a loop is active, never blocks a call, and swallows its own
errors. It reuses the existing Node hook mechanism, so it adds no new dependency
where hooks already run, and is a clean no-op where they don't — the trajectory
and routing still stand there.
Routing is the model's half and needs no hook: each trace entry now records the
phases run and why each was chosen, so a phase that keeps running without moving
a metric is a visible wasted call. driving-the-loop §13 covers both; the loop
folds the hook's durations into the trace and report.
Tests: the trace-entry schema carries the richer phases shape (name + reason);
the hook's logic has its own unit tests (paired pre/post, FIFO fallback, no-op
without a loop, malformed input), run in CI via the hooks group, which also
checks the hook is registered on both events. A live `timinghook` probe spawns a
real subagent and confirms a timing line lands — verified against Claude Code
(matcher, payload fields and pre/post correlation all fire).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
When the loop drives a big goal it can run for many ticks. If it stops converging, with tests not improving and weed still dirty, there is no signal until it hits an iteration cap. The loop now records each tick and warns as soon as it stalls, instead of at the cap.
What it does
Each tick it appends a line to a trace file with the test counts, the weed verdict, the open questions, and which subagent ran and why. If a tick improves none of those, it flags a stall rather than waiting for the cap. A hook records how long each subagent call took. The final report shows the trace.
Implementation
.allium-loop/<slug>.trace.jsonl, built from the phases' typed records..allium-loop/timings.jsonl. It runs where hooks are supported and does nothing elsewhere.Tests
Potential next steps
The model applies the stall rule today, with the deterministic version pinned in the tests. It could move into a script or the CLI later, which would also be the natural home for the timing.