Skip to content

oom(27/29): bound renderer components (native-chat, pet, sidebar, automations) - #10320

Closed
nwparker wants to merge 1 commit into
oom/26-b-renderer-editor-terminalfrom
oom/27-b-renderer-components
Closed

nwparker wants to merge 1 commit into
oom/26-b-renderer-editor-terminalfrom
oom/27-b-renderer-components

Conversation

@nwparker

@nwparker nwparker commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

OOM hardening slice 27/29 — bound renderer components (native-chat, pet, sidebar, automations)

Part of a 29-PR stack re-landing #10179 ("bound OOM-prone accumulators across Orca"), which was reverted in #10255 for being too large (~1,600 files). This is slice 27/29 (base: oom/26-b-renderer-editor-terminal). Merge in numeric order. The full stack's end-state is byte-for-byte the commit that passed #10179's complete CI matrix (36k+ tests, multi-OS).

Summary

This slice re-lands memory ceilings across renderer components: a byte/entry-bounded native-chat scope cache (drafts, session options, pending sends), bounded custom-pet blob/media caches and load queue, a rolling 256KB automation-run output buffer, bounded file-explorer undo history, model-enrichment caps, and ~12 concurrency caps on repo/host/worktree fan-outs. All changes are whole-file and ship with dedicated bound tests.

ELI5

The app has lots of little in-memory "scratch pads" — draft chat text, pet images, automation logs, undo history — that previously grew forever and could crash the app by eating all memory. This change puts a size limit on each: once a pad is full it drops the oldest stuff (or refuses new stuff). It also stops the app from firing off hundreds of network/git requests at once by only allowing a handful in parallel. Normal users never notice; only pathological inputs hit the limits.

Proof of OOM fix

  • native-chat-composer-scope-cache.ts: bounded scope cache — 128 entries/cache, 32MB aggregate, 16MB/value, 4KB/key; UTF-8 byte accounting with per-entry release on delete/clear. Tests: native-chat-composer-scope-cache.test.ts (retains at-limit, rejects one byte over, aggregate eviction, byte-accounting release, 128-entry cap).
  • native-chat-pending.ts: PENDING_SEND_LIMIT=8 (pending.slice(-8)) and COMMAND_MARKER_LIMIT=8. Test: native-chat-pending.test.ts.
  • native-chat-session-option-enrichment.ts: NATIVE_CHAT_MODEL_ENRICHMENT_CACHE_MAX=128 inactive entries evicted FIFO; NATIVE_CHAT_MODEL_ENRICHMENT_PENDING_MAX=8 concurrent probes; pendingEnrichmentCount decremented in finally (no leak). Test: native-chat-session-option-enrichment.test.ts.
  • pet/custom-pet-load-queue.ts: MAX_CONCURRENT_CUSTOM_PET_LOADS=2, MAX_PENDING_CUSTOM_PET_LOADS=16 (rejects over cap). Test: custom-pet-load-queue.test.ts.
  • pet/pet-blob-cache.ts: CUSTOM_PET_BLOB_CACHE_MAX=16 blob URLs, CUSTOM_PET_MEDIA_CACHE_MAX_BYTES=128MB; evicts only inactive entries, revokes object URLs. Test: pet-blob-cache.test.ts.
  • pet/sprite-frame-detection.ts: MAX_CUSTOM_PET_DETECTED_FRAMES cap — returns null rather than allocating unbounded frames. Test: sprite-frame-detection.test.ts.
  • automations/automation-run-output-snapshot.ts: MAX_OUTPUT_SNAPSHOT_CHARS=256KB rolling FIFO buffer, sets truncated flag, strips ANSI/OSC control sequences. Test: automation-run-output-snapshot.test.ts.
  • right-sidebar/fileExplorerUndoRedo.ts: MAX_STEPS=50, entry cap 16MB, aggregate cap 32MB; drops oldest past ops. Test: fileExplorerUndoRedo.test.ts.
  • settings/mcp-config-inspection.ts: skips reading config files over MCP_CONFIG_INSPECTION_MAX_BYTES (constant in shared foundation slice). Test: mcp-config-inspection.test.ts.
  • Concurrency (fan-out) caps via mapSettledWithConcurrency: GITLAB_REPO_FETCH_CONCURRENCY=8, PROJECT_GROUP_LOOKUP_CONCURRENCY=4, WORKSPACE_PORT_SCAN_CONCURRENCY=4, SOURCE_CONTROL_QUIESCE_CONCURRENCY=8, FILE_EXPLORER_REFRESH_CONCURRENCY=8, RUNTIME_ENVIRONMENT_REFRESH_CONCURRENCY=5, REPO_HOOK_PROBE_CONCURRENCY=4, SSH_TARGET_STATE_READ_CONCURRENCY=8, WORKTREE_META/LINEAGE_MUTATION_CONCURRENCY=8, CROSS_REPO_DELETE_CONCURRENCY=4, RUNTIME_HOST_CATALOG_FETCH_CONCURRENCY=4. Boundary tests: task-page-gitlab-fanout-boundary.test.ts, settings-repo-hook-fanout-boundary.test.ts, delete-worktree-parallel-flow.test.ts, github-item-dialog-source-boundary.test.ts, pull-request-page-host-boundary.test.ts.
  • PullRequestPage.tsx / TaskPage.tsx wiring: COMBINED_DIFF_VIEW_STATE_CACHE_MAX_ENTRIES + retainCombinedDiffSectionText/retainCombinedDiffViewStateText release cached diff text for evicted files (bound constants live in editor foundation slice).

