milestoneStops

Function: milestoneStops()

milestoneStops(commitLog, executionTree?): Stop<Milestone>[]

Defined in: src/time-travel/milestoneStops.ts:161

Derive one stop per committed MILESTONE from a recorded commit log.

The rule, in one sentence: take footprintjs's own per-stage axis, keep the stages milestoneFor classifies, and give the survivors the log back.

Composed, never re-derived. The hard parts of the per-stage axis are already solved upstream and are not repeated here: one stop per runtimeStageId at its FIRST commit (a subflow mount commits twice, a parallel fork child commits twice and siblings interleave — every repeat is an empty bundle), the authoritative mount set read off the execution tree, the 'start' / 'end' bookends, and the id-less leading commit that carries a subflow's inputMapper seed. And since 9.89.0 the COMPOSITION itself is the port's too: filterStops checks the [start, …stages, end] shape, keeps what keep keeps, and re-partitions the log. A second implementation of any of that would be a second chance to disagree with the library about what a stage is.

The survivors re-partition the log. A stage that classifies null is not a stop, but its commits still happened, so they are folded into the stop that PRECEDES them: every kept stop's lastCommitIdx is extended to just before the next kept stop begins. Nothing is orphaned, and stateAt(stop) remains exactly "the state that existed when the next milestone's stage started". Commits before the FIRST milestone belong to 'start' for the same reason — so on a drilled cursor 'start' reads as "what this subflow began with, after its plumbing ran".

What that costs 'start', and the flag that says so. On footprintjs's own axis 'start' is the fold base: the state before ANY stage ran. Here it absorbs every stage that ran before the first milestone, so stateAt(start) is the state the first milestone READ, not the run's raw base — a real agent seeds a couple of dozen keys in seed before anything a reader would scrub to. That is the right answer for this axis (the stops must still partition the log), and since 9.18 the port marks it: the returned start carries prologue: true whenever it absorbed a stage, so a renderer that means "before anything ran" checks kind === 'start' && !prologue instead of assuming it from the kind.

The milestone rides on the stop. Every kept stop carries its classification as meta — Stop<Milestone> — so a reader asks stop.meta?.kind (or milestoneOf, which reads the same slot) rather than re-running the classifier over the id. The bookends carry none.

A log with no milestones in it at all. A non-empty log the classifier recognises nothing in — a non-agent chart handed this strategy — yields the two bookends and nothing between them: 'start' folds the whole log, 'end' folds the whole log, and jumpTo any stage id refuses with reason: 'miss'. An axis with nowhere meaningful to stand, which is the truthful shape for a run that has no milestones — not an error, and not the empty [] that says the log itself was empty.

It does not know which log it was given. The classifier reads the LOCAL segment of a stage id, so sf-llm-call#3 on a run's outer log and sf-llm-call/call-llm#7 on that mount's drilled inner history are both classified without the strategy being told which one it is holding. That is what lets one strategy serve reactMode: 'dynamic' (the call-llm bundle is in the outer log → an llm-turn stop on the outer cursor) and 'dynamic-grouped' (the outer log holds sf-llm-call mounts → iteration stops; drill() gives the inner cursor, where the same strategy finds the llm-turn, tool-call and decision stops).

Parameters

commitLog

readonly CommitBundle[]

the run's commitLog, or a subflow's own history.

executionTree?

StageSnapshot

the run's executionTree, when the caller has it — it is what makes a mount stop say kind: 'mount' rather than being guessed from bundle shape.

Returns

Stop<Milestone>[]

Example

import { timeTravel } from 'foottrace';
import { milestoneStopsStrategy } from 'agentfootprint';

const cursor = timeTravel(agent.getSnapshot()!, { strategy: milestoneStopsStrategy });
cursor.stops.map((s) => [s.label, s.meta?.kind]);
// measured on a two-turn `dynamic` run — 42 commits, 15 stops:
// [['Run start', undefined], ['Iteration', 'iteration'],
//  ['System prompt', 'slot'], ['Messages', 'slot'], ['Tools', 'slot'],
//  ['LLM turn', 'llm-turn'], ['Route', 'decision'], ['Tool call', 'tool-call'],
//  … the second turn …, ['Run end', undefined]]
cursor.stops[0].prologue; // true — `seed` and the plumbing ran before the first Iteration

On this page