Skip to content

fix: clear tool output and attachments when pruning to prevent memory leak#7049

Open
hendem wants to merge 9 commits intoanomalyco:devfrom
hendem:fix/memory-leak-compaction-cleanup
Open

fix: clear tool output and attachments when pruning to prevent memory leak#7049
hendem wants to merge 9 commits intoanomalyco:devfrom
hendem:fix/memory-leak-compaction-cleanup

Conversation

@hendem
Copy link

@hendem hendem commented Jan 6, 2026

Fixes #3013

Summary

Clear tool output and attachments when compaction prunes old tool results, actually freeing memory instead of just flagging.

Evidence

In a real session working on this repo:

Metric Value
OpenCode process RAM 16.4 GB
Tool parts on disk 34,887 files
Session duration ~3 hours

Sample tool output sizes from this session:

  • webfetch of docs page: 10 KB
  • read of source file: 5-50 KB
  • git diff / gh pr diff: 10-100 KB
  • bash command outputs: 1-50 KB

All of these outputs stay in memory even after compaction marks them "old".

The Problem

In SessionCompaction.prune(), when tool outputs are pruned:

// BEFORE: Only sets a flag, output data stays in memory
part.state.time.compacted = Date.now()
await Session.updatePart(part)

The compacted timestamp is used by toModelMessage() to replace output with placeholder text like "(Old tool result content cleared)" - but the actual data never gets freed.

Over a long session:

  • 34,887 tool calls × average output size = hundreds of MB to GBs retained
  • Memory never decreases, even after compaction runs
  • Eventually Bun runs out of memory and crashes

The Fix

// AFTER: Actually clear the data
part.state.time.compacted = Date.now()
part.state.output = ""              // Free the output string
part.state.attachments = undefined  // Free any attachments
await Session.updatePart(part)

Now when compaction runs, the memory is actually freed. The placeholder text is already shown to the LLM (that logic exists), we just were not clearing the source data.

Testing

Existing compaction tests pass. Added test verifying output/attachments are cleared after prune.

Copilot AI review requested due to automatic review settings January 6, 2026 05:49
@github-actions
Copy link
Contributor

github-actions bot commented Jan 6, 2026

The following comment was made by an LLM, it may be inaccurate:

No duplicate PRs found

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a memory leak where compacted tool outputs and their attachments were retained indefinitely in storage. When the pruning mechanism marks old tool outputs as compacted, the fix now clears both the output string and attachments array to free memory.

Key changes:

  • Clears tool output and attachments during compaction to prevent unbounded memory growth
  • Adds comprehensive test coverage for the pruning behavior

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
packages/opencode/src/session/compaction.ts Sets output to empty string and attachments to undefined when marking tool parts as compacted, with explanatory comment
packages/opencode/test/session/compaction.test.ts Adds two new test cases: one verifying output/attachments are cleared during pruning, and one testing that pruning respects the disabled configuration

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@hendem
Copy link
Author

hendem commented Jan 6, 2026

Addressed review comment about test not verifying behavior when pruning is disabled (commit 488173440).

The test now:

  1. Creates a tool part with large output (200,000 chars) and an attachment (similar to the first test)
  2. Creates additional user messages to get past turn protection
  3. Calls prune() with pruning disabled via config
  4. Verifies that:
    • output.length remains 200,000 (unchanged)
    • attachments.length remains 1 (unchanged)
    • time.compacted is undefined (not set)

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@kevoconnell
Copy link

this would be really helpful

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 9, 2026

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

Mark Henderson and others added 7 commits January 10, 2026 14:14
… leak

When compaction prunes old tool outputs, only the compacted timestamp
was being set - the actual output string and attachments array remained
in storage indefinitely. This caused storage files to grow unbounded
with large tool outputs (file contents, base64 images/PDFs, etc.).

Now prune() clears both output and attachments when marking parts as
compacted. The toModelMessage function already replaces compacted
outputs with placeholder text, so this is safe.

Fixes part of anomalyco#4315
This reverts commit b302472.
@fwang fwang force-pushed the fix/memory-leak-compaction-cleanup branch from 9d089b6 to 141c413 Compare January 10, 2026 19:15
@Th0rgal
Copy link

