agent-retro-omp fires a periodic self-retrospective into a running AI coding agent session — clock-aligned or elapsed-interval, your choice (/ai/agents/productivity/2026/07/03/clock-vs-elapsed-retro-triggers-omp.html). Each retro reports what happened in the segment: tool-call tally, cost, context-window growth. Restart the agent process mid-session — a crash, a Ctrl-C, an update — and until today, all of that was gone. The next retro would report only the handful of calls made since the process came back up, silently under-reporting a segment that actually spanned hours.

The first fix I reached for was wrong, and the reason it was wrong is the actual point of this post.

The instinct: persist a checkpoint

The extension tracked two kinds of state entirely in memory:

const activityBuffer: ActivityEntry[] = [];           // every tool call this session
const segmentStarts = new Map<string, string>();       // schedule name -> ISO start time

A process restart wipes both. The obvious fix is the one every engineer reaches for first: write them to disk.

interface PersistedSegmentState {
	sessionId: string;
	segmentStarts: Record<string, string>;
	segmentCtxStartTokens: Record<string, number>;
}

Load on session_start, match against the current session id, restore if it’s the same session resuming after a restart, reset otherwise. I built this. It worked. It also added a file format, a staleness-matching rule, and a restore path that needed its own tests — real complexity, for a feature that turned out not to need it.

The question that killed it

“Can we just compute the tool calls and info for the segment — starting from T-10 to T — would it be too much computation?”

That’s the whole fix. Once you ask it, the persisted-state design falls apart on inspection:

In steady state, segStart = fireTime - intervalMs recovers the identical value the persisted map would have held. The only case where it diverges is a restart landing mid-window — and starting a fresh window there isn’t a new edge case. It’s the same “no catch-up, no backfill” rule the extension already applied to session resume. Applying it consistently to restart, too, isn’t a compromise; it’s the more coherent design.

function fire(schedule: RetroSchedule): void {
	const now = new Date().toISOString();
	const segStart = new Date(new Date(now).getTime() - schedule.intervalMs).toISOString();
	// ...
}

No file. No session-id matching. No restore path. segmentStarts doesn’t exist anymore.

The buffer was the same mistake, twice

activityBuffer — the in-memory log of every tool call — had the identical problem, and I’d already half-solved it for a different field without noticing the pattern applied here too.

Cost tracking in this extension was never a buffer. sumSegmentCost() scans sessionManager.getBranch() — the session’s own append-only transcript — for assistant messages in [segStart, now) and sums usage.cost.total. It’s additive history that’s already durable, on disk, unrelated to the extension’s own process lifetime. Once segStart is derivable, tool tally can use the exact same trick:

function getToolTally(sessionManager, segStart, segEnd) {
	const counts: Record<string, number> = {};
	let count = 0;
	for (const entry of sessionManager.getBranch()) {
		if (entry.type !== "message" || entry.message.role !== "assistant") continue;
		const ts = new Date(entry.timestamp).getTime();
		if (ts < startMs || ts >= endMs) continue;
		for (const block of entry.message.content) {
			if (block.type !== "toolCall") continue;
			counts[block.name] = (counts[block.name] ?? 0) + 1;
			count++;
		}
	}
	return { count, tally: formatTally(counts) };
}

activityBuffer and ActivityEntry disappear entirely. Nothing to lose on restart, because nothing is cached.

The one I thought was a real exception

Context-usage delta felt different. ctx.getContextUsage() presents as a live gauge — “how much context is used right now” — with no obvious history to replay. I’d built a small baseline cache for it (segmentCtxStartTokens, reseeded on every session_start) and treated that as the one legitimate exception to “everything is branch-derivable.”

It wasn’t. Every real (non-aborted, non-error) assistant message in OMP’s session log already carries:

contextSnapshot?: { promptTokens: number; nonMessageTokens: number; lastMessageTimestamp?: number }

And OMP’s own live gauge is computed from exactly this field — getContextBreakdown() walks the branch for the latest assistant message’s contextSnapshot as ground truth, only estimating the small in-flight tail after it. The “live” number and the “historical” number are the same underlying data, just read at different points in time. So:

function getCtxDelta(sessionManager, segStart, segEnd) {
	let startTokens, endTokens;
	for (const entry of sessionManager.getBranch()) {
		if (entry.type !== "message" || entry.message.role !== "assistant") continue;
		const snapshot = entry.message.contextSnapshot;
		if (!snapshot) continue;
		const total = snapshot.promptTokens + snapshot.nonMessageTokens;
		const ts = new Date(entry.timestamp).getTime();
		if (ts < startMs) startTokens = total;
		else if (ts <= endMs) endTokens = total;
	}
	return { startTokens, endTokens };
}

Baseline cache gone. resetForSession() no longer seeds anything for context tracking, because there’s nothing left to seed. The extension now holds zero cached or persisted per-segment state — tool tally, cost, and context delta are all reconstructed from the branch at fire time.

The general check

Before reaching for any persisted-state or in-memory-cache design to survive a restart, ask one question first: is this value actually a pure function of time/config, or a live gauge with genuinely no other source — or is it already denormalized onto something durable that already exists?

This is the same lesson Bastian Terric’s “The Snapshot Paradox” makes about event-sourced systems generally: reaching for a snapshot is frequently a symptom of not re-examining whether the thing you’re snapshotting is actually expensive to recompute, or whether a better-drawn boundary makes the snapshot unnecessary. Their number for a real 18-month event stream: 8,610 events, 5MB, replays in milliseconds. Sequential scan is fast; the reflex to cache is what’s expensive, in code you now have to maintain and get right on restart.

A number that looked wrong and wasn’t

Once cost and context delta were both branch-derived, an obvious next metric fell out: cost per token of net context growth, in the same $/1M unit providers advertise pricing in.

Segment: 03:40 PM → 03:50 PM (10min), 51 calls (..., +41k ctx, $4.95, $120.73/1M ctx-growth)

$120.73/1M looks broken next to Sonnet’s advertised $10/MTok output rate — a 12x gap. It isn’t a bug. The two numbers measure different things:

Every turn in an agentic loop re-sends the entire growing conversation as input, and re-caches it. A 51-call segment does 51 rounds of cache read/write against an already-large context, while net context growth that segment was only 5–41k tokens — most tool output gets consumed and doesn’t survive 1:1 into the running total. Philip Zeyliger’s “Expensively Quadratic” has the receipts: cache-read cost crosses 50% of total spend around 27,500 tokens of conversation length, and 87% by the end of a long one. The metric answers “how much did this segment’s work cost per token of context it actually kept” — an efficiency signal, not a rate card. I renamed it from $/1M ctx to $/1M ctx-growth specifically so the label doesn’t invite the wrong comparison.

Verified against a real restart

The best test of “does this actually survive a restart” is an actual restart, not a simulated one. Mid-session, the OMP process bounced (session_shutdownsession_start, 11 seconds apart). The very next retro, 25 seconds after the new process came up, correctly reported the full ten-minute window — 51 calls, spanning back to before the restart. The old design would have shown a handful of calls from the new process only. It’s a small thing to see work exactly as specified, on the first real occurrence, with no code path written for restart-recovery — because there’s nothing to recover.

Source