Extends the in-memory cache into a durable mirror. The reconcile snapshot is
written to disk on every successful reconcile; on boot the app shows it instantly
(stale-while-revalidate) instead of a blank board, and if gitea is unreachable,
reads fall back to it (offline). Rebuildable — the durable truth stays in gitea.
- snapshot-store.ts: load/save the snapshot as JSON in app userData (never throws;
corrupt/absent → "no cache"). At this scale (~34 issues, 37KB) the whole snapshot
fits in memory, so a JSON file beats indexed SQL — no query benefit yet, no
native-module (better-sqlite3/electron-rebuild) or WASM dependency. That's the
next step if the mirror ever needs indexed queries over larger data.
- gitea.ts: getSnapshot persists on a fresh pull; bootSnapshot() returns the
persisted snapshot (without seeding the cache — agents still reconcile fresh);
gitea:boot serves it; gitea:reconcile falls back to it on failure (stale:true).
- useBacklog: stale-while-revalidate — boot instantly, then a fresh reconcile
supersedes; a reconcile error keeps the shown snapshot instead of erroring.
Verified: desktop typecheck clean, 14 fixture e2e green. Live: the snapshot
persists (34 issues / 44 deps / 34 timelines / 5 milestones written to disk); a
second launch with gitea unreachable renders the full real board — NOW/NEXT/LATER
+ the Monte Carlo cone — entirely from the cache (new live-persistence e2e).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A full reconcile is ~2N gitea calls (deps + timelines per issue). Every agent
tool call (query_project) was doing a fresh one; the UI reconcile and the agent
didn't share anything. Now a single in-memory snapshot cache backs both.
- gitea.ts: getSnapshot(client, { maxAgeMs }) — reads within the window reuse the
cache; maxAgeMs:0 forces fresh. invalidateSnapshot() drops it. The explicit UI
reconcile forces fresh (and warms the cache); agent tool calls tolerate a 30s
TTL to stay responsive; applyChange + createIssues invalidate so the board and
forecast reflect the write immediately.
- model.ts: query_project reads getSnapshot (30s TTL) instead of reconciling live.
This is the SQLite mirror's cache semantics in memory — rebuildable, the durable
truth stays in gitea (purity split, D4). Persistent SQLite (offline + instant
boot) is a separate slice: Electron 34's Node 20 has no node:sqlite, so it needs
better-sqlite3 + electron-rebuild or sql.js/WASM — deferred as its own decision.
Verified: desktop typecheck clean, 14 fixture e2e green, live Reginald still
answers correctly from the cache (writes invalidate → board stays correct).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>