Th0rgal commented Jan 17, 2026

Server Mode Diagnostic Data

Running OpenCode v1.1.23 as a systemd daemon (opencode serve) on a 64GB Linux server. Here is data that may help with this PR.

Memory Growth (from systemd logs)

Runtime Memory Peak Swap
2.5h 263 MB 0
4.5h 699 MB 0
4.75h 56 GB 8.8 GB
2.25h 20.9 GB 0

Memory grows over time and eventually causes the process to become unresponsive.

Storage Accumulation

storage/part/     141MB (10,820 directories)
storage/message/   19MB (728 directories)
tool-output/       21MB (15 files)

The 10,820 part directories correlate with the unfree-d tool outputs described in this PR.

Timeout Pattern

When memory gets high, HTTP requests to /session/{id}/message stop responding. Downstream clients see 10-minute timeouts:

17:22:29 WARN Retry attempt=2 session=ses_xxx
17:32:30 WARN Retry attempt=3 (10 min gap = timeout)
17:42:31 ERROR elapsed_secs=1801 Failed after retries

Environment

  • OpenCode v1.1.23, server mode (opencode serve --port 4096)
  • Ubuntu Linux, systemd service
  • Multiple concurrent sessions over extended periods
  • Heavy tool usage (grep, read, bash)

Happy to provide more data if useful.

@kryptobaseddev
Copy link

can the PR be reviewed along with this one, it was fixing similar issues from a different persepctive: #14650

vbuccigrossi pushed a commit to vbuccigrossi/opencode that referenced this pull request Mar 7, 2026
Tier 1 bug fixes:
- Fix O(n²) bash output concatenation with StreamingOutput class (anomalyco#9693)
- Fix memory leaks in Bus.once, Format, Plugin, ShareNext, Bootstrap (anomalyco#13514)
- Fix FileTime race condition using actual file mtime instead of JS clock
- Free memory on compaction prune: clear output/attachments/metadata (anomalyco#7049)
- Throttle reasoning-delta storage writes to 50ms intervals (anomalyco#11328)
- Handle SIGHUP/SIGTERM to prevent orphaned processes (anomalyco#12718)
- Add process.once("close") handler for bash tool reliability

Tier 2 features:
- Support 1M context window for Anthropic models via beta header (anomalyco#14375)
- Input-only token counting for compaction with limit.input models
- MCP lazy loading: on-demand tool discovery via mcp_search tool (anomalyco#8771)
- MCP servers listed in system prompt when lazy mode enabled
- StreamingOutput: output_filter regex for build diagnostics
- LSP server cleanup callback for temp directory removal
- Extract formatSize utility from uninstall to shared util/format
- GitHub CI: fix Bus subscription leak in session event handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
binarydoubling added a commit to binarydoubling/opencode that referenced this pull request Mar 9, 2026
Addresses the remaining memory leaks identified in anomalyco#16697 by
consolidating the best fixes from 23+ open community PRs into
a single coherent changeset.

Fixes consolidated from PRs: anomalyco#16695, anomalyco#16346, anomalyco#14650, anomalyco#15646,
anomalyco#13186, anomalyco#10392, anomalyco#7914, anomalyco#9145, anomalyco#9146, anomalyco#7049, anomalyco#16616, anomalyco#16241

- Plugin subscriber stacking: unsub before re-subscribing in init()
- Subagent deallocation: Session.remove() after task completion
- SSE stream cleanup: centralized cleanup with done guard (3 endpoints)
- Compaction data trimming: clear output/attachments on prune
- Process exit cleanup: Instance.disposeAll() with 5s timeout
- Serve cmd: graceful shutdown instead of blocking forever
- Bash tool: ring buffer with 10MB cap instead of O(n²) concat
- LSP index teardown: clear clients/broken/spawning on dispose
- LSP open-files cap: evict oldest when >1000 tracked files
- Format subscription: store and cleanup unsub handle
- Permission/Question clearSession: reject pending on session delete
- Session.remove() cleanup chain: FileTime, Permission, Question
- ShareNext subscription cleanup: store unsub handles, cleanup on dispose
- OAuth transport: close existing before replacing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

Uses a huge amount of memory

7 participants