Regressions

Normal-path (ordinary use, below the new limits): none — behavior is unchanged below the ceilings.

Overload-only (extreme input / sustained backpressure) — intended, documented degradations:

  • Pending native-chat sends beyond 8 per pane/agent are dropped (oldest first) — only reachable if 9+ sends queue unconfirmed simultaneously.
  • Model enrichment is silently skipped for new hosts while 8 discovery probes are in flight; retried on next render once a probe settles (count decremented in finally).
  • Importing a custom pet is rejected once 16 loads are pending; blob cache refuses a new pet once 16 are actively retained or 128MB media is resident.
  • Automation run output over 256KB is truncated to the most recent 256KB with a truncated indicator.
  • File-explorer undo history is capped at 50 steps / 32MB aggregate; oldest undo steps expire silently.
  • Combined-diff view-state/section text for files beyond the retention cap is released and re-derived when scrolled back into view (very large PRs/tasks only).
  • Fan-out operations (repo fetch, port scan, worktree mutation, cross-repo delete, host reads) run at most 4-8 at a time — slower wall-clock only when the underlying set is large.
  • Adding a 17th actively-displayed custom pet (or importing while 16 loads are pending) fails to render/import; cacheCustomPetBlobUrl returns false and the load queue rejects with 'Too many pending custom pet loads'. (only when: User imports/displays more than 16 custom pets simultaneously, or queues >16 concurrent loads — far above ordinary use of a cosmetic feature.)
  • Sprite-frame auto-detection returns null (no frame overlay) for sprite sheets that decompose into more than MAX_CUSTOM_PET_DETECTED_FRAMES frames. (only when: Pathologically large/noisy sprite sheet in an imported pet bundle.)
  • File-explorer undo cannot reach steps older than 50 (or beyond 32MB aggregate); very old undo entries silently expire. (only when: More than 50 file-explorer mutations without undoing, or a single op over 16MB.)

Build / CI

Adversarial review

Verified numeric ceilings and judged headroom:

  • pet-blob-cache.ts: CUSTOM_PET_BLOB_CACHE_MAX=16 pets, CUSTOM_PET_MEDIA_CACHE_MAX_BYTES=128MB; custom-pet-load-queue.ts MAX_PENDING=16, MAX_CONCURRENT=2. Cosmetic feature; 17th simultaneously-displayed pet is far above ordinary use. cacheCustomPetBlobUrl returning false is tolerated by callers (gated on pending>=16). Overload-only.
  • sprite-frame-detection.ts: returns null past MAX_CUSTOM_PET_DETECTED_FRAMES (shared limit) — pathological sprite sheets only.
  • automation-run-output-snapshot.ts: MAX_OUTPUT_SNAPSHOT_CHARS=256KB rolling buffer with truncated flag. This is a SAVED snapshot artifact, and selectAutomationRunOutputSnapshot prefers the hook transcript (assistantMessage) when present; terminal snapshot is fallback. Keeps most-recent 256KB. Overload-only truncation of saved output.
  • fileExplorerUndoRedo.ts: only new bounds are the 16MB/32MB byte caps (see boundsOrCleanupBugs); MAX_STEPS=50 pre-existed at BASE. Edge-only.
  • native-chat-session-option-enrichment.ts: PENDING_MAX=8 in-flight per-host discovery probes, CACHE_MAX=128. CONFIRMED the pendingEnrichmentCount is decremented in BOTH the synchronous-throw catch AND the promise .finally, so the cap cannot get permanently stuck. Seed model is immediately usable; only background enrichment is deferred and retried on next render. Overload-only (users have few hosts).
  • native-chat-pending.ts: PENDING_SEND_LIMIT=8 unconfirmed sends per pane, drops oldest. 9+ simultaneous unconfirmed sends is not ordinary. Overload-only.
  • native-chat-composer-scope-cache.ts: 128 scopes / 32MB aggregate / 16MB value / 4KB key, global-LRU eviction of least-recently-touched scope. A user has a handful of composer scopes, not 128; LRU never fires in normal use. set() returns false only on oversized key/value/aggregate. Verified no active-draft eviction path in ordinary use.
  • Concurrency caps are wall-clock-only, not correctness: GITLAB_REPO_FETCH_CONCURRENCY=8, PROJECT_GROUP_LOOKUP_CONCURRENCY=4, WORKSPACE_PORT_SCAN_CONCURRENCY=4, SOURCE_CONTROL_QUIESCE_CONCURRENCY=8, FILE_EXPLORER_REFRESH_CONCURRENCY=8, combined-diff load scheduler maxConcurrent=2 (GitHubItemDialog/PullRequestPage). These only slow wall-clock when the underlying set is large.
  • Combined-diff section-text retention (GitHubItemDialog/PullRequestPage/TaskPage) wires retainCombinedDiffSectionText + createCombinedDiffLoadScheduler; the actual retention CAP lives in editor/combined-diff-text-retention.ts (a DIFFERENT slice), so it's out of scope here. Wiring evicts loaded-but-scrolled-away section text and re-derives on scroll-back — only reachable on very large PRs/tasks.

All caps ship with dedicated bound tests (custom-pet-load-queue.test.ts, pet-blob-cache.test.ts, fileExplorerUndoRedo.test.ts, native-chat-*.test.ts, automation-run-output-snapshot.test.ts, WorkspacePortScanner.test.tsx, useFileExplorerTree.test.ts, plus fanout-boundary tests). OOM evidence is real and corroborated. No off-by-one, no missing release on error/abort, no broken FIFO. Safe to open as a draft PR for human review.

Files

58 files changed (+2300 / −840).


🤖 Decomposed, reviewed & assembled by Claude Code from the reverted #10179. Draft — do not merge out of order.

Made with Orca 🐋

@nwparker
nwparker force-pushed the oom/26-b-renderer-editor-terminal branch from e6c630e to 1edaa5e Compare July 24, 2026 07:49
@nwparker
nwparker force-pushed the oom/27-b-renderer-components branch from db22c47 to 0dc8f04 Compare July 24, 2026 07:49
@nwparker
nwparker force-pushed the oom/26-b-renderer-editor-terminal branch from 1edaa5e to 0989736 Compare July 25, 2026 02:21
@nwparker
nwparker force-pushed the oom/27-b-renderer-components branch from 0dc8f04 to 49f07ba Compare July 25, 2026 02:21
@nwparker
nwparker force-pushed the oom/26-b-renderer-editor-terminal branch from 0989736 to b4dd4b3 Compare July 25, 2026 02:40
@nwparker
nwparker force-pushed the oom/27-b-renderer-components branch from 49f07ba to 5bf5366 Compare July 25, 2026 02:40
@nwparker
nwparker force-pushed the oom/26-b-renderer-editor-terminal branch from b4dd4b3 to f7f7b90 Compare July 25, 2026 03:11
@nwparker
nwparker force-pushed the oom/27-b-renderer-components branch from 5bf5366 to 1f063cd Compare July 25, 2026 03:11
Files: 58 applied, 0 deleted (from 6eb70d8)

Co-authored-by: Orca <help@stably.ai>
@nwparker
nwparker force-pushed the oom/26-b-renderer-editor-terminal branch from f7f7b90 to e4f6b98 Compare July 25, 2026 03:34
@nwparker
nwparker force-pushed the oom/27-b-renderer-components branch from 1f063cd to 9a4f14d Compare July 25, 2026 03:34
@nwparker

Copy link
Copy Markdown
Contributor Author

Superseded by #10548, which re-scopes this work to the bound-at-ingress doctrine (see #10533 and src/shared/memory-safety/README.md).

An audit found the original #10179 scope was ~50x the size of the fix for the crash it targeted, and that bounds had landed ahead of their callers. Every file in these slices was re-adjudicated individually: ingress bounds (transport frames, PTY flow control, on-disk transcripts, parsing, enumeration, media decode, user-keyed accumulators) were kept; interior ceilings behind an already-bounded input were dropped. 1,882 files → 1,155, full suite green, and no main feature regressed.

No memory protection was lost — only speculation.

@nwparker nwparker closed this Jul 25, 2026
Sign up for free to 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.

1 participant