How Nodeterm Manages Offscreen Terminal Memory and Resources
Nodeterm treats offscreen terminals as disposable viewers that can be released without killing the underlying tmux session, using a three-stage pipeline of visibility tracking, configurable timers, and safety predicates that defer disposal when active agents are present.
Nodeterm, developed in the open-source repository eneskirca/nodeterm, implements sophisticated offscreen terminal resource management to prevent memory leaks in long-running desktop sessions. The architecture distinguishes between the visual viewer component and the persistent tmux session, enabling aggressive memory reclamation while preserving CLI state. This analysis examines the source code mechanisms that determine when and how terminals are safely disposed, parked, or hibernated.
Visibility Tracking with IntersectionObserver
Each terminal node mounts a single IntersectionObserver that monitors viewport presence. When a node leaves the visible area, the observer marks it as offscreen via setNodeOffscreen in src/renderer/nodes/TerminalNode.tsx.
The implementation maintains a global offscreenNodes set that records all offscreen IDs:
// src/renderer/nodes/TerminalNode.tsx
if (offscreen) offscreenNodes.add(nodeId) else offscreenNodes.delete(nodeId);
This set is consulted during the hibernation sweep to determine which terminals are candidates for resource release. The observer remains mounted for the lifecycle of the node, providing real-time visibility state without polling overhead.
Off-Screen Disposal Policy and Timer Configuration
The core disposal logic resides in src/renderer/terminal/offscreen-policy.ts. The system converts user settings into millisecond timeouts via offscreenDisposeMs(settingMinutes), which processes settings.offscreenTerminalMinutes (defaulting to 10 minutes). A value of zero or negative disables the feature entirely.
The planOffscreenVisibility function evaluates four states for each node:
- Arm a dispose timer if the node is offscreen and no timer exists
- Cancel an existing timer if the node becomes visible
- Revive a previously disposed view if it returns to visibility
- Defer if safety predicates block disposal
// Off-screen policy decision
const plan = planOffscreenVisibility({
visible: isNodeWatched(id),
down: offscreenDown,
timerArmed: !!offscreenTimerRef.current,
disposeMs: offscreenDisposeMs(settings.offscreenTerminalMinutes),
});
if (plan.armTimer) offscreenTimerRef.current = setTimeout(disposeViewer, plan.disposeMs);
Users control this behavior via Settings → Terminal → "Off-screen minutes" (settings.offscreenTerminalMinutes). Setting this to 0 causes releaseStillEnabled to return false, disabling automatic disposal.
Safe Release: Live-Work and Eco Guards
Before tearing down a PTY client, Nodeterm evaluates two safety predicates to prevent interrupting active workflows.
Live-Work Protection (src/renderer/terminal/live-work.ts): The shouldDeferReleaseForLiveWork check prevents disposal when the PTY is the actual shell (not tmux-backed) and a live agent (working, waiting, or blocked) is attached. The wouldKillLiveWork function returns true when !i.tmuxBacked && !!i.agentState && LIVE_AGENT_STATES.has(i.agentState):
// src/renderer/terminal/live-work.ts
export function wouldKillLiveWork(i: LiveWorkInput): boolean {
return !i.tmuxBacked && !!i.agentState && LIVE_AGENT_STATES.has(i.agentState);
}
Eco Deferral (offscreen-policy.ts): The shouldDeferReleaseForEco function adds a secondary guard when the Eco hibernation feature is active. It calculates a cap based on idleMinutes + offscreenMinutes to ensure that even non-hibernating agents are eventually released after a bounded window:
// src/renderer/terminal/offscreen-policy.ts
export function shouldDeferReleaseForEco(i: {
ecoEnabled: boolean;
resumableAgent: boolean;
hibernated: boolean;
idleKnown: boolean;
offscreenElapsedMs: number;
idleMinutes: number;
offscreenMinutes: number;
}): boolean {
if (!i.ecoEnabled || !i.resumableAgent || i.hibernated || !i.idleKnown) return false;
if (!(i.idleMinutes > 0)) return false;
const cap = (i.idleMinutes + Math.max(0, i.offscreenMinutes)) * 60_000;
return i.offscreenElapsedMs < cap;
}
These guards ensure that AI agents like Claude Code or other long-running processes are not killed mid-turn when their terminal scrolls offscreen.
Remote and Relay Session Exclusion
Offscreen disposal is bypassed for terminals whose sessions live on remote machines. The offscreenCoreIsRemote(session.source) function returns true for relay or server sources, causing mayDisposeOffscreen to return false.
This exclusion prevents attempts to resurrect remote tmux sessions while the SSH ControlMaster connection is down, which would otherwise render an offline overlay. The check ensures that only local terminal viewers are candidates for the offscreen disposal pipeline.
Parked Terminals and LRU Caching
If a node is unmounted (e.g., during a project switch) before the offscreen timer fires, Nodeterm parks the PTY client rather than killing it immediately. The park system is implemented as a bounded LRU cache in src/renderer/terminal/park-budget.ts with a default lifetime of 5 minutes (TERM_PARK_MS).
When the park entry expires, canDisposeParkedEntry checks the same live-work predicate before finally terminating the PTY. This caching strategy allows users to pan back to a node quickly (warm re-attach) while still reclaiming memory for long-idle viewers. The park budget operates independently of the offscreen timer but shares the same safety constraints.
Integration with Eco Hibernation
The offscreen release pipeline is ordered after Eco hibernation. If Eco is enabled, the release waits for the agent to be hibernated (shouldDeferReleaseForEco). The combined idleMinutes + offscreenMinutes cap guarantees that a node which never becomes hibernatable—such as a cron job or never-idle agent—will still be freed after a bounded period, preventing permanent memory leaks.
This layered approach ensures that nodeterm offscreen terminal memory management respects both aggressive power-saving policies and the requirement for bounded resource usage.
Summary
- Nodeterm distinguishes between disposable terminal viewers and persistent tmux sessions, enabling memory reclamation without killing underlying processes.
- Visibility tracking uses a single
IntersectionObserverperTerminalNodeto populate the globaloffscreenNodesset. - Safety predicates in
live-work.tsandoffscreen-policy.tsdefer disposal when agents are active or when Eco hibernation is pending. - Remote sessions are excluded from disposal to prevent SSH reconnection failures when ControlMaster is unavailable.
- Parked terminals live in a 5-minute LRU cache (
park-budget.ts) for warm re-attachment before final disposal. - User configuration controls the disposal timer via
settings.offscreenTerminalMinutes, set to 10 minutes by default.
Frequently Asked Questions
What happens to the tmux session when a terminal goes offscreen?
The tmux session persists. Nodeterm treats only the viewer component as disposable; releasing an offscreen terminal viewer detaches the local PTY client but leaves the underlying tmux session running. When you return to the terminal, Nodeterm reattaches to the existing session rather than creating a new one.
How does Nodeterm prevent killing active AI agents during offscreen disposal?
The system checks shouldDeferReleaseForLiveWork before disposal. If the terminal is the actual shell (not tmux-backed) and an agent is in a working, waiting, or blocked state (as defined by LIVE_AGENT_STATES in live-work.ts), the disposal is deferred. Additionally, shouldDeferReleaseForEco waits for Eco hibernation to complete before releasing resumable agents.
Can I disable automatic offscreen terminal disposal?
Yes. Set Settings → Terminal → "Off-screen minutes" to 0 or any negative value. This causes releaseStillEnabled to return false and disables the disposal timer entirely. Alternatively, set a very high value to delay disposal significantly while keeping the feature active for extremely long idle periods.
What is the difference between parking and disposing a terminal?
Parking occurs when a terminal node is unmounted (e.g., switching projects) and stores the PTY client in an LRU cache for 5 minutes (TERM_PARK_MS), allowing instant reattachment. Disposing occurs when the offscreen timer fires or the park expires, and actually terminates the PTY client after checking live-work and Eco safety predicates. Parking is a temporary cache; disposal is permanent resource release.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →