fix(cli): a step outlives the pause inside it - #775
Conversation
A `turn_end` closed a step interval, so a skill spanning several prompts was credited with its first turn and nothing after. It is a pause, not the end of a step, which is the rule `buildTaskIntervals` and `buildFlowIntervals` already read from this very journal. Measured on the one orchestrated session captured, 2026-09-04: four steps opened across four hours of continuous work, every one closed by the next pause, the last at 06:02:34 against a session that ran until 09:27:21. Of its 1,073 records, 69 fell inside a step interval. The same journal gave the flow axis 1,052 records and this axis 1 — two walks over identical evidence disagreeing by three orders of magnitude. `buildStepIntervals` now runs through `buildClosedIntervals`, the shared walk the other two already use, over the same three-array merge. A `step_end` naming the step's own skill or the next `step_start` closes an interval; nothing else does. On a 30-day report holding 30,222 requests, `journal-interval` goes from 2 to 974, and all ten axes still reconcile to the period total. Two consequences, both stated in code rather than discovered later: - An unclosed step is capped at the journal's own last witnessed moment instead of left open. The premise for leaving it open — that this walk saw no later moments to cap at — is false now it reads `filesWritten` and `taskDeclarations` like the other two. - A journal whose only line is the opener witnesses no later moment, so the step it opened covers nothing, and `records-join` can read fail where it read ok. Found by running it. Failing there is the honest answer: nothing in such a journal says the step was still running. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Two guards for consequences the change states but nothing asserted. `check` on a journal whose only line is the opener: the step covers nothing, so a record carrying no step of its own joins nothing and `records-join` reads fail. That is a real host — Copilot fires no stop event — and `check` is the command whose job is saying measurement is broken, so the verdict belongs in a test rather than in a doc comment alone. A step whose end shares its start's moment covers nothing. The journal stamps `nowIso()`, whose resolution is the second, so two lines sharing a moment is the common case; the shared walk orders them by a stable sort, which for two `boundaries` entries is file order. Pinned because that ordering is inherited from the sort now rather than written out. Mutation run: an unclosed step left open again turns both red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
|
Two guards added in 97d922d, after a review pass named them as gaps rather than defects.
A step whose Mutation: an unclosed step left open again turns both red. One clarification on the figures above, since the sink stores
|
The defect
A
turn_endclosed a step interval.turn_endis written on every Stop, so a skill spanning several prompts was credited with its first turn and nothing after.Measured on the one orchestrated session captured, 2026-09-04:
Four hours of continuous orchestrated work; the last interval closes at 06:02:34 and the session runs until 09:27:21. Of the session's 1,073 records, 69 fell inside a step interval.
The same journal gave the flow axis 1,052 records and the step axis 1. Two walks over identical evidence, disagreeing by three orders of magnitude — because
buildFlowIntervalsandbuildTaskIntervalsalready treat a pause as a pause, andbuildStepIntervalscarried its own older walk that did not.The change
buildStepIntervalsnow runs throughbuildClosedIntervals, the shared walk the other two use, over the same[boundaries, taskDeclarations, filesWritten]merge. Astep_endnaming the step's own skill, or the nextstep_start, closes an interval. Nothing else does; every other line only contributes its moment toward the journal's last witnessed one.Its own
timed/parseableBoundariespair and closer scan are deleted — that duplication is how the two readings came to disagree.Measured on real data
30-day report, 30,222 requests, same sink read by both binaries:
next(a6f7412)journal-intervalby_stepunnamedThe global figure moves little because one captured session holds 98% of the records and its journal opens no step at all. On the session that is journalled, coverage goes from 69 records to 974.
Determinism unchanged: three
--jsonruns byte-identical, all eleven--axisartefacts stable across runs.Two consequences, stated in code
vendor_idspanning 22 days.records-joincan read fail where it read ok, for a journal whose only line is the opener and whose records carry no tool-stated step. Found by running it —diagnose-telemetry-use-case.unit.test.tsheld exactly that journal. Failing there is the honest answer: nothing in such a journal says the step was still running. Every host that writes a pause is unaffected — Claude Code, Cursor and OpenCode, perjournal.cjs's ownHOOK_EVENT_NAME_TO_CANONICAL.Guards
New test: a step runs past a pause to the journal's last witnessed moment. Mutations run, both killed:
turn_endas a closer → 3 tests redtaskDeclarations/filesWrittenfrom the walk → 4 tests redTwo existing fixtures gained a later moment so their assertions still discriminate a refused closer from a cap, rather than passing by coincidence.
Gates
pnpm test3457 passed / 309 files · typecheck ·biome ci(2 pre-existing warnings, unchanged) · knip · jscpd · layering · bundle 594.3 KB / 596 KB (down 0.5 KB) · repository scripts 371 passed.🤖 Generated with Claude Code
https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp