Skip to content

Conversation

@popododo0720
Copy link
Contributor

@popododo0720 popododo0720 commented Feb 12, 2026

Summary

Eliminate redundant session.messages() API calls in hot paths to fix memory leaks during extended sessions.

Root Cause

Multiple hooks call session.messages() (full session history fetch) on every tool execution, causing memory to grow proportionally with session length. In a 200+ message session, each tool call triggered 3-4 full fetches of the entire message history.

Changes

File Change Effect
context-window-monitor.ts Replace session.messages() with message.updated event cache 0 API calls per tool (was 1)
preemptive-compaction.ts Same event-based caching 0 API calls per tool (was 1)
transcript.ts Per-session transcript cache (5min TTL) 1 call per session (was 1 per tool)
background-agent/manager.ts Remove session.messages() from polling loop 0 calls per poll (was 1 per task × every 3s)
todo-continuation-enforcer/session-state.ts TTL-based pruning for session state Map Prevents unbounded Map growth
tool-input-cache.ts setInterval.unref() Prevents blocking process exit

Impact

  • Before: ~3-4 session.messages() calls per tool execution + continuous polling fetches
  • After: 0 calls per tool execution (event-cached), 1 per session for transcript

Testing

  • 12 new tests (TDD RED→GREEN→REFACTOR)
  • All 2595 existing tests pass, 0 new failures
  • bun run typecheck
  • bun run build

Verification

bun run typecheck
bun test src/hooks/context-window-monitor.test.ts
bun test src/hooks/preemptive-compaction.test.ts
bun test src/hooks/claude-code-hooks/transcript.test.ts
bun test src/features/background-agent/manager.test.ts
bun test src/hooks/todo-continuation-enforcer/todo-continuation-enforcer.test.ts
bun run build

Related Issues

Fixes #1222
Related: #1769, #1752, #1486, #361


Summary by cubic

Replaced repeated session.messages() fetches with event-based caching to stop memory leaks in long sessions and cut API calls to near-zero during tool runs. Addresses #1222 by moving hot paths to use message.updated data and adding TTL-based cleanup.

  • Bug Fixes
    • Context-window-monitor and preemptive-compaction now use message.updated token cache; no history fetches on tool.execute.after; cache cleared on session.deleted.
    • Background-agent manager removes session.messages() from the polling loop; progress tracked via event handlers.
    • Transcript builder adds a per-session cache (5 min TTL) to avoid rebuilding from full history on each tool call; includes cleanup helpers.
    • Todo-continuation-enforcer store gains TTL pruning and a shutdown method to prevent unbounded Map growth.
    • Tool-input-cache cleanup timer now uses setInterval.unref() so it won’t block process exit.
    • Impact: 0 session.messages() calls per tool execution (was ~3–4) and no polling fetches; transcript now 1 fetch per session.

Written for commit eb56701. Summary will update on new commits.

…ent memory leaks

- Replace session.messages() fetch in context-window-monitor with message.updated event cache
- Replace session.messages() fetch in preemptive-compaction with message.updated event cache
- Add per-session transcript cache (5min TTL) to avoid full rebuild per tool call
- Remove session.messages() from background-agent polling (use event-based progress)
- Add TTL pruning to todo-continuation-enforcer session state Map
- Add setInterval.unref() to tool-input-cache cleanup timer

Fixes code-yeongyu#1222
@popododo0720
Copy link
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@popododo0720
Copy link
Contributor Author

🔍 Detailed Analysis: Why This Fix Matters

The Problem

I noticed memory usage growing continuously during extended sessions with oh-my-opencode. On a 62GB RAM server, the opencode process reached ~5GB RSS in just 15 minutes, increasing at ~30MB/s.

Investigation revealed the root cause: session.messages() is called excessively in hot paths, fetching the entire session history on every tool execution.

What happens on every single tool call (before this fix):

tool.execute.after fires
  → context-window-monitor: session.messages() — full fetch ①
  → preemptive-compaction: session.messages() — full fetch ②
  → claude-code-hooks/transcript: session.messages() — full fetch ③
  → (if background tasks) polling every 3s: session.messages() — full fetch ④

In a session with 200+ messages, each fetch returns several MB of JSON. Since this happens on every tool call, memory balloons rapidly.

What this PR does:

The key insight is that message.updated events already carry token informationpreemptive-compaction was already using this in its message.updated handler! So the fix is straightforward:

  1. Cache token info from events instead of fetching all messages
  2. Cache transcripts per-session (5min TTL) instead of rebuilding every time
  3. Remove redundant session.messages() from pollinghandleEvent(message.part.updated) already tracks progress

The approach is minimal and low-risk — no architectural changes, just removing unnecessary API calls and using data that is already available via events.

Before → After

Hook Before After
context-window-monitor session.messages() every tool 0 calls (event cache)
preemptive-compaction session.messages() every tool 0 calls (event cache)
transcript builder session.messages() every tool 1 call per session (cached)
background-agent poll session.messages() every 3s per task 0 calls (event-based)

This should address the core memory issue reported in #1222, #1486, and #361.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 9 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Significant logic changes including removal of stability detection in background-agent and new caching layers. Cannot guarantee 100% no regressions without validation of event-based progress tracking.

@code-yeongyu
Copy link
Owner

Excellent work on this! The memory leak analysis was thorough and the fix is surgical — replacing redundant session.messages() calls with event-based caching is exactly the right approach. The detailed before/after table in your description made reviewing a breeze. Thank you! 🙏

@code-yeongyu code-yeongyu merged commit 9fe48d2 into code-yeongyu:dev Feb 13, 2026
8 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.

[Feature]: Cleanup old subagents/sessions.

2 participants