Skip to content

Trace the loop and flag the stall - #70

Merged
yavorpanayotov merged 4 commits into
juxt:mainfrom
panayotovk:loop-telemetry
Aug 22, 2026
Merged

Trace the loop and flag the stall#70
yavorpanayotov merged 4 commits into
juxt:mainfrom
panayotovk:loop-telemetry

Conversation

@yavorpanayotov

@yavorpanayotov yavorpanayotov commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

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

  • One trace entry per tick in .allium-loop/<slug>.trace.jsonl, built from the phases' typed records.
  • The stall rule is simple counting over the trace, so the model applies it directly. The threshold is a config value.
  • A hook times each subagent call into .allium-loop/timings.jsonl. It runs where hooks are supported and does nothing elsewhere.
  • Each entry records which phase ran and why, so a wasted call is visible.

Tests

  • Offline: trace entries validate, malformed ones are caught, and the stall rule is proven over trajectories.
  • The hook has unit tests in CI, plus a live check that spawns a real subagent and confirms a timing line lands.

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.

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).
@yavorpanayotov
yavorpanayotov merged commit b18580c into juxt:main Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant