Skip to content

fix(cli): a step outlives the pause inside it - #775

Merged
blafourcade merged 2 commits into
nextfrom
fix/a-step-outlives-its-turn
Sep 5, 2026
Merged

fix(cli): a step outlives the pause inside it#775
blafourcade merged 2 commits into
nextfrom
fix/a-step-outlives-its-turn

Conversation

@blafourcade

Copy link
Copy Markdown
Contributor

The defect

A turn_end closed a step interval. turn_end is 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:

aidd-refine:01-brainstorm   05:23:51 -> 05:55:01  closer=turn_end    1870s
aidd-orchestrator:01-sdlc   05:56:27 -> 05:59:53  closer=step_start   206s
aidd-pm:04-spec             05:59:53 -> 06:00:50  closer=step_start    57s
aidd-dev:01-plan            06:00:50 -> 06:02:34  closer=turn_end     104s

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 buildFlowIntervals and buildTaskIntervals already treat a pause as a pause, and buildStepIntervals carried its own older walk that did not.

The change

buildStepIntervals now runs through buildClosedIntervals, the shared walk the other two use, over the same [boundaries, taskDeclarations, filesWritten] merge. A step_end naming the step's own skill, or the next step_start, closes an interval. Nothing else does; every other line only contributes its moment toward the journal's last witnessed one.

Its own timed/parseableBoundaries pair 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) this branch
journal-interval 2 974
by_step unnamed 27,562 (91.2%) 26,590 (88.0%)
axes reconciling to the total 10 of 10 10 of 10

The 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 --json runs byte-identical, all eleven --axis artefacts stable across runs.

Two consequences, stated in code

  • An unclosed step is capped at the journal's own last witnessed moment, where it used to be left open. The premise for leaving it open was that this walk "saw boundaries alone" and so had no later moment to cap at; it now reads the same three arrays the other two do, so that premise is gone. Capping also bounds a real hazard: one captured session carries a single vendor_id spanning 22 days.
  • records-join can 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.ts held 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, per journal.cjs's own HOOK_EVENT_NAME_TO_CANONICAL.

Guards

New test: a step runs past a pause to the journal's last witnessed moment. Mutations run, both killed:

  • reinstate turn_end as a closer → 3 tests red
  • drop taskDeclarations/filesWritten from the walk → 4 tests red

Two existing fixtures gained a later moment so their assertions still discriminate a refused closer from a cap, rather than passing by coincidence.

Gates

pnpm test 3457 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

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
@blafourcade
blafourcade requested a review from a team as a code owner September 5, 2026 05:36
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
@blafourcade

Copy link
Copy Markdown
Contributor Author

Two guards added in 97d922d, after a review pass named them as gaps rather than defects.

check on a journal whose only line is the opener now has a test: records-join reads fail / all-unattributed. Fixing the existing diagnose fixture by giving it a pause had removed the only case that exercised the flip, so the consequence was documented and no longer asserted anywhere. Copilot fires no stop event, so that journal shape is a real host's, not a corner.

A step whose step_end shares its step_start's moment covers nothing. The journal stamps nowIso() at second resolution, 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 — the same order the walk this replaced read. Pinned because it is now inherited from the sort rather than written out.

Mutation: an unclosed step left open again turns both red.

One clarification on the figures above, since the sink stores step_attribution at catch-up and never revises it (storeNewCandidates freezes the field set). The 974 is not affected by that: withDerivedStep re-derives step attribution on every report for any session a journal covers, and rebuilds the record rather than spreading over it, so a stale stored value is never carried into a figure. Records already on disk keep the truncated attribution in the file; no report reads it.

pnpm test 3459 passed / 309 files after the addition.

@blafourcade
blafourcade merged commit 9f19b94 into next Sep 5, 2026
17 checks passed
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