A file's shard span was read off turns[0]/turns[-1], but several providers
emit turns non-chronologically (cursor by ROWID, goose/crush/copilot by a
DESC ordering). That produced until < bucket - an empty span, so the shard
was unreachable at every scope and its sessions re-parsed every run.
cacheFileSpan now takes the min and max month over all turns.
An entry re-bucketing out of a month the run never loaded (a re-parse that
moved its oldest turn, or the #441 failure marker that has no turns at all)
left the old copy in the carried shard, so one path lived in two shards and
a later load could resolve to the stale one. A save now prunes those paths
from the shards it carries, and a load merges shards in envelope order,
resolving any duplicate to the freshest fingerprint and dirtying both
buckets so the next save retires the loser.
A carried month whose shard another writer had republished was dropped from
the envelope outright, losing expired-transcript PR orphans no re-parse can
recover. The envelope is now re-read just before publishing and the current
shard name adopted; a ref is dropped only when that envelope lacks it too.
The same re-read moves every merge read after the ownership fence and gives
the merge one optimistic retry, so the read-modify-write window shrinks to
the publish itself.
Also: retire an orphaned v8 directory / v7 file left by an interrupted
re-layout, age-guarded, once a v9 envelope is published.
Two live processes share one cache directory routinely (a one-shot CLI
beside the resident serve child, two menubar polls), and the shard layout
had two ways to lose data there.
- The atomic write used a FIXED temp name, so two writers publishing the
envelope — every save does — shared one `envelope.json.tmp` and
interleaved into a torn payload, or one deleted the shards the other's
envelope named. 39 of 40 rounds ended in a total cache loss. The temp
name carries a nonce again, as it did before the shard layout.
- A save reused a shard filename from its own load snapshot without
checking the file was still there. Another process republishing that
provider unlinks the old shard, so the stale writer published an
envelope naming a deleted file — read back as a corrupt provider and
dropped whole, including PR-linked orphans no re-parse can recover. A
reused shard is now existence-checked, and re-verified once more
immediately before the envelope is published; a vanished one is
rewritten from memory.
Also:
- Progress saves take a 30s floor beside the file counter. Only the
claude scan reports per file; every other provider calls saveProgress
once at its own boundary, so the counter alone never fired there.
- The unreferenced-shard sweep waits an hour (temps still 5 minutes): an
unreferenced shard may belong to a concurrent save whose envelope has
not landed yet.
- The sweep also retires the pre-v8 single-file temps in the parent
directory, which nothing writes anymore.
- The shard directory is created 0o700.
- The claude and provider paths mark the cache dirty where they DELETE a
stale entry, not only where they replace it: an unreadable file skips
the replace, and the deletion would otherwise live only in memory.
- Codex only treats a grown file as an append when the recorded boundary
still lands just after a newline, so a same-inode rewrite that happens
to end up larger re-parses instead of resuming mid-line.
A warm launch rewrote the entire session cache whenever any provider
appended a few KB: on a 6 GB corpus that is a 155 MB stringify + fsync
every run. The on-disk cache is now a version-suffixed directory holding
one shard per provider plus a small envelope, and a save rewrites only
the providers marked dirty.
- Dirtiness is tracked per provider (markCacheDirty) instead of one
global flag, so an appended Claude session no longer republishes
Codex, Copilot and the rest.
- Shards carry a nonce in their filename and the envelope is renamed
last, so a save is published at a single point: readers never see a
half-updated set, and a writer that loses the refresh ownership fence
leaves the canonical shards untouched.
- A shard that fails validation is treated as an absent provider rather
than rejecting the whole cache, so one malformed turn costs one
provider's re-parse instead of every provider's history.
- v7 migrates losslessly: the blob is re-laid-out into shards and
removed only once that save publishes. Nothing re-parses.
- Cold-parse progress saves now trigger every N files parsed rather than
every 5s, so a slow cold parse no longer rewrites the growing cache on
a wall clock.