# Server: the last two IO hotspots — scheduler ticks and chat-history loads

The disk-IO audit that produced the trace index left two hotspots standing; both are closed.

## Scheduler ticks and project-config reads

The 30-second scheduler tick used to re-read every schedule file of every agent and re-parse the same `.project_config.toml` once per schedule file, forever, with no change detection — the only unconditional recurring disk load in the process. Schedule scanning is now cached per agent, keyed on the schedule directory's mtime plus a per-file mtime/size check (the per-file stat closes the POSIX blind spot where an in-place edit never touches the directory mtime, so hand-edited files are still picked up on the next tick); the agents-dir enumeration itself is gated the same way, and the "fresh mtime is never cached as clean" sentinel moved to a shared `internal/mtime-gate.ts` that the trace index now imports too — the contract lives in one place. `.project_config.toml` gains a parsed-table cache inside `ProjectConfigService`, stat-checked before reuse and invalidated synchronously by every service write path, so scheduler validation, the schedules routes and usage pricing all share one parse per on-disk version while hand edits still land. Steady state per tick: a handful of stats, zero file reads; scheduling behavior, listing order and validation semantics are byte-identical, and tests pin the zero-read steady state with fs probes.

## Chat-history pagination

`GET /api/sessions/:id/messages` rebuilt a session's entire transcript per request — every shard read whole, subagent sessions recursively included, all of it re-done on every SSE resync. The endpoint now takes additive cursor params: `tailLimit` returns the newest window (with the running session's live attachment, same capture semantics), `before=<shard>:<ordinal>` pages older history; without params the response stays byte-identical to before, pinned by a raw-byte test. Windows cut on Task boundaries only — tool-call pairings, compaction spans and grouped sends are never split — and only the shards overlapping the window are read, with shard inventory from the trace index and older shards' turn counts and token totals served from a per-shard `page_stats` prefix cache (a derived, versioned column added idempotently to existing databases; no user action). The web loads the tail first (200 units) and backfills 100 at a time on scroll-up with anchored prepends; a resync splices onto the retained prefix only when epoch and cursor continuity are provable, anything doubtful falling back to the legacy full refetch. The conversation outline keeps globally correct turn numbering over partial history via a server-supplied earlier-turn count, and the header token/elapsed figures stay truthful through seeded prefix stats.
