Preserve and re-key durable Copilot history across fingerprint and privacy-key changes, retain local sent-ledger aliases for all affected providers, and retry transient durable reads without clearing cached turns.
Restore the published observation 0.2.0 contract byte-for-byte and move the model identifier hardening to observation 0.3.0.
Conflict: packages/cli/src/daily-cache.ts. #926 landed
DAILY_CACHE_VERSION/MIN_SUPPORTED_VERSION at 23 for the structural codex
discovery re-derivation; this branch had claimed 17 for the midnight-straddle
re-derivation. Kept #926's rationale as the foundation and took 24, the next
free number, with the straddle reason appended: per-call day slicing changes
which day a historical straddling call lands on, so it needs its own one-time
re-derivation and cannot ride on a 23 cache written by a #926 binary.
Ports upstream fixes for two defects this branch never received.
**A turn that spans local midnight was filtered as a unit.** The range and day
filter keyed on the turn's FIRST call, so every later call that landed in the
requested day was discarded along with it. A long autonomous Codex run, or
Claude work that crossed midnight, made `codeburn today` under-report until the
turn ended, and multi-day totals attributed the whole turn to its start day.
Range and day filters now slice inside the turn: only the calls inside the
requested window survive, and the turn's timestamp re-anchors to its first
surviving call so turn-anchored rollups — category, editTurns, oneShotTurns,
the daily cache — land on the day the retained calls actually happened. Cost,
calls, savings and tokens bucket under each call's own local day, so day N plus
day N+1 conserves the whole-range total. A sliced turn is still classified from
its FULL call list, because category, hasEdits and retries describe the whole
exchange rather than the surviving slice — matching the Claude path.
**The inverse leak hit provider-filtered runs.** `--provider <other>` still
entered the claude scan, whose orphan pass read the entire cached claude
section, treated every cached PR-bearing transcript as no-longer-discovered and
re-injected it. By Project and By Model listed Anthropic spend under
`--provider cursor` while the headline showed cursor alone. The scan is now
guarded by an explicit in-scope check — deliberately not a directory-count
check, so when claude IS in scope but every transcript has been pruned,
PR-attributed orphans still survive.
**The daily cache is bumped to 17**, because leaving it at 15 would double-count
the post-midnight half of a straddling turn for an upgrading user. A v15
rollup finalized by the pre-fix binary holds the WHOLE turn on its start day,
and the new slicing then also puts the post-midnight call on the next day —
the same cost twice, in a cache whose ten-year retention never ages it out.
The bump mints a fresh filename; adoption marks the merged result incomplete,
so the next hydration re-derives every day whose sources survive under
per-call bucketing and carries forward only what it cannot re-derive.
16 is deliberately skipped: main already spent it on the codex
structural-discovery fix (eece4cf), so claiming 16 here would load a
main-built v16 cache — which holds only the codex fix — as current and
complete and the invalidation would never fire.
Blast radius: daily-history rows, the JSON daily fallback and range-query
session totals change shape for straddling days, as call-derived values move
to the call's own day — the intended correction, asserted by this commit's
tests. The session-cache FORMAT is unchanged.
One caveat worth stating rather than leaving to be found. The multi-day
all-provider By Activity rollup still derives today's slice from the unsliced
range parse, so a straddling turn's category cost stays anchored on its start
day and categories can sum below the headline on that surface; upstream has a
follow-up for it.
Second-reviewer findings on top of the port (f3f5814). Test-only.
cache-refresh-lock-process: afterEach removed the temp roots but never
killed the spawned workers, which block on their barrier files
indefinitely. The success path reaps them (waitForExit after each run),
but a failed assertion or waitFor timeout leaves the blocked winner
behind; with the global retry: 2, every attempt then spawns a fresh pair
on top of the leaked ones, so they accumulate. Verified: a forced
assertion failure left 3 stray worker processes after the run before
this fix, and 0 after, with the kill path running between retry
attempts. afterEach now kills every child it spawned and waits for it to
actually die (SIGTERM, then SIGKILL after 1s), is robust to children
that already exited (exitCode !== null short-circuits), and detaches
waitForExit listeners first so a SIGTERM cannot surface an unhandled
rejection on top of the real failure.
Upstream 2a4b8f2 has the identical leaky afterEach, so this is not a
regression the port introduces; it is a latent leak the retry makes
reachable.
Same file: worker() resolved its fixture relative to process.cwd(), so
`vitest run --root packages/cli` from the repo root spawned children at
a nonexistent tests/fixtures path and every one died on ENOENT before
touching a barrier. Resolve the fixture relative to this file
(import.meta.dirname) so the suite works from any invocation cwd.
parser.test.ts createJsonlSession: the fixture dated events at exactly
now minus two days. That is safely inside the 90-day retention window
but sits exactly ON the 48h 'recent' cutoff in optimize.ts
(RECENT_WINDOW_MS — recent iff ts >= now-48h), so any clock skew between
the helper and a consumer flips the classification, and a current-month
date range (cli-date.ts `month`, which starts at local midnight of the
1st) would exclude it on the 1st-2nd. Moved the offset to 6h, clamped
into the current month. Six hours keeps the events unambiguously recent
(42h clear of the cutoff) and inside retention with ~89 days of
headroom; the clamp keeps them inside any current-month range in any
timezone (it uses the same local-calendar construction as cli-date.ts).
Both existing cases that use this helper, (a) and (f), pass with the new
offset.
The CLI suite is red on this branch for two reasons that upstream already
fixed and that never made it here.
parser.test.ts (a)/(f): createJsonlSession stamped fixture events at a fixed
2026-05-01. Durable providers age out at 90 days, so on 2026-07-30 the fixture
silently pruned to zero and both cases began failing with `expected +0 to be
200`. Confirmed directly: the identical fixture dated relative to now yields
one project and 200 output tokens; with the literal it yields none. Date the
events relative to now so they stay inside the window whenever the suite runs.
vitest.config.ts / cache-refresh-lock: a handful of integration tests exercise
real servers, spawned subprocesses and real filesystem locks, and starve under
a saturated parallel run — failing closed, which is correct behaviour but not
what those tests measure. A small global retry rides that out; a real
regression is deterministic and fails every attempt.
Two upstream hunks are deliberately not ported. cli-durable-totals already has
an equivalent fix on this branch (ec449af) that clamps to a fraction of the
elapsed day rather than a fixed offset. The `corrupt lock recovery` describe
does not exist here, because upstream's d514459 has not been ported either.
Test-only; no production code changed.