Skip to content

Bound terminal output flow across Electron and xterm#169

Merged
luckeyfaraday merged 3 commits into
mainfrom
fix/terminal-output-flow-control
Jun 20, 2026
Merged

Bound terminal output flow across Electron and xterm#169
luckeyfaraday merged 3 commits into
mainfrom
fix/terminal-output-flow-control

Conversation

@luckeyfaraday

@luckeyfaraday luckeyfaraday commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Two unbounded-memory paths could drive the Electron main process to V8 heap OOM. This branch bounds both.

Terminal output flow (aafdaac, a5f9b64)

  • cap PTY-host and Electron pending terminal output backlogs
  • add per-terminal sequence acknowledgements so Electron sends at most one unacknowledged renderer batch
  • serialize xterm writes and retain only a bounded 64k pending tail
  • bound terminal SSE/backfill paths and clear output state when terminals exit
  • ignore local Grok agent configuration via .grok/

Agent session transcript scans (ee83d09)

  • add readFilePrefix (512 KB cap via fs.open/read) and replace whole-file readFile(…, "utf8") reads in the codex, claude, and grok session scanners (plus the terminal-restore path)
  • add mapWithConcurrency (cap 8) in place of unbounded Promise.all over session files
  • memoize the workspace-independent ~/.codex/sessions metadata scan in a 30s cache so it runs once instead of once per workspace

Root cause (transcript scans)

PR #154 (b4c801c, "Improve multi-workspace review handoffs", first shipped in v0.1.9) replaced the single active-workspace session scan with an all-workspaces fan-out:

for (const reviewWorkspace of workspaces) void refreshWorkspaceAgentSessions(reviewWorkspace);

With N saved workspace tabs this issues N concurrent listAgentSessions calls. Each one re-scanned the global ~/.codex/sessions corpus and materialized every file via an unbounded Promise.all(files.map(readFile(…, "utf8"))), so N workspaces produced N concurrent full copies of the same transcript corpus. The per-workspace 30s session cache does not help (it is keyed by workspace), and the utf8 decode path matches the crash stack StringDecoder::DecodeData → NewStringFromUtf8 → NewRawTwoByteString.

Evidence

Terminal output flow

The Electron main process hit V8 heap OOM at 12:45:12, 14:48:07, and 15:15:31 CEST. The 15:15 recurrence ran the initial backlog-cap build, showing retained strings were bounded but Electron IPC and xterm write queues were still unbounded.

Transcript scans (identical corpus, 2 GB heap, v0.1.9)

Scenario Result Peak RSS
1 workspace (pre-#154 behavior) completed 1.40 GB
11 workspaces (#154 behavior) V8 heap OOM 2.88 GB
11 workspaces (bounded reader) completed 0.94 GB

Verification

  • npm run build:electron
  • npm run test:electron — 135 tests across 20 files pass (incl. terminal-buffer, file-prefix)
  • npm run build
  • packaged client/release/ATHENA-0.1.9.AppImage

Follow-up

The Hermes provider scan had the same global-corpus-rescanned-per-workspace shape; it is deduplicated separately in #170.

@luckeyfaraday luckeyfaraday merged commit 3802202 into main Jun 20, 2026
luckeyfaraday pushed a commit that referenced this pull request Jun 20, 2026
readHermesSessions() re-ran the entire workspace-independent ~/.hermes scan
-- reading and JSON-decoding up to MAX_PROVIDER_ROWS session files -- once per
open workspace. After PR #154 began refreshing agent sessions for every open
workspace tab, an N-workspace review fanned this into N concurrent full
re-reads of the same corpus, multiplying peak heap by the workspace count.

Scan the corpus once per CACHE_TTL_MS via a new memoizeAsyncWithTtl helper and
apply the per-workspace match to the shared result. This closes the last
unbounded per-workspace session-scan path from the v0.1.9 OOM investigation,
complementing the codex/claude bounding in #169.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JJPuertas pushed a commit to JJPuertas/Athena that referenced this pull request Jun 20, 2026
The codex `~/.codex/sessions` metadata scan (bounded in luckeyfaraday#169) carried its
own inline TTL cache with identical semantics to the `memoizeAsyncWithTtl`
helper added for the Hermes scan dedup (luckeyfaraday#170): share one in-flight promise
per TTL window, never cache rejections. Fold the codex cache into the shared
helper to remove the duplicated cache bookkeeping. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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