ADR-054 — Monotonic active-duration + semantic loop counting
Status: accepted (2026-07-03; shipped 85ea12d7f).
Context
The studio's run console showed a run's "duration" and per-node "iteration" counts. Dogfooding whole_improve_loop (run 019f2247) surfaced two skews that made a run's telemetry actively misleading:
Active-duration counted OS suspend.
active_duration_mswas derived in the runview reducer by summing wall-clock event-timestamp windows (run_started/resumed → paused/failed/interrupted/finished). When the host was suspended overnight (23:56→05:42, a 5h46m gap with no events, the process frozen by the OS), that gap fell inside a resume→failure window with norun_pausedevent, so the reducer kept accruing it. A run with ~9h of real work displayed ~15h. A naive fix (a periodic heartbeat that subtracts large inter-event gaps) was rejected: it would wrongly subtract legitimate long LLM thinking.The engine already had the correct number:
SharedBudget.Snapshot()measures elapsed with Go's CLOCK_MONOTONIC (time.Since), which freezes during OS suspend but advances during LLM thinking — exactly the right semantics — and is preserved across resume viaRestore(time.Now().Add(-elapsed)). The display simply wasn't using it.Iteration display was an off-by-one. Logs label a node
claude_reviewer#48fromtask.Iteration(the semantic loop counterreview_loop.iteration). The studioIterationCrumbshowedposition/totalfrom the node's physical execution array, which includes resume RE-executions (a resume from a mid-iteration checkpoint re-runs that iteration), so the UI drifted above the semantic counter.
Decision
- Surface the engine's monotonic active-elapsed as the authoritative display value.
Event.ActiveMs int64(new,omitempty) is stamped on every event atAppendEventtime viaSetActiveDurationFn— the exact twin of theLogOffset/SetLogPositionFnpattern — returning the run'sSharedBudgetmonotonic elapsed in ms. The runview reducer adoptsActiveMsas theActiveDurationMsbase (max-guarded); the wall-clock event-window summation stays only as a fallback for pre-fix events (ActiveMs == 0). Cross-store + cloud-runner parity: the Mongo store and the cloud runner wireSetActiveDurationFnthe same way (the runner owns the per-run engine, so it registers the setter, twin of its log-writer wiring). - The studio shows the semantic
loop_iteration(matching thenode#Nlog label) instead of the physical execution index; the execution position moves to a tooltip. A run-level⟳ <loop> current/maxindicator (sourced fromiteration_pathmax — resume-dedup-safe — andrun_startedbounds) gives a "real loops" count distinct from any per-node execution count.
Consequences
- Run telemetry is trustworthy: OS suspend excluded, long thinking counted, resume-safe, with no heuristic threshold.
- A new persisted event field (
ActiveMs), backward compatible: old runs withActiveMs == 0render via the wall-clock fallback. - Limitation: budget-less workflows produce a nil
SharedBudget, soActiveMsstays 0 and those runs use the wall-clock fallback (documented atEvent.ActiveMs/Engine.activeBudget). The target scenario — overnight loop bots — all declare budgets. - This telemetry was the precondition for diagnosing the loop-bot yield problem behind ADR-055: without it the waste read as "15h / 49 iterations" and was invisible.
