Found while profiling the dashboard event-loop stall for PAN-4543.
What is still blocking
GET /api/costs/stream (src/dashboard/server/routes/costs.ts, search readEvents({ startDate: since, limit }) : tailEvents(limit)) answers with a synchronous full scan of ~/.overdeck/costs/events.jsonl through scanEventLinesSync (src/lib/costs/events.ts):
tailEvents(limit) walks every line of the log to keep the last N.
readEvents({ startDate, limit }) walks every line older than since before it reaches the window.
On this machine the log is 386 MB. One synchronous scan measured ~1.6 s of blocked event loop (readEvents timed out of process, 2026-10-04). The Pipeline view polls this route through useCostStream (src/dashboard/frontend/src/hooks/useCostStream.ts, refetchIntervalInBackground: true), so every poll freezes every HTTP and WebSocket request on the dashboard for ~1.6 s.
What PAN-4543 already fixed
GET /api/costs/summary (God View polls it every 30 s, project overview every 60 s) did three synchronous scans per request, ~4.8 s blocked. That matched the 4.05 s / 4.66 s /api/health stalls that failed DoD row 8 on 6 of 22 close-outs. PAN-4543 added readEventsSince() (async chunked reader, max event-loop block measured at 10 ms) and the summary route now does one async scan.
Proposed fix
Give the stream route an async read path: an async tail (read backwards from the end of the file in chunks, or reuse readEventsSince for the since case and an async tail for the no-since case). Then audit the other request-reachable callers of scanEventLinesSync / readEvents / forEachCostEvent in the dashboard server. Keep the synchronous variants only for documented CLI callers (src/lib/costs/retention.ts), per docs/EFFECT-BRIDGING.md.
Acceptance
/api/costs/stream never blocks the event loop for more than ~50 ms on a 400 MB log (measure with monitorEventLoopDelay).
- Unit test with a multi-chunk log proving the read yields the event loop (see
tests/unit/lib/costs/read-events-since.test.ts from PAN-4543 for the pattern).
Found while profiling the dashboard event-loop stall for PAN-4543.
What is still blocking
GET /api/costs/stream(src/dashboard/server/routes/costs.ts, searchreadEvents({ startDate: since, limit }) : tailEvents(limit)) answers with a synchronous full scan of~/.overdeck/costs/events.jsonlthroughscanEventLinesSync(src/lib/costs/events.ts):tailEvents(limit)walks every line of the log to keep the last N.readEvents({ startDate, limit })walks every line older thansincebefore it reaches the window.On this machine the log is 386 MB. One synchronous scan measured ~1.6 s of blocked event loop (
readEventstimed out of process, 2026-10-04). The Pipeline view polls this route throughuseCostStream(src/dashboard/frontend/src/hooks/useCostStream.ts,refetchIntervalInBackground: true), so every poll freezes every HTTP and WebSocket request on the dashboard for ~1.6 s.What PAN-4543 already fixed
GET /api/costs/summary(God View polls it every 30 s, project overview every 60 s) did three synchronous scans per request, ~4.8 s blocked. That matched the 4.05 s / 4.66 s/api/healthstalls that failed DoD row 8 on 6 of 22 close-outs. PAN-4543 addedreadEventsSince()(async chunked reader, max event-loop block measured at 10 ms) and the summary route now does one async scan.Proposed fix
Give the stream route an async read path: an async tail (read backwards from the end of the file in chunks, or reuse
readEventsSincefor thesincecase and an async tail for the no-sincecase). Then audit the other request-reachable callers ofscanEventLinesSync/readEvents/forEachCostEventin the dashboard server. Keep the synchronous variants only for documented CLI callers (src/lib/costs/retention.ts), perdocs/EFFECT-BRIDGING.md.Acceptance
/api/costs/streamnever blocks the event loop for more than ~50 ms on a 400 MB log (measure withmonitorEventLoopDelay).tests/unit/lib/costs/read-events-since.test.tsfrom PAN-4543 for the pattern).