OpenCode for long, agent-heavy workdays. If you keep many sessions open, delegate to dozens of subagents, or leave the TUI running all day, this fork is designed to stop old work from quietly consuming more and more memory and CPU.
It is still OpenCode: the same commands, sessions, auth, config, tools, and plugins. The fork adds limits and cleanup around the places that grow during heavy use. There is no database migration, and you can switch back to stock OpenCode at any time.
curl -fsSL https://github.com/ghraw/renekris/opencode-lowmem/lowmem/scripts/fork-install.sh | bashStart a new opencode session afterward. Existing sessions keep running their
current binary; new sessions use the lowmem build.
| Heavy-workload problem | What this fork does |
|---|---|
| Finished agents and old sessions stay resident | Evicts inactive payloads and old finished-agent tabs, then reloads them when needed |
| Long streamed answers get progressively more expensive | Coalesces text updates and removes quadratic delta concatenation |
| Compacted sessions reload too much history | Hydrates a recent window instead of materializing the whole session |
| Large edits and summaries carry huge patches | Caps snapshot patches and stores new summary diffs as metadata, recomputing content on demand |
| LSP files and diagnostics accumulate | Bounds document text, oversized-document records, and pull diagnostics by count and bytes |
| Event replay and background-job output spike memory | Pages durable events and keeps a bounded terminal-job ring |
| Idle state updates wake every client repeatedly | Deduplicates unchanged idle status broadcasts |
The goal is bounded growth, not a magic benchmark number. A fresh short session may look similar to stock OpenCode; the difference becomes clearer as sessions, agents, tool output, and edited files accumulate. Exact savings depend on the workload, model output, plugins, and which UI you use.
The terminal TUI has the most complete set of bounds. Server-side fixes also
benefit opencode serve, web, desktop, and IDE clients. One pathological active
session can still exceed the documented allowances through fields that are not
yet capped; the exact remaining gaps are stated below rather than hidden.
The fork is built and dogfooded with the oh-my-openagent plugin toolkit.
One heavy delegation day: 50+ finished subagent tabs sitting in the UI forever, a process that had crept past 12 GB inside a 28 GB WSL budget, and swap doing overtime. The stale tabs were the visible symptom — underneath, every compaction re-materialized full session histories, text deltas accumulated quadratically (server- and UI-side), multi-megabyte diff patches rode every update event, and finished streams kept their listeners alive.
Most fixes already existed as unmerged upstream PRs; the rest were small bounds-oriented patches nobody had written. This fork ships all of them.
This README is the single authoritative fork document. It lists everything shipped, who authored it, what was deliberately not taken, and how the fork is maintained. Commit hashes are intentionally not listed (history consolidations rewrite them) — find fork work with:
git log --grep '(port of upstream #42150)' # a specific port
git log --oneline <base-tag>..HEAD -- <file-path> # everything touching a seam
# <base-tag> = the latest merged upstream release tag (the vX.Y.Z behind HEAD)| Fix | Plain language | Upstream | Author |
|---|---|---|---|
| Bounded compacted-history hydration | After context compaction, only the recent window loads; old history stays on disk until needed — a 609-message session materialized 11 MB, now ~0.8 MB | #39930 | brauliobo |
| O(N) delta accumulation | Very long replies no longer get slower the longer they run (processor side) | #42150 | hardes11 |
| PubSub listener leak fix | Subscriptions to finished streams are released (local deviation: listeners keep Array semantics so duplicate registrations deliver independently) | #38939 | Shalin-Shah-2002 |
| Config cache isolation | Two projects open at once can't corrupt each other's settings | #41950 | weiconghe |
| Empty-stream retry | A gateway that closes cleanly with no content retries instead of silently ending your turn; we add a clean-EOF regression test and an observed-output guard so a stream that already emitted text is never retried | #43881 | moritzscheele |
| Stream failures marked errored | Failed streams are recorded as errored, not as normal stops | #42176 | vladislav-miroshnikov |
| Honest SSE chunk timeout | Server keepalive comments can't fake "data is flowing" during a stall; we add a multi-byte-split regression test. Fork addition: a single SSE frame is capped (8M decoded characters, terminated or not) and aborts the stream — the deadline only bound its duration, not its size | #43607 | 1052326311 |
| PartUpdated shallow copy | Parts stop being deep-cloned on every publish — a top RAM driver in long sessions (issue #35107 by xingruodong-sys; fix shape from closed #43733 by ColeLindfors) | #35107 / #43733 | credited here |
| Idle status dedupe | Repeated "session is idle" writes stop re-broadcasting status events to every connected client — passive-CPU fix | #40984 | zcxGGmu |
| Durable event codec reuse | Event codecs are compiled once per definition and cached instead of being rebuilt on every encode/decode — cuts CPU and allocation churn on the hot event path (129 M read syscalls observed on a 2.5 h session) | #43778 | opencode-agent[bot]* |
| Database stats + vacuum subcommands (partial port) | Adds opencode db stats [--json] [--exhaustive] and opencode db vacuum; the stats implementation is a partial port of the upstream command shape, while this fork's offline vacuum guard is documented under customs below. Default stats read only cheap metadata (page counts, freelist, sidecar sizes, table names — no table scans). --exhaustive additionally reports per-table row counts and approximate payload bytes by scanning the full database. Stats read a read-only immutable view of the main database file (no locks, no sidecar writes); page/table values exclude uncheckpointed WAL contents, which wal_bytes reports separately |
#43456 | AndyS77 |
| Write-only summary diff patches | New message summaries retain file/count/status metadata without patch text; recomputation returns available patch content, subject to the existing snapshot diff limits, while pruned snapshots fall back to metadata; zero migration and historical events untouched | #40861 | KirillDeviatka |
Each port commit carries a (port of upstream #NNNNN) trailer — never dropped.
*AI-authored upstream PR; credited here in prose only.
| Feature | Plain language | Detail |
|---|---|---|
| Diff-patch caps | Snapshots of large edits can't balloon memory and disk | snapshot-produced patches are capped at 10 MiB per patch and 10 MiB aggregate (packages/opencode/src/snapshot/index.ts:25-26); generated-path denylist; new summaries store metadata and recompute on read |
| Run-UI subagent tab eviction | The run-mode subagent list keeps the newest 50 finished agents, not every one ever spawned | running, pinned, and permission-holding sessions exempt; evicted agents revive if they ask again; synthetic-gated background settlement |
| TUI payload eviction | Sessions you navigate away from drop their message/part payloads from memory (keep-last-~20 viewed); they rehydrate on return | guards for active/running/permission/in-flight sessions; failed revival re-arms; plugin getters auto-refetch |
| Child conveyor (daily TUI) | Child sessions rank by their newest inbound user message — a task continuation moves its child to the conveyor tail with the rest of the current wave instead of staying buried between stale agents. The child you are viewing stays pinned (footer stable; its queued messages apply as one re-rank when you leave). Children whose row events were missed are still discovered: session.created inserts directly and an unknown-row message triggers one row fetch; a 404 from that fetch drops the provisional rank while transient failures keep it for retry. Session-list hydration never drops rows a live event just created nor resurrects tombstoned sessions, and a session.sync GET cannot overwrite a row a live event updated mid-flight. A session.next.moved mutation counts as live too (a stale snapshot can neither drop nor revert it), and list-sourced rows hydrate from a fresh GET when no live event intervened |
client-side rank map (first 256 distinct children, updated in place, never regressed, roots excluded); newest-50 window; viewed child pins with one deferred re-rank; single-flight session-row reconciliation with 404 rank cleanup on unknown-row messages; per-row generation guard around session.list reconcile and session.sync row writes, extended to session.next.moved mutations and undefined (list-sourced) baselines; footer shows an honest N of 50 |
| TUI delta coalescing | The UI-side twin of #42150: streamed chunks coalesce and apply every 120 ms instead of quadratic per-chunk store writes — for every session in the process, background subagents included | packages/tui/src/context/part-delta-buffer.ts; authoritative part/message updates drop pending buffers |
| Deleted-session cleanup | Deleting a session actually drops its message/part/diff/status/permission/question buckets from the UI store, tombstones the session so late events can't resurrect it, and a removed message drops its part bucket immediately (upstream #12351, reported by Limme-swe) | session.deleted/message.removed handlers + tombstone gate in packages/tui/src/context/sync.tsx |
| Git subcommand classifier | "Always allow" for git -C ../worktree commit stores git commit * — never junk, never a wider grant than you approved. Privilege-wrapped commands (sudo git status, env sudo …) offer no always pattern at all, so one approval can never widen into sudo * |
env/command unwrapping + git global-option skipping + scoped patterns + privilege-wrapper suppression |
| Payload byte+count budget | TUI payload memory has a hard ceiling: when non-active session payloads exceed it, the least-recently-viewed ones drop (no auto-refetch); routing back rehydrates them. Streaming children keep working — their payloads drop only when non-active, and their status/permission events still flow. Permission prompts for evicted parts survive via a toolInput field carried on the permission event itself, so a prompt never needs its part payload resident |
packages/tui/src/context/payload-budget.ts; see the bound-knob inventory below. |
| Durable event paging | Durable aggregate replay reads two-phase byte+row bounded pages instead of materializing an unbounded tail | packages/core/src/event.ts; fixed page limits are 100 decoded rows and 8 MiB serialized UTF-8 bytes; no environment knobs. |
| Background-job terminal ring | Settled background jobs retain a bounded terminal ring; oldest entries evict and output is stripped under byte pressure while active jobs remain protected | packages/core/src/background-job.ts; see the bound-knob inventory below. |
| Offline database vacuum guard | opencode db vacuum requires an exclusive lock and, on Linux, refuses when /proc finds another process holding the database or sidecars; the lock stays held across the guard commit and VACUUM |
Fork-custom safety procedure around the partial #43456 command; opencode db stats uses a detached snapshot that excludes live WAL contents and reports wal_bytes separately. |
| LSP document LRU | Open LSP documents (full text kept for incremental sync) are evicted least-recently-used with an explicit didClose — bounded by both count and bytes, refresh reads one file at a time, and diagnostics for closed docs are discarded via generation tokens |
packages/opencode/src/lsp/document-store.ts; see the bound-knob inventory below. |
| Session id in terminal title | The terminal/tmux-pane title carries the active session id (`OC | <title> [ses_…]), so the id you pass to opencode -sor Hermes tooling is always visible without opening/debug` |
| TUI event-listener lifecycle | Components that subscribe to server events (useEvent) drop their subscriptions when they unmount; previously every keyed session remount left the old session-route and prompt handlers in the process-global emitter set forever, so heavy session navigation grew per-event dispatch work for the life of the process |
released in 1.18.29-lowmem.4: subscribe-time owner cleanup in packages/tui/src/context/event.ts (getOwner + onCleanup), preserving effect-rerun disposal; explicit unsubscribe and ownerless subscriptions keep the stock lifetime; guarded by test/context/event.test.tsx |
Fixes unbounded TUI event-listener growth: components subscribing to server
events through useEvent now dispose their subscriptions with the owning scope
on unmount. Previously every keyed session remount left the previous
session-route and prompt handlers in the process-global emitter set forever, so
heavy session navigation grew per-event dispatch work for the life of the
process. Explicit unsubscribes and ownerless subscriptions keep the stock
lifetime.
Scope: this bounds listener accumulation, not overall process RSS. There is no database migration, retention policy, or bundled OMO plugin change.
The fix passed the focused TUI regression suite (6 pass, 0 fail, 16 assertions,
covering keyed remounts, manual unsubscribe, computation reruns, and ownerless
lifetimes) and the TUI package typecheck. The release was built for all 12
platform targets with the embedded web UI in an isolated detached checkout at
the fix commit under the serialized 6 GiB hard cap. Linux x64 and baseline
version smokes report 1.18.29-lowmem.4. Other platforms were cross-compiled,
not executed on their native systems. The Linux x64 binary also passed isolated
API/history compatibility checks against the locally installed .4 build, and
a real PTY run started both numeric recorders and exited cleanly on Ctrl-D.
Existing Vite chunk/dynamic-import/source-map warnings remain. The tag marks
the documentation commit; only README and evidence files changed after the
matrix build, so the tagged tree compiles to the same binaries.
This release targets long-session growth without claiming that every source of RSS growth has been eliminated:
- Summary and diff reads select only the relevant user turn instead of loading unrelated history.
- Fork history is copied in 50-message pages with a fixed entry-time horizon. Parent and compaction-tail ID mappings remain available across pages.
- Instance SSE listeners filter unrelated project and workspace events before they enter the queue. Matching-event queues are not newly capped because overflow recovery is not proven for every consumer.
- Optional numeric sampling is enabled with
OPENCODE_MEMORY_STATS_PATH=/absolute/directory. It records at most 60 one-minute samples per role and is diagnostics only.
Sampling starts immediately, never overlaps writes, and stops with an actionable error if a scheduled write fails. Use a distinct directory per running OpenCode process. RSS is process-wide: do not add the TUI and worker RSS values together. This is diagnostics, not a process RAM cap.
This is the .3 publication version. .2 was skipped as a public release to
avoid version ambiguity. The detailed validation below is historical evidence
from the locally verified 1.18.29-lowmem.2 binary. The .3 release was then
built for all 12 platform targets in an isolated checkout under the same 6 GiB
hard cap. Linux x64 and baseline version smokes pass; the Linux x64 binary also
passed isolated API/history compatibility and TUI numeric-sampling/exit checks.
Other platforms were cross-compiled, not executed on their native systems.
The latest upstream check on 2026-09-07
found v1.18.29 already merged, and .3 is based on that release.
The OpenCode and SDK typechecks, full embedded-web build, and synthetic binary import/reopen/fork/diff proofs passed. The session suite reported 441 passing tests with seven existing skips and one existing todo. Real PTY startup confirmed both numeric samplers started and Ctrl-D exited cleanly. Validation used a serialized 6 GiB hard cap with no swap after host RAM was freed. The code and final lifecycle delta passed review. Building this worktree did not change an installed binary or active configuration.
There is no universal RSS cap, no database retention policy or migration, and no OMO modification bundled in this release. Old durable events serve history and workspace replay, so deleting them while preserving only visible messages is not a safe general retention policy. Whole-session cold archival requires a separately agreed restore and selection contract with its own proofs.
All knobs below use the source parser for their unit: byte limits require a KB or MB suffix and count limits are plain integers. The exact string "0" disables that individual bound. Durable event page limits are fixed constants, not environment knobs.
| Area | Knob | Default | Bound |
|---|---|---|---|
| TUI payload | OPENCODE_TUI_PAYLOAD_BUDGET_MB |
256MB |
Total non-active payload bytes |
| TUI payload | OPENCODE_TUI_PAYLOAD_SESSION_LIMIT |
20 |
Retained non-active sessions |
| TUI payload | OPENCODE_TUI_ACTIVE_ALLOWANCE_MB |
128MB |
Active-session payload allowance |
| TUI payload | OPENCODE_TUI_ACTIVE_PART_MAX_MB |
32MB |
Per-part scalar-leaf cap for active sessions (see below) |
| TUI payload | OPENCODE_TUI_PART_INGRESS_MAX_KB |
256KB |
Per-part ingress bytes |
| TUI delta buffer | OPENCODE_TUI_DELTA_BUFFER_MAX_KB |
4096KB |
Pending delta bytes |
| TUI delta buffer | OPENCODE_TUI_DELTA_BUFFER_MAX_ENTRIES |
512 |
Pending delta entries |
| TUI mirror | OPENCODE_TUI_MIRROR_BUDGET_MB |
64MB |
Mirrored message bytes |
| TUI mirror | OPENCODE_TUI_MIRROR_MSG_MAX_KB |
512KB |
Per-message mirrored bytes |
| TUI mirror | OPENCODE_TUI_MIRROR_SESSION_LIMIT |
20 |
Mirrored sessions |
| TUI permissions | OPENCODE_TUI_PERMISSION_ALLOWANCE_MB |
32MB |
Stored permission-input byte bound (2x allowance) |
| TUI permissions | OPENCODE_TUI_PERMISSION_INPUT_MAX_ENTRIES |
512 |
Stored permission-input entry count bound |
| LSP documents | OPENCODE_LSP_DOC_LIMIT |
128 |
Resident full-text documents |
| LSP documents | OPENCODE_LSP_DOC_MAX_MB |
64MB |
Resident full-text bytes |
| LSP documents | OPENCODE_LSP_DOC_OPEN_ALLOWANCE_MB |
32MB |
Single-document open allowance |
| LSP documents | OPENCODE_LSP_OVERSIZED_LIMIT |
8 |
Metadata-only oversized-document records |
| LSP diagnostics | OPENCODE_LSP_PULL_DIAGNOSTICS_LIMIT |
256 |
Retained pull-diagnostic files (never-opened) |
| LSP diagnostics | OPENCODE_LSP_PULL_DIAGNOSTICS_MAX_MB |
8MB |
Retained pull-diagnostic bytes (never-opened) |
| Background jobs | OPENCODE_BGJOB_SETTLED_MAX |
100 |
Settled terminal entries |
| Background jobs | OPENCODE_BGJOB_SETTLED_OUTPUT_MAX_MB |
8MB |
Settled terminal output bytes |
| Durable events | fixed DURABLE_PAGE_ROWS / DURABLE_PAGE_BYTES |
100 / 8 MiB |
Row and serialized-byte page caps; no env knobs |
Part-cap scope (honest bounds): OPENCODE_TUI_ACTIVE_PART_MAX_MB truncates
selected scalar leaves only — part text/reasoning/completed-tool output, and
permission inputs. It is not a whole-part envelope: ToolPart.state.input,
state.metadata, error strings, SnapshotPart.snapshot, SubtaskPart.prompt,
and AssistantMessage.structured pass through untruncated. The legacy TUI's
initial and incremental message window is 100, but active-session todos and
session diffs remain unbounded. A
pathological active session therefore has no finite worst-case RSS under this
design; the bound removes the streaming-text and tool-output accumulators that
dominated real-world growth.
| Upstream PR | Reason |
|---|---|
| #42771 (event payload → side table) | only remaining schema change; revisit if event-table disk growth becomes acute |
| #43455 (snapshot retry/circuit breaker) | robustness not memory; conflicts with diff-cap customs' surface |
| #22428 (PRAGMA mmap_size=0) | no-op on Linux; macOS-targeted |
| #16695 (memory-leak consolidation) | closed unmerged by stalebot; useful pieces (LSP LRU) belong in fork customs instead |
| #33713 (idle instance eviction) | dormant upstream, wrong shape for multi-process usage |
Made for the terminal. This fork targets
opencodein the terminal (the TUI) — the daily driver it was built from, and where every memory bound is tested. Everything else (web UI,opencode serve, desktop/IDE clients) keeps working and inherits the server-side fixes too, but the UI-side memory wins are terminal-only.
curl -fsSL https://github.com/ghraw/renekris/opencode-lowmem/lowmem/scripts/fork-install.sh | bashThe script detects your OS and CPU (Linux/macOS/Windows, including
musl-vs-glibc on Linux), downloads the matching binary from the latest GitHub
release, verifies its SHA-256 against the release digest, and installs it to
~/.opencode/bin/opencode. The publish itself is an atomic same-directory
rename, so an interrupted install can never leave a truncated live binary.
Then run opencode as usual:
- Open sessions are never killed — they keep their old build.
- Every new session runs the lowmem build.
- Auth, config, sessions, and plugins carry over unchanged — same
opencode.db, same settings, nothing to migrate. - Set
"autoupdate": falsein your opencode config. With autoupdate on, the next upstream patch release would download stock opencode over the fork binary (the updater compares against upstream releases and a<upstream>-lowmem.<round>version never matches). Update the fork by re-running the install script instead.
Going back to stock opencode later is just reinstalling the upstream binary the way you originally installed it; the shared database needs no changes.
Requires bun:
git clone https://github.com/renekris/opencode-lowmem
cd opencode-lowmem
./scripts/fork-build.shBuilds all platform targets, stamps the version (<upstream>-lowmem.<round>
from git tags — round = highest existing + 1), smoke-tests, and tags the build.
Before declaring a build or zero-migration round complete, run the isolated summary-diff proof from the repository root:
BIN=packages/opencode/dist/opencode-linux-x64/bin/opencode \
SOURCE_XDG_DATA_HOME="$HOME/.local/share" \
bun scripts/ram-bounds/summary-diff-proof.tsThe script validates SOURCE_XDG_DATA_HOME and an optional
SANDBOX_XDG_DATA_HOME before use: configured roots cannot contain .., be
symlinks, or overlap after both roots are canonicalized with realpath(). A
configured sandbox is only created under its nearest existing ancestor after
that ancestor is verified component-by-component to be free of symlinks, so a
symlinked ancestor cannot redirect creation into the live data root. It
opens the source database through sqlite3 --readonly and uses the CLI
.backup command to make a WAL-safe copy inside the sandbox. The destination
path is rejected if it contains a single quote or control character, rather
than relying on dot-command quoting. The binary never receives the source
root: it receives an environment allowlist containing PATH, sandbox HOME,
the four sandbox XDG directories, LANG, and TZ.
After the backup, every session directory in the sandbox database is remapped
to the sandbox scratch directory. The script runs session list, then reads
only the sandbox database. A metadata-only candidate gets the always-runnable
HTTP diff assertion, which requires HTTP 200 and the stored file/count/status
metadata. A retained-snapshot candidate must have both snapshot hashes in its
step-start/step-finish parts and matching storage under
opencode/snapshot/<project-id>/<sha1(worktree)> and a copyable standalone Git
worktree (worktrees whose .git uses object alternates are disqualified
because the alternates point at a live object store). The script ranks
qualifying worktrees with du and skips the retained branch with that measured
reason when the smallest exceeds the 300 MiB bound, and each attempt starts
from a clean scratch directory so a failed candidate cannot leak into the next
one. The copies themselves are transfer-bounded with a chunked copy that never
writes more than the remaining byte allowance, so even a concurrently growing
source cannot push peak sandbox disk usage past the bound — and the snapshot
Git storage is made self-contained in the sandbox: the full object closure
reachable from the two snapshot hashes is enumerated with rev-list --objects
streamed to a file whose capture is capped at the remaining candidate bound
(never buffered in this process), materialized locally by piping
pack-objects --stdout into the same capped writer — git is killed the moment
the pack crosses the remaining allowance, so a pathological pack can never
land in full — indexed with index-pack, and the closure list plus
pack+index(+rev) bytes are post-checked against the remaining bound before the
borrowed alternates link into the live repository is removed. The index pair
is the one artifact that can transiently overshoot before that post-check
(its size is proportional to the packed object count, not content size); a
failing check removes the whole candidate. The
closure walk is repeated alternates-free so a missing blob anywhere in the
reachability set fails before any git command serves the proof. Candidates
are tried smallest-first until one satisfies the branch; only after every
qualifying candidate fails does the retained check report SKIP with the
aggregated per-candidate reasons. Otherwise it starts one sandbox-owned
serve process, and requires the retained HTTP diff response to contain a real
non-omitted patch string. Both checks use the remapped sandbox directory.
Missing source, missing sqlite3, invalid configured roots, a non-empty
configured sandbox, an unsafe backup destination, or a missing BIN are
friendly failures. Missing metadata or retained candidates are explicit
SKIP results, never fabricated fixtures. Shutdown sends SIGTERM, waits at
most two seconds, then sends SIGKILL and waits up to another two seconds under
a hard deadline. Set SANDBOX_XDG_DATA_HOME to an empty isolated directory to keep
the sandbox for inspection; otherwise the generated temporary directory is
removed during cleanup.
Rebase onto the new upstream tag. Behavior-pinning tests fail loudly if an upstream refactor moved a hunk — re-check the seam, never delete the test. Full upkeep procedure and rollback recipe: fork section of AGENTS.md.
Rebase seams (where fork hunks live inside upstream files; everything else is fork-owned files):
packages/tui/src/app.tsx— session-id-in-title custom on the terminal-title effect (Fork(lowmem)comment block; guarded bytest/session-title.test.tsx)packages/opencode/src/session/processor.ts— #43881 empty-stream guard + #42176finish = "error"(guarded bytest/session/processor-effect.test.ts)packages/opencode/src/provider/provider.ts— #43607wrapSSEsingle deadline (guarded bytest/provider/header-timeout.test.ts)packages/opencode/src/session/session.ts— one-line shallow-copy inupdatePart(#35107), plus theSession.fork()legacy-summary trim that strips full patches from cloned summaries before re-publishing them as new durable eventspackages/opencode/src/session/summary.ts— one-line write seam applying metadata-only trimming before the durable message updatepackages/opencode/src/session/summary-diff-trim.ts— fork-owned helper that strips patch text from new summary entries; keep it separate for rebasespackages/opencode/src/cli/cmd/run/subagent-data.ts+stream.transport.ts— run-UI eviction (settle/revive/compact helpers; small transport delta)packages/tui/src/context/sync.tsx— markedFork(lowmem)hunks: delta-buffer wiring + flushed-part tracking, inbound-rank hook + root purge,session.deletedcleanup, eviction-gate breaks, budget accounting hooks (append/replace/remove/part-upsert/bulk-hydration), permission-askedtoolInputcapture (guarded bytest/payload-budget.test.ts,test/no-revival.test.ts,test/cli/cmd/tui/sync-payload-eviction.test.tsx)packages/tui/src/routes/session/index.tsx,subagent-footer.tsx— conveyor call-site injections onlypackages/tui/src/context/event.ts— subscribe-time owner cleanup for event subscriptions, released in1.18.29-lowmem.4(guarded bytest/context/event.test.tsx)
RAM-bounds seams (rounds 1–2, same rules — re-check each on rebase):
packages/core/src/event.ts— codec WeakMap cache + two-phase byte-awarereadAfterpaging (test/durable-paging.test.ts,test/event.test.ts)packages/core/src/background-job.ts— terminal ring + settled contract + late-callerwaitForPromotion(test/background-job-settled.test.ts)packages/opencode/src/cli/cmd/db.ts— stats partial port + wiring; fork vacuum guard lives in fork-owneddb-vacuum.ts(test/db-cmd.test.ts)packages/opencode/src/lsp/client.ts— lifecycle coordination: bounded close-tombstones, per-open generation tokens, oversized transient close, deferred single-flight loader (test/lsp-reopen.test.ts,test/document-store.test.ts)packages/opencode/src/permission/index.ts+session/tools.ts— preserve and populatetoolInputonpermission.asked(same tests as schema row)packages/schema/src/v1/permission.ts— optional typedtoolInputfield (contract test inpackages/sdk/js; schema manifest count intentionally shifts — pre-existing failures documented in the round-1 evidence)packages/tui/src/context/data.tsx— mirror-budget wiring; implementation is fork-ownedcontext/mirror-budget.ts(test/mirror-budget.test.tsx)packages/tui/src/plugin/adapters.tsx—requestRevivalcall-sites removed (no-revival rule;test/no-revival.test.ts)packages/tui/src/routes/session/permission.tsx— readstoolInputfrom the permission map (store copy is stripped; same payload-budget tests)packages/sdk/js/src/v2/gen/*— REGENERATED viabun run generate(packages/client) for thetoolInputsurface; never hand-edited
Watch-list (adopt upstream if merged, replacing our port): #39970
(comprehensive stream-incomplete handling, supersedes #43881/#43607), #41466
(same empty-stream bug via new error type), #40142 (finish=length loop exit),
#43302 (v2 sync engine — design-borrow only). Candidates adopted from the
latest update sweep: #39930 (bound compacted history hydration),
#38939 (PubSub allBounded listener leak), #41950 (config global-cache clone),
#33713 (evict idle per-directory serve instances), #44631 (Bedrock 16 MiB
event-stream frame reject).
Deferred ports: #43769 (parallel-session snapshot scan CPU −77%; blocked —
authored against the post-split packages/ai/packages/util tree that does
not exist in our base yet) and #40698 (TUI
syntax-highlight LRU cache; wraps getTreeSitterClient().highlightOnce).
Correction 2026-08-25: highlightOnce IS present in our pinned
@opentui/core (lib/tree-sitter/client.d.ts) — the earlier absence claim
was wrong. #40698 stays deferred for scope/priority (own behavior-preserving
round), not for a missing seam; #43769 stays blocked until the base carries
the post-split tree. Porting #43769 now would mean inventing seams upstream
will replace. The perf PRs that were riding this tag (#42826, #43292, #42346,
#42579, #42741, #42952, #43191, #43158, #42467, #42458, #42468, #42972)
arrived with the v1.18.29 merge.
Backlog (unclaimed, no upstream equivalent): run-UI delta coalescing
(packages/opencode/src/cli/cmd/run/session-data.ts
still concatenates and flushes per delta — changing it means changing footer
render cadence, so it needs its own behavior-preserving round); serve-mode SSE
event queue is unbounded (handlers/event.ts) — backpressure policy needed;
log rotation cap; tool-output/ retention policy; PRAGMA auto_vacuum.
All ported work is credited in the tables above and in each commit's
(port of upstream #NNNNN) trailer. Upstream authors did the hard diagnosis;
this fork just ships it.
This fork builds on opencode — the open source AI coding agent, pinned to the latest merged upstream release tag (see this repo's tags and releases for the current base). See the upstream README for everything not fork-specific.