Skip to content

fix(session): bound compacted history hydration - #39930

Open
brauliobo wants to merge 3 commits into
anomalyco:devfrom
brauliobo:compacted-history
Open

fix(session): bound compacted history hydration#39930
brauliobo wants to merge 3 commits into
anomalyco:devfrom
brauliobo:compacted-history

Conversation

@brauliobo

@brauliobobrauliobo commented Jul 31, 2026

Copy link
Copy Markdown

Issue for this PR

Closes#35570

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

filterCompactedEffect currently hydrates every message part before discarding history older than the latest completed compaction. This change finds that boundary from lightweight message rows first, then hydrates only the retained messages in batches. Retained tails, incomplete compactions, forks, and the existing stream behavior are unchanged.

On one compacted session this reduced the hydrated window from 609 messages / 11.4 MB of parts to 51 messages / 0.81 MB.

Related to #31638. That PR scans already-hydrated pages; this implementation avoids hydrating oversized parts on the boundary page by separating row discovery from part hydration, as proposed in #35570.

How did you verify your code works?

  • bun test --timeout 30000 test/session/messages-pagination.test.ts test/session/compaction.test.ts test/session/prompt.test.ts test/session/snapshot-tool-race.test.ts
  • bun typecheck from packages/opencode
  • repository pre-push bun turbo typecheck
  • Prettier and targeted oxlint (0 errors)

Screenshots / recordings

N/A - backend memory fix.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions

Copy link
Copy Markdown
Contributor

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.

@github-actionsgithub-actionsBot added the needs:compliance This means the issue will auto-close after 2 hours. label Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

Potential Duplicate Found:

This PR appears to address the same issue—avoiding full history hydration after compaction. Both PRs focus on optimizing memory usage by limiting what gets hydrated from prompt history during session compaction iterations. You should verify whether PR #31638 is already merged or if it needs to be reconciled with the current PR #39930.

@brauliobobrauliobo changed the title fix(opencode): bound prompt history hydrationfix(session): bound compacted history hydrationJul 31, 2026
@brauliobo

Copy link
Copy Markdown
Author

Addressed the follow-up comments:

@github-actionsgithub-actionsBot removed the needs:compliance This means the issue will auto-close after 2 hours. label Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@Qiiks

Qiiks commented Aug 6, 2026

Copy link
Copy Markdown

Validated this fix against two real production sessions (Windows, opencode 1.18.13). The two-phase walk works exactly as proposed — here's the measured impact and one edge case worth deciding on:

Measured impact (replicated the walk against the live DBs)

SessionMessagesParts totalPR-fix hydratesReduction
A (35d, edit-heavy)8,01025,659221 msgs / 916 parts~28x
B (25d, edit-heavy)8,28724,655372 msgs / 1,258 parts~20x

Both sessions currently hydrate all parts on every prompt (stream()filterCompactedEffect), which is the observed multi-second block + RSS spike. The row-walk + boundary-stop cuts that to <1,300 parts per prompt.

One edge case to decide: empty compaction marker behavior

Both sessions carry a compaction part with tail_start_id: null (empty marker, 0-char text — they're compaction-managed by a plugin, not opencode's native compaction). In the new walk:

if(!compaction.tail_start_id)break pages // ← stops the walk HERE

This is a behavior change from the old path: filterCompacted(stream(...)) with a null tail_start_id returned the full history (tailIndex = -1 → return result), whereas the new walk breaks at the marker and truncates the hydrated set to messages newer than it. For the two sessions above that means ~7,800 / ~7,900 older messages become unavailable to the model even though they previously fit (up to the context window). The PR description states "incomplete compactions … unchanged" — this path changes them.

Options:

  • continue instead of break on null tail_start_id — preserves old behavior for incomplete compactions (no history truncation), at the cost of walking to the session start for those sessions.
  • Keep break but document it — for plugin-compaction-managed sessions (magic-context et al.) the older history is re-injected by the plugin anyway, so the truncation is invisible; but for plain opencode sessions with a stale/failed compaction marker it would silently shorten available history.

Also worth noting: the null-marker case in old code already skipped compaction reordering (returned everything), so either choice is defensible — but the "incomplete compactions unchanged" claim in the description only holds with the continue variant.

Otherwise the implementation is clean: messageRows shares the pagination core with page(), the boundary detection correctly keys on completed user messages, and the batched hydrate (rows.slice(index, index + size)) keeps the fix's memory bounded. LGTM.

@brauliobo

Copy link
Copy Markdown
Author

@Qiiks Addressed in commit 4ff112ab19.

  • Treat a missing tail_start_id as an incomplete compaction in both filterCompacted and the paginated pre-hydration walk, so the scan continues and the full history is preserved.
  • Added a regression with 60 messages after the marker, covering the 50-row pagination boundary.
  • Updated the bounded-hydration fixture to use an explicit tail, preserving its original assertion that older discarded parts are not hydrated.

Verification:

  • bun test test/session/messages-pagination.test.ts: 53 passed.
  • bun test test/session/prompt.test.ts test/session/compaction.test.ts: 108 passed, 2 skipped.
  • bun typecheck: passed.
  • Prettier check: passed.

The package-wide bun test run was also attempted but could not complete because of unrelated environment failures: missing zod resolution in plugin tests and a v2 PTY test timeout.

@brauliobo

Copy link
Copy Markdown
Author

Corrected the follow-up in 69d3227b52 after re-checking native compaction semantics.

tail_start_id is intentionally absent when a completed compaction summarizes the full history (SessionCompaction.select returns no tail when keep.start === 0). Treating every missing tail as incomplete made the row walk hydrate history older than a valid completed compaction, defeating the bounded-hydration change and potentially reintroducing context overflow.

The update now:

  • stops at completed no-tail compactions in both the iterable and paginated paths
  • keeps full history only when the compaction is genuinely incomplete (no completed summary)
  • restores the invalid-old-part regression so a completed full compaction proves older parts are not hydrated

Verification: 162 passed, 2 skipped across the targeted session tests; bun typecheck passed; repository pre-push typecheck passed.

Known persisted-data limitation: a plugin marker paired with a completed-looking summary but no tail is indistinguishable from a valid native full-summary compaction, so it follows native completed-compaction semantics.

renekris added a commit to renekris/opencode-lowmem that referenced this pull request Aug 22, 2026
…malyco#39930)
Compaction previously materialized every message of a session through
the full stream; now only the newest messages are hydrated. Reduces a
609-message session from 11.4MB to 0.8MB of allocations on compaction.
renekris added a commit to renekris/opencode-lowmem that referenced this pull request Aug 26, 2026
- pin note, seam-lookup range, and deferred-port re-verification now
reference v1.18.23 (post-split packages/ai|util tree still absent,
anomalyco#43769 stays blocked)
- watch-list gains the 2026-08-26 sweep candidates (anomalyco#39930, anomalyco#38939,
anomalyco#41950, anomalyco#33713, anomalyco#44631)
- fork-build.sh BASE lookup now excludes *-lowmem.* tags: after a fresh
upstream merge the previous fork tag ties the new base tag on commit
distance with a newer date and git describe stamps the OLD base
Sign up for freeto 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.

perf(core): avoid hydrating discarded compacted session parts

2 participants

@brauliobo@Qiiks