Skip to content

perf(runtime): cap long-lived runtime caches to stop daemon memory drift#201

Merged
LIU9293 merged 1 commit into
mainfrom
feat/bounded-runtime-caches-7697
Apr 21, 2026
Merged

perf(runtime): cap long-lived runtime caches to stop daemon memory drift#201
LIU9293 merged 1 commit into
mainfrom
feat/bounded-runtime-caches-7697

Conversation

@LIU9293

@LIU9293 LIU9293 commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

The daemon runs forever, so a handful of in-memory caches that never shrink were leaking steadily across its lifetime. This PR caps the ones that are safe to cap and also trims the largest single contributor (raw SDK event payloads) before they land in the live-event history.

What changed

1) Truncate raw event payloads before storing them in liveEventHistory

  • New packages/utils/event-truncation.ts: generic recursive truncator (4KB per string default).
  • packages/core/kernel/request-run.ts: call truncateEventPayload() before push.
  • The live-status renderer only reads tool input fields (30–200 char previews) and never tool output / assistant body, so the truncation is invisible to UI output — see the stability test.

2) Shared BoundedSet / BoundedMap

  • New packages/utils/bounded-collections.ts: Set/Map-compatible FIFO containers.
    • Eviction only on overflow, no order refresh on re-insert (matches the bespoke variant that lived in message-updates.ts).
  • Refactored packages/core/runtime/message-updates.ts to reuse them — removes duplicated classes.

3) Apply BoundedSet / BoundedMap to the caches where eviction is safe

Cache Location Cap Overflow behaviour
finalizedMessages core/runtime/message-updates.ts 5000 Stale message ids never receive further updates
rateLimitedMessages same 5000 Same
rateLimitErrors same 5000 Same
newSessions (per agent) agents/{claude,gemini,goose,kilo,kiro,qwen}/client.ts 1000 An evicted id simply loses its "this is a new session" flag
  • agents/runtime/cli-session.ts: widen the newSessions? param to a structural { add } contract so agent clients can pass either a raw Set or a BoundedSet without forcing a cross-package dep here.

Explicitly NOT capped: sessionProviders

The agent adapter's session → provider map is used by abortSession, subscribeToSession, ensureSession, and the task / cron schedulers to dispatch calls to the correct agent client. Evicting entries and falling back to the default provider would misroute calls for still-valid non-opencode sessions once the cap is exceeded (flagged by Codex review on the first revision of this PR). It stays an unbounded correctness index; its growth is bounded in practice by the number of unique session ids the daemon has observed.

Expected memory impact

  • Per-turn peak: truncating to 4KB/string cuts long-turn live event history from several MB down to hundreds of KB (tool outputs dominate).
  • Daemon-lifetime baseline: the bounded caches replace three unbounded Sets/Maps in message-updates plus six unbounded newSessions sets; at worst each now stays under its cap forever instead of growing with every new message / session id.

Tests

  • packages/utils/test/event-truncation.test.ts — 9 unit cases.
  • packages/utils/test/bounded-collections.test.ts — 12 unit cases (FIFO order, no-refresh-on-re-set, delete/clear, Set/Map-compatible add/set return value).
  • packages/live-status-harness/test/truncation-stability.test.ts — replays 6 captured harness fixtures and asserts rendered status is byte-identical with and without truncation.
  • Full bun test: 314 pass, 1 skip, 0 fail.
  • tsc --noEmit: no new errors (pre-existing web-ui Svelte module-resolution noise is unrelated).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 849c9a2902

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/agents/adapter.ts Outdated
@LIU9293
LIU9293 force-pushed the feat/bounded-runtime-caches-7697 branch from 849c9a2 to f3c3097 Compare April 21, 2026 04:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3c309766b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/kernel/request-run.ts Outdated
The daemon is a long-lived singleton, so several in-memory caches that
never shrink leak steadily for the life of the process:

- liveEventHistory entries retain entire raw SDK events (tool outputs,
  file contents, subagent replies can be 2-50KB each) during a turn.
- finalizedMessages / rateLimitedMessages / rateLimitErrors in the
  message-update layer record one entry per finalized message, forever.
- newSessions in each CLI agent client (claude/gemini/goose/kilo/kiro/
  qwen) records abandoned session ids that are never consumed.

This change introduces two fixes:

1) Deep-truncate raw event payloads before pushing them into the live
   event history (4KB per string, default). The live-status UI only
   renders tool input fields (30-200 char previews) and never the raw
   tool output, so the truncation is invisible to the renderer.

   Exception: assistant `text` parts inside `message.part.updated`
   events (and their `reasoning` / `thinking` siblings) are kept
   verbatim because they feed `state.currentText`, which
   `request-run.ts` publishes to Slack as the final reply on `stop`
   and tool-only turns — truncating them would post a `...[truncated
   N bytes]` marker to the user. The truncator now accepts a
   `preserveStringAtPath` predicate and the request runner uses it to
   whitelist `properties.part.text` for those part types.

   A stability test replays six captured harness fixtures and asserts
   the rendered status bytes are identical with and without
   truncation, and a new regression test proves the full
   truncate → fold → publish pipeline returns the assistant reply
   verbatim even for 50KB final messages.

2) Ship a generic BoundedSet / BoundedMap in @/utils with Set/Map
   compatible APIs (FIFO eviction on overflow, no order refresh on
   re-set — matching the bespoke copy that used to live in
   message-updates.ts). Apply them to finalizedMessages (5000),
   rateLimitedMessages (5000), rateLimitErrors (5000), and each
   agent's newSessions (1000). Eviction is safe in every case:
   stale message ids receive no further updates, and an evicted
   session id simply loses its 'this is a new session' flag.

Note on sessionProviders: the agent adapter's session → provider
lookup is intentionally NOT capped. It is used by abortSession,
subscribeToSession, ensureSession, and the task / cron schedulers
to dispatch to the correct agent client, so evicting entries would
misroute calls for still-valid non-opencode sessions once the cap
is exceeded. Its growth is bounded in practice by the number of
unique session ids the daemon has observed.

Tests:

- packages/utils/test/event-truncation.test.ts (11 cases)
- packages/utils/test/final-reply-preservation.test.ts (4 cases)
- packages/utils/test/bounded-collections.test.ts (12 cases)
- packages/live-status-harness/test/truncation-stability.test.ts
  (6 fixtures x render-stability assertions)
- Full suite: 320 pass, 1 skip, 0 fail.
@LIU9293
LIU9293 force-pushed the feat/bounded-runtime-caches-7697 branch from f3c3097 to 07e9267 Compare April 21, 2026 04:13
@LIU9293
LIU9293 merged commit 9015c76 into main Apr 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant