Skip to content

perf(agent-manager): shared incremental session-summary cache (stop full transcript re-parse on every refresh) #256

Description

@codeaholicguy

Problem

On every agent-list refresh (every 3 s in ai-devkit agent console, and on each ai-devkit agent list), the session parsers read and JSON.parse the entire transcript of every live agent. Nothing is cached, even when the file hasn't changed.

  • ClaudeSessionParser.readSession (packages/agent-manager/src/providers/claude/ClaudeSessionParser.ts) reads the whole JSONL file. It only needs the first entry's timestamp, the last conversation entry type, the last cwd, the interrupted flag, and the first/last user message.
  • CodexSessionParser.readSession (packages/agent-manager/src/providers/codex/CodexSessionParser.ts) does the same. It is called from CodexAdapter.mapSessionMappingMatches, mapRegistryCache, mapDirectMatches and the legacy matches.

Cost grows linearly with total live transcript size: CPU and memory scale with history size, not with what changed.

Evidence

Measured on a machine with 28 live agents, using the 0.65.0 build:

Measurement Value
Transcript bytes read per refresh 147 MB across 47 files
Share of that from Codex readSession 72 MB (+ 8 MB via mapRegistryCache)
Share of that from Claude readSession 28 MB
CPU per refresh ~350 ms
Event loop blocked per refresh ~630 ms (synchronous reads)
Parser cost 2.2–2.7 ms per MB
Peak RSS ~4× bytes parsed; a 400 MB transcript peaks at 1.59 GB RSS

A headless loop running listAgents every 3 s used 11.7% CPU and ~400 MB RSS. With the Claude, Codex and Antigravity adapters removed, the same loop used 0.5% CPU and 57 MB. That isolates transcript parsing as the dominant cost.

Proposed approach

  • Add one shared utility in packages/agent-manager/src/utils/ (e.g. IncrementalJsonlSummary), not a separate cache per adapter.
  • Key it by file path and store {dev, ino, size, mtimeMs, offset, pendingPartialLine, state}.
  • Each parser supplies a pure reducer, reduce(state, entry) → state, plus an initial state.
  • Read rules:
    • dev, ino, size and mtimeMs all unchanged → return the cached state (0 bytes read).
    • Same inode and size grew → read only [offset, size), reduce complete lines, and keep any trailing partial line for the next read.
    • Size shrank or inode changed → reset and rebuild.
  • Keep the cache on the adapter or parser instance; AgentManager keeps adapter instances alive across refreshes.
  • Evict entries for paths not referenced in the latest refresh.
  • Migrate the Claude and Codex adapters in this issue. The other adapters adopt the utility in follow-up issues.

Acceptance criteria

  • A shared incremental summary utility exists in agent-manager/src/utils, with unit tests.
  • Unchanged file: a second listAgents() reads 0 bytes of transcript content. A test verifies this by instrumenting fs.
  • Appended file: after appending K bytes, the refresh reads ≤ K + 64 KiB for that file.
  • Truncation/rotation: if the size shrinks or the inode changes, the summary is rebuilt and correct (test).
  • Partial trailing line: a line without a trailing newline is not parsed until complete; entries are neither dropped nor duplicated (test).
  • Equivalence: for all existing Claude and Codex fixtures, the incremental summary equals the current full-parse readSession result (property/fixture test).
  • Bounded memory: cache entries hold O(1) summary state, no message arrays or file contents, and entries for paths absent from the last refresh are evicted.
  • Claude and Codex detection paths use the utility, with no remaining full-file readFileSync in their refresh path.
  • Benchmark: an unchanged refresh with ~30 agents / ~150 MB of transcripts costs ≤ 20 ms CPU (was ~350 ms).

Out of scope

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions