The progressive cold start introduced by #1109 paints the dashboard from
recent files, then runs a background fill that parses the deferred
history and writes the per-file session cache. On a real 21k-file corpus
that fill's cache save publishes one JSON.stringify + fsync per provider
shard in a tight loop, and the whole save held the event loop long
enough that Ink's stdin reader never ran - a q or Ctrl+C pressed in the
post-paint window was swallowed, not buffered, and the user saw a
frozen terminal (#1139, #1141).
saveCache now yields to the event loop between every shard write, so
the Ink input handler fires between publishes. The yield sits BETWEEN
shards, never inside one, so each shard's temp+rename atomicity and the
lost-fence cleanup that owns the canonical envelope are preserved;
nothing marked seen without being parsed, the kill-safe resume
invariant from #1109 is intact, and the floored-parse memo key suffix
keeps warm reads byte-identical. The fill still parses every deferred
file, so money totals are unchanged.
The dashboard's useInput now exits on Ctrl+C through the same path as
q, so a raw 0x03 from the terminal lands as soon as the loop breathes.
Pinning tests:
- tests/save-cache-yields.test.ts: a setImmediate probe must interleave
with saveCache's shard writes.
- tests/dashboard-exit.test.ts: a raw \x03 exits the dashboard the
same way q does.
- tests/parser.test.ts: the copilot validated-clean re-parse test
tightens its fake-timer scope to only Date, so the new event-loop
yield inside saveCache still resolves under vitest's fake timers.
The #1109 cold-start one-shots (report, sessions, models, json/csv/
markdown, the desktop and menubar payloads) are unchanged.