fix(opencode): cap snapshot patches at 1000 files, summary diffs at ~1MB#18925
Open
BYK wants to merge 16 commits intoanomalyco:devfrom
Open
fix(opencode): cap snapshot patches at 1000 files, summary diffs at ~1MB#18925BYK wants to merge 16 commits intoanomalyco:devfrom
BYK wants to merge 16 commits intoanomalyco:devfrom
Conversation
Contributor
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
8cd1a62 to
fc19cd1
Compare
5f7c1ae to
61ed31a
Compare
Contributor
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
Add interactive-widget=resizes-content to the viewport meta tag so the layout viewport (and dvh) shrinks when the virtual keyboard opens. The existing flex layout then keeps the composer visible above the keyboard.
Session.touch() now clears time_archived so that sending a prompt to an archived session automatically unarchives it and places it back in the sidebar. As defense in depth, the frontend event-reducer no longer calls cleanupSessionCaches() when a session.updated event arrives with time.archived set. Archive is reversible — caches are harmless to keep and will be reclaimed naturally. session.deleted still cleans caches.
meta.limit grows monotonically via loadMore() as the user scrolls up through message history (200 → 400 → 600 → ...). When switching sessions and returning, the 15s TTL expires and triggers a force-refresh that reuses this inflated limit as the fetch size — re-downloading the entire browsing history from scratch on every session switch. Cap the limit to messagePageSize (200) on force-refreshes so only the latest page is fetched. loadMore() still works normally after returning since loadMessages() resets meta.cursor to match the fresh page.
…ded dir walk Three changes to reduce server event loop contention when many projects load concurrently (e.g. opening ~/Code with 17 sidebar projects): 1. Instance bootstrap concurrency limit (p-limit, N=3): Each Instance bootstrap spawns 5-7 subprocesses (git, ripgrep, parcel/watcher). With 17 concurrent bootstraps that's ~100 subprocesses overwhelming a 4-core SATA SSD system. The p-limit semaphore gates new directory bootstraps while allowing cache-hit requests through instantly. Peak subprocesses: ~100 → ~18. 2. Async Filesystem.exists and isDir: Filesystem.exists() wrapped existsSync() in an async function — it looked async but blocked the event loop on every call. Replaced with fs.promises.access(). Same for isDir (statSync → fs.promises.stat). This lets the event loop serve health checks and API responses while directory walk-ups are in progress. Added Filesystem.existsSync() for callers that genuinely need sync behavior. 3. Bounded Filesystem.up/findUp walk depth (maxDepth=10): Previously walked all the way to / with no limit. Added maxDepth parameter (default 10) as a safety net against degenerate deep paths.
…g in memory Previously, bash tool and /shell command accumulated ALL stdout/stderr in an unbounded string — a verbose command could grow to hundreds of MB. Now output beyond Truncate.MAX_BYTES (50KB) is streamed directly to a spool file on disk. Only the first 50KB is kept in memory for the metadata preview. The full output remains recoverable via the spool file path included in the tool result metadata. Includes test for the spooling behavior.
The edit tool's fuzzy matching uses Levenshtein distance for similarity scoring. The old implementation allocated a full (n+1)×(m+1) matrix which could spike to ~400MB for 10K character strings. Replace with a standard 2-row algorithm that swaps the shorter string to the inner loop. Produces identical results with dramatically less memory — O(min(n,m)) instead of O(n×m).
When compaction marks a tool part as compacted, toModelMessages() already skips it — but the full output string (up to 50KB per part) was still stored in SQLite as dead weight. Now set output to '[compacted]', clear metadata and attachments when compacting. This reduces database bloat over time.
- FileTime: add remove() to clean up per-session read timestamps - LSP client: delete diagnostics map entries when empty instead of storing empty arrays (prevents monotonic growth) - RPC: add 60s timeout to pending calls so orphaned promises from dead workers are cleaned up instead of leaking indefinitely
Add configurable session retention that auto-deletes archived sessions older than retention.days (default: 30, 0 = disabled). Runs every 6 hours via Scheduler, batched at 100 sessions per run. Also call FileTime.remove() on session archive and delete to clean up per-session read timestamps. The database had grown to 1.99GB with 1,706 sessions spanning 53 days because there was no automatic cleanup — sessions were only soft-archived but never deleted.
Build on anomalyco#19299 by @thdxr with several production hardening improvements: - Filter optional fonts during embedding (~27MB savings) — only core fonts (Inter + IBM Plex Mono) are embedded, the rest fall through to CDN proxy - Add pre-bootstrap static middleware before Instance.provide() to avoid DB migration checks on every asset request - Add SPA fallback routing — return index.html for page routes but not for asset requests (which fall through to CDN proxy for excluded fonts) - Add OPENCODE_APP_DIR env var for dev override - Auto-detect packages/app/dist in monorepo dev mode - Use explicit MIME type map instead of Bun.file().type for consistent typing - CDN proxy fallback for any asset not found locally
Two optimizations to drastically reduce memory during prompting: 1. filterCompactedLazy: probe newest 50 message infos (1 query, no parts) to detect compaction. If none found, fall back to original single-pass filterCompacted(stream()) — avoids 155+ wasted info-only queries for uncompacted sessions. Compacted sessions still use the efficient two-pass scan. 2. Context-window windowing: before calling toModelMessages, estimate which messages from the tail fit in the LLM context window using model.limit.context * 4 chars/token. Only convert those messages to ModelMessage format. For a 7,704-message session where ~200 fit in context, this reduces toModelMessages input from 7,704 to ~200 messages — cutting ~300MB of wrapper objects across 4-5 copy layers down to ~10MB. Also caches conversation across prompt loop iterations — full reload only after compaction, incremental merge for tool-call steps.
… minimize Two additional fixes: 1. Add plan_exit/plan_enter to the tools disable map in task.ts. The session permissions were being overwritten by SessionPrompt.prompt() which converts the tools map into session permissions. Without plan_exit in the tools map, it wasn't being denied. 2. Add minimize/expand toggle to the question dock so users can collapse it to read the conversation while a question is pending. Adds a chevron button in the header and makes the title clickable to toggle. DockPrompt gains a minimized prop that hides content and footer.
…nder as markdown Three fixes for experimental planning mode: 1. Deny plan_exit/plan_enter permissions on subagent sessions so explore/general agents spawned by the plan agent cannot accidentally trigger a mode switch to build. 2. Read the plan file content in PlanExitTool and embed it in the question text so users see the full plan before deciding to switch. 3. Render question text with the Markdown component instead of raw text, both in the live question dock and historical message parts. Add overflow-y/max-height to question-text CSS so long plans scroll within the dock.
- One-time migration to incremental auto-vacuum (PRAGMA auto_vacuum=2) so disk space is reclaimed when sessions are deleted - Add Database.checkpoint() (TRUNCATE mode) and Database.vacuum() (incremental_vacuum(500)) helpers
61ed31a to
690ef2b
Compare
Snapshot.patch() now stores relative paths (instead of joining with worktree root) and caps at 1000 entries. Revert handles both old absolute and new relative paths via path.isAbsolute() for backward compat. Summary diffs (summarizeSession/summarizeMessage) use a ~1MB byte budget based on actual before+after content size instead of an arbitrary count cap. This prevents multi-MB payloads while allowing many small file diffs. Closes anomalyco#18921
690ef2b to
a944246
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes #18921
Type of change
What does this PR do?
Prevents oversized snapshot patches and summary diffs when worktrees contain massive numbers of files (e.g. 99K+ files from
.test-tmpdirectories).snapshot/index.ts— Relative paths + count cappatch()now stores relative paths from git instead of joining withstate.worktree. Saves ~55 bytes per entry (the repeated worktree prefix).revert()handles both old (absolute) and new (relative) paths viapath.isAbsolute()for backward compat with existing DB entries.session/summary.ts— ~1 MB byte budgetbefore + aftercontent size.capDiffs()accumulatesFileDiffentries until content exceeds 1 MB, always including at least 1 entry.summarizeSession()andsummarizeMessage().Why byte budget > count cap for diffs
Each
FileDiffcarries full before/after file content. With a count cap:The byte budget caps the actual payload size regardless of file count or individual file sizes.
How did you verify your code works?
Checklist