These PRs stop the app from eating all your memory
Every one of these pull requests tackles the same family of problem: somewhere in shuv2code, data was piling up without a limit. A provider could send a giant response, a browser tab could hoard gigabytes, a terminal could log forever. These PRs put hard ceilings on all of it — so the app stays fast and doesn't crash when things get weird.
Right now, shuv2code has no caps on how much data it holds onto at various points. A single rogue provider response, a long browser recording, or a chatty event stream can balloon memory usage until the app slows down or crashes. These 14 PRs fix that by adding hard limits everywhere data accumulates — provider responses, browser snapshots, terminal logs, thread history, automation queues, and more. Each PR is small and focused, fixing one area at a time. Most of them cut memory usage by over 90% in their specific area. None of them change how the app works for normal use — they only kick in when things would otherwise go sideways.
| PR | Title | Area | What it bounds | Biggest win | Size | Stats |
|---|---|---|---|---|---|---|
| #32 | Bound provider response and stream buffers | Provider | HTTP responses, SSE events, health probes, process stdout/stderr | 64 MiB cap on response chunks | L | +704 / -29 |
| #31 | Compact accessibility before transport | Browser | Chrome AX tree before it crosses IPC/WebSocket | Prunes raw tree at the source | L | +680 / -22 |
| #29 | Bound terminal history by bytes | Terminal | Terminal transcript memory + disk | 99.95% smaller (2 MiB to 1 KiB in test) | M | +141 / -27 |
| #28 | Bound thread history hydration | Data | Client thread snapshots, keyset pagination | 97.99% smaller (7.25 MB to 146 KB) | XL | +899 / -134 |
| #27 | Bound preview automation broker requests | Browser | Outstanding automation requests per host | 512 requests to 64 admitted, 448 rejected | L | +334 / -83 |
| #26 | Backpressure provider event ingestion | Provider | Provider event queue, coalescing, hot-path reads | 8,004 to 3 SQLite queries (99.96%) | L | +511 / -34 |
| #25 | Deduplicate Claude tool result payloads | Provider | Claude SDK event normalization | 1 MiB result embedded 4x to 1x (75% cut) | M | +98 / -37 |
| #23 | Route orchestration events to interested projections | Data | Projection pipeline routing, cursor writes | 66.7% fewer projector applications | L | +438 / -18 |
| #22 | Bound automation list payloads | Data | Automation list endpoint, prompt previews | 99.49% smaller (3 MB to 15 KB) | L | +433 / -54 |
| #21 | Bound browser recording memory | Recording | MediaRecorder chunk transport, renderer memory | 98.45% less peak memory (1.51 GiB to 24 MiB) | XL | +724 / -72 |
| #20 | Own unattended browser tabs in desktop main | Browser | Background tab lifecycle ownership | Tabs survive renderer crashes | L | +753 / -146 |
| #19 | Bound thread event resume | Data | Thread catch-up scans, incremental replay | 98.04% fewer events decoded | L | +443 / -44 |
| #16 | Bound collaborative browser tool payloads | Browser | Snapshot fields, screenshots, diagnostic entries | Screenshots off by default, metadata halved | L | +462 / -111 |
| #14 | Reduce collaborative browser snapshot token cost | Browser | Raw AX tree to compact relevance-pruned tree | 92.12% smaller (256 KB to 20 KB) | L | +491 / -12 |
When a provider (like OpenAI or Anthropic) sends data back to shuv2code, that data comes in as HTTP responses and Server-Sent Events. Before this PR, there was no limit on how big those could be. A misbehaving provider could send a 500 MB response and the app would try to hold it all in memory.
This PR caps OpenCode JSON responses at 64 MiB, individual SSE events at 16 MiB (with 256 active parts max), health-probe bodies at 64 KiB, and provider process stdout/stderr at 8 MiB each. It also fixes a UTF-8 decoding bug that could corrupt text when data was split across network chunks.
PR #14 already made accessibility snapshots smaller. But the raw, full-size tree from Chrome was still traveling through every internal hop — IPC, React, WebSocket — before getting compacted at the end. This PR moves the compaction to the very beginning, right where Chrome hands off the data.
Now the compact tree is the only thing that travels through the app. The raw tree never leaves the desktop capture function unless someone explicitly asks for full diagnostics. This is a stacked PR that depends on #14 landing first.
The terminal had a 5,000-line limit, but a single line could be any size. A program that spits out 2 MiB of text with no line breaks would stay 2 MiB in memory. This PR adds a 4 MiB byte ceiling alongside the line limit, so the total terminal history can never grow beyond that regardless of line length.
It also fixes reopening old transcript files — instead of loading the whole file and then truncating, it reads only the tail end and rewrites the compacted version. UTF-8 is preserved correctly when trimming through multibyte characters.
When you open a conversation thread, shuv2code used to load everything — every message, activity, plan, and checkpoint — all at once. A thread with 10,000 messages in each collection (40,000 records total) would serialize 7.25 MB of data and send it to the client.
This PR limits the initial snapshot to the newest 200 records per collection (800 total), adds keyset cursor pagination for loading older history, and includes a "Load earlier history" UI. The server still keeps full reads for internal command correctness, but the client only gets the recent stuff. It also fixes prompt-history bootstrap to stop constructing message blocks when the budget is full.
The preview automation broker had no limit on how many requests could be queued up at once. If 512 requests came in simultaneously, all 512 were held in memory. This PR caps each host at 64 outstanding requests and immediately rejects the rest with a typed error that includes capacity and load context.
New sessions can route to other non-full hosts. Interrupted or timed-out requests are filtered out before delivery, so stale queued commands can't execute later. The connection handshake doesn't count against the capacity.
When a provider streams events (like text deltas as the AI types), each delta was processed individually and triggered separate database reads. A burst of 2,000 one-character deltas meant 2,001 database reads for thread-shell state and 2,001 for pending-turn lookup — 8,004 total SQLite queries.
This PR replaces the unbounded worker with a serial queue capped at 128 items, coalesces adjacent text deltas losslessly, and caches thread-shell state for 30 seconds. Result: the same 2,000-delta burst now triggers 3 SQLite queries total.
The Claude adapter was duplicating tool results. When Claude finished a tool call, the result payload got embedded four times: once as structured data, once as raw SDK payload, and then both of those got emitted again on both the item.updated and item.completed events. A 1 MiB tool result became 4 MiB of event data.
This PR stops the redundant item.updated copy, keeps the result only on item.completed, and replaces repeated raw SDK user-message bodies with compact diagnostic metadata. The same 1 MiB result now appears exactly once.
The projection pipeline applies every orchestration event to every projector, even if a projector can't actually mutate for that event type. For 100 events across 9 projectors, that's 900 projector applications and 900 SQL transactions — even though two-thirds of them do nothing.
This PR routes events only to projectors that can actually mutate, advances uninterested projector cursors in a single batch UPSERT, and skips four full child-collection reads when assistant messages can't affect thread shell summary fields. Each interested projector still gets its own atomic transaction and independent recovery.
The automations.list endpoint returned full prompt text for every automation. With 25 automations each having 120,000-character prompts, that was a 3 MB response. This PR makes it return prompt-free summaries with bounded previews and prompt length, paginated at 50 per page (max 100). Full prompts are only fetched through automations.get when needed.
The Settings page now loads more records and only fetches full records for edit/duplicate actions.
Browser recordings were held entirely in renderer memory. A 54-minute recording retained roughly 1.51 GiB in memory and needed a 3.02 GiB final payload. This PR streams MediaRecorder chunks through Electron IPC directly into a desktop-owned .partial file, with atomic rename only on successful stop.
It enforces renderer backpressure (pause at 16 MiB queued, resume below 4 MiB), an 8 MiB chunk ceiling, a 2 GiB recording ceiling, and cleans up partial files on abort/failure.
Background browser tabs (automations running without a visible UI) were owned by the React renderer as hidden <webview> elements. If the renderer crashed or navigated away, the browser tab was destroyed. This PR moves their ownership to Electron's main process, so they survive renderer restarts and UI navigation.
The tabs stay hidden and off the taskbar. When a tab needs to become visible, it's adopted into the renderer on demand. The PR handles renderer crash recovery, window/lease cleanup, and session preservation.
An honest remaining boundary: the automation RPC consumer still originates in the renderer, so an in-flight request can fail during renderer restart. That transport migration is a separate stacked PR.
When a client reconnected to a thread, the server would scan through all events since the client's last cursor — even events from completely unrelated threads. With 100 unrelated events and 2 target events, the old path read and decoded all 102 rows.
This PR uses the existing (aggregate_kind, stream_id, sequence) index to read only the target thread's events, caps incremental replay at 1,000 events, and sends a fresh snapshot when a client cursor is too old. Live-event buffering and sync markers are preserved across both paths.
The browser snapshot tool bounded arrays by entry count but left each entry's strings unbounded. A single DOM container, console message, or action error could make a "bounded" snapshot arbitrarily large. Every snapshot also captured a screenshot by default and carried metadata in two formats.
This PR: screenshots are off by default (opt-in with includeScreenshot: true), metadata is emitted once instead of twice, each field has per-field limits (names 512 chars, selectors/URLs 2,048 chars, error text 4,096 chars), pending network requests capped at 500, diagnostic entries limited to latest 20, and total MCP metadata rejected above 512 KB / screenshots above 2 MB.
This is the foundation PR. Every time the browser tool took a snapshot, it sent Chrome's complete raw accessibility tree to the AI model — all the browser-internal metadata, decorative nodes, and static text that's useless for navigation. A typical page produced 602 nodes and 256 KB of data.
This PR adds a relevance-pruned compact tree that keeps only actionable controls, named landmarks, and meaningful state — dropping to 117 nodes and 20 KB. That's a 92% reduction. The raw tree is still available behind mode: "full" for diagnostics. GPT-5.6 Luna was tested against the compact output and successfully identified navigation targets.