shuv2code · 14 open pull requests

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.

14
Pull Requests
6,758
Lines Added
747
Lines Removed
112
Files Changed
99.9%
Max Reduction
In Plain English

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.

§ At a Glance
PRTitleAreaWhat it boundsBiggest winSizeStats
#32Bound provider response and stream buffersProviderHTTP responses, SSE events, health probes, process stdout/stderr64 MiB cap on response chunksL+704 / -29
#31Compact accessibility before transportBrowserChrome AX tree before it crosses IPC/WebSocketPrunes raw tree at the sourceL+680 / -22
#29Bound terminal history by bytesTerminalTerminal transcript memory + disk99.95% smaller (2 MiB to 1 KiB in test)M+141 / -27
#28Bound thread history hydrationDataClient thread snapshots, keyset pagination97.99% smaller (7.25 MB to 146 KB)XL+899 / -134
#27Bound preview automation broker requestsBrowserOutstanding automation requests per host512 requests to 64 admitted, 448 rejectedL+334 / -83
#26Backpressure provider event ingestionProviderProvider event queue, coalescing, hot-path reads8,004 to 3 SQLite queries (99.96%)L+511 / -34
#25Deduplicate Claude tool result payloadsProviderClaude SDK event normalization1 MiB result embedded 4x to 1x (75% cut)M+98 / -37
#23Route orchestration events to interested projectionsDataProjection pipeline routing, cursor writes66.7% fewer projector applicationsL+438 / -18
#22Bound automation list payloadsDataAutomation list endpoint, prompt previews99.49% smaller (3 MB to 15 KB)L+433 / -54
#21Bound browser recording memoryRecordingMediaRecorder chunk transport, renderer memory98.45% less peak memory (1.51 GiB to 24 MiB)XL+724 / -72
#20Own unattended browser tabs in desktop mainBrowserBackground tab lifecycle ownershipTabs survive renderer crashesL+753 / -146
#19Bound thread event resumeDataThread catch-up scans, incremental replay98.04% fewer events decodedL+443 / -44
#16Bound collaborative browser tool payloadsBrowserSnapshot fields, screenshots, diagnostic entriesScreenshots off by default, metadata halvedL+462 / -111
#14Reduce collaborative browser snapshot token costBrowserRaw AX tree to compact relevance-pruned tree92.12% smaller (256 KB to 20 KB)L+491 / -12
§ How They Stack Together
Provider Layer
#32 response and stream buffers
#26 event ingestion backpressure
#25 Claude tool result dedup
Browser Layer
#14 compact AX snapshots
#31 compact before transport
#16 bound tool payloads
#20 durable background tabs
#27 bound automation queue
Recording
#21 stream to file, cap renderer memory
Data and Storage
#28 thread history hydration
#19 thread event resume
#23 projection routing
#22 automation list payloads
Terminal
#29 byte ceiling on history
32 Bound provider response and stream buffers
DRAFT #32
Bound provider response and stream buffers
agent/bound-provider-response-buffers
+704
added
-29
removed
6
files
L

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.

HTTP chunks: unbounded to 64 MiBSSE events: unbounded to 16 MiBHealth probes: unbounded to 64 KiBProcess I/O: unbounded to 8 MiB each
Plain English
When an AI provider talks to shuv2code, the response could be any size. Now there's a hard limit. If something sends back way too much data, the app rejects it instead of trying to swallow it all and crashing.
31 Compact accessibility before transport
DRAFT #31
Compact accessibility before transport
agent/compact-ax-before-transport
+680
added
-22
removed
15
files
L

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.

Stacks on #14 — depends on it landing first.
Plain English
PR #14 made the accessibility data smaller, but the big version was still bouncing around inside the app before getting shrunk. This PR shrinks it at the very start, so only the small version ever travels through the system.
29 Bound terminal history by bytes
DRAFT #29
Bound terminal history by bytes
agent/bound-terminal-history-bytes
+141
added
-27
removed
2
files
M

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.

2 MiB burst: stays 2 MiB to capped at 1 KiBReduction: 99.95%
Plain English
The terminal had a line count limit, but a single line could be infinitely long. Now there's also a byte limit, so even a single giant line gets trimmed. Old transcript files are also reopened more efficiently — only the end is read, not the whole thing.
28 Bound thread history hydration
DRAFT #28
Bound thread history hydration
agent/bound-thread-detail-hydration
+899
added
-134
removed
16
files
XL

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.

Records: 40,000 to 800 (98% fewer)Snapshot size: 7.25 MB to 146 KB (97.99%)Prompt bootstrap: 50k blocks to under 30 (99.94%)
Plain English
When you open a conversation, the app used to load every single message and activity from the beginning of time. Now it loads just the recent stuff and lets you click "load more" if you need older history. This makes opening threads much faster, especially for long conversations.
27 Bound preview automation broker requests
DRAFT #27
Bound preview automation broker requests
agent/bound-preview-broker-queue
+334
added
-83
removed
3
files
L

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.

512 requests: 512 retained to 64 admitted, 448 rejectedNo loss or mis-correlation
Plain English
When the browser automation system gets flooded with requests, it used to queue all of them with no limit. Now it caps the queue at 64 and immediately rejects anything beyond that, so the system doesn't get overwhelmed. Rejected requests get a clear error so the caller knows what happened.
26 Backpressure provider event ingestion
DRAFT #26
Backpressure provider event ingestion
agent/backpressure-provider-events
+511
added
-34
removed
7
files
L

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.

SQLite queries: 8,004 to 3 (99.96%)Queue work: 2,001 to 2 batches (99.9%)Max queue depth: 2 of 128
Plain English
When the AI sends text one character at a time, the app used to hit the database for every single character. Now it batches them together, so 2,000 characters become 2 batches instead of 2,000 separate database lookups. That's a 2,668x reduction in database work.
25 Deduplicate Claude tool result payloads
DRAFT #25
Deduplicate Claude tool result payloads
agent/dedupe-claude-tool-results
+98
added
-37
removed
2
files
M

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.

1 MiB result: embedded 4x to 1x (75% cut)Event batch: over 70% smaller
Plain English
When Claude finished a tool call, the result was being stored four times instead of once. A 1 megabyte result turned into 4 megabytes of data. Now it's stored once, which cuts the data by 75%.
23 Route orchestration events to interested projections
DRAFT #23
Route orchestration events to interested projections
agent/route-orchestration-projections
+438
added
-18
removed
4
files
L

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.

Projector applications: 900 to 300 (66.7%)SQL transactions: 900 to 400 (55.6%)No cursor rows lost
Plain English
The system that updates conversation state was doing work for every event, even when the event couldn't possibly change anything. Now it skips the projectors that don't care about a given event type, cutting the work by about two-thirds without losing any data.
22 Bound automation list payloads
DRAFT #22
Bound automation list payloads
agent/summarize-automation-list
+433
added
-54
removed
9
files
L

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.

List response: 3 MB to 15 KB (99.49%, 197.7x)Max rows: unbounded to 100
Plain English
The automation list was returning the full text of every prompt, even when you just wanted to see what automations exist. With 25 automations, that was 3 megabytes of data. Now it returns short summaries instead, and only fetches the full prompt when you actually need it. That's a 197x reduction.
21 Bound browser recording memory
DRAFT #21
Bound browser recording memory
agent/bound-browser-recordings
+724
added
-72
removed
9
files
XL

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.

Peak renderer memory: 1.51 GiB to 24 MiB (98.45%)Recording ceiling: 2 GiBChunk ceiling: 8 MiB
Plain English
When you record the browser, the video used to pile up entirely in the app's memory. A 54-minute recording ate 1.5 gigabytes. Now it streams straight to a file as it records, so only a tiny bit stays in memory at any time. That's a 98% reduction in memory usage during recordings.
20 Own unattended browser tabs in desktop main
DRAFT #20
Own unattended browser tabs in desktop main
agent/durable-background-browser
+753
added
-146
removed
12
files
L

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.

Plain English
Background browser tabs used to live inside the app's UI layer. If the UI crashed or you navigated away, the tab died. Now the tabs are owned by the main process, so they survive crashes and navigation. They're brought into the UI only when you need to see them.
19 Bound thread event resume
DRAFT #19
Bound thread event resume
agent/bounded-thread-event-resume
+443
added
-44
removed
8
files
L

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.

Events decoded: 102 to 2 (98.04%)Replay cap: 1,000 events
Plain English
When you reconnected to a conversation, the server used to scan through every event from every thread since you last checked in — even ones that had nothing to do with your conversation. Now it uses a database index to grab only your thread's events. That's a 98% reduction in data read.
16 Bound collaborative browser tool payloads
DRAFT #16
Bound collaborative browser tool payloads
agent/budget-browser-tool-payloads
+462
added
-111
removed
11
files
L

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.

Default screenshots: 1 per snapshot to 0Metadata copies: 2 to 1Diagnostic entries: 200 to 20Total metadata: unbounded to 512 KB
Plain English
Browser snapshots were supposed to be bounded, but individual fields inside them had no size limit — one giant URL or error message could blow everything up. Also, every snapshot took a screenshot by default and stored its data twice. Now each field has a cap, screenshots are opt-in, and data is stored once.
14 Reduce collaborative browser snapshot token cost
OPEN #14
Reduce collaborative browser snapshot token cost
agent/compact-preview-snapshots
+491
added
-12
removed
7
files
L

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.

Snapshot size: 256 KB to 20 KB (92.12%, 12.69x)AX nodes: 602 to 117
Foundation for #31 which moves compaction to the source.
Plain English
Every time the browser tool looked at a page, it sent the AI model a massive accessibility tree full of useless browser-internal stuff. Now it sends only the useful parts — buttons, links, forms, landmarks — cutting the size by 92%. The AI can still navigate just fine with the smaller version, as tested with GPT-5.6.