Session Management for Long-Running Terminal Sessions in DesktopCommanderMCP
DesktopCommanderMCP handles long-running terminal sessions through a PID-keyed TerminalManager that maintains bounded output buffers (50 MiB), detects interactive prompts, and provides snapshot-based pagination to stream output efficiently without unbounded memory growth.
DesktopCommanderMCP provides robust session management for long-running terminal sessions by tracking child processes through their entire lifecycle—from spawn to termination—while protecting against memory exhaustion through intelligent output eviction. The core implementation resides in src/terminal-manager.ts, which maintains active sessions in memory and exposes APIs for paginated reads, interactive input detection, and forceful termination. This architecture enables the UI to monitor background processes, detect when commands enter interactive (blocked) states, and retrieve historical output without losing data to buffer overflows.
Session Lifecycle and PID-Based Storage
Creating Sessions with executeCommand()
When executeCommand() spawns a child process, it instantiates a TerminalSession object containing the process handle, line-based output buffers, timing metadata, and eviction counters. The session is immediately registered in the internal sessions Map using the process PID as the unique key.
// src/terminal-manager.ts – session registration
this.sessions.set(childProcess.pid, session);
Active vs. Completed Sessions
Active sessions live in this.sessions while their processes are running. Upon process exit, the manager copies the buffered output into a CompletedSession object and moves it to this.completedSessions. To prevent memory leaks, only the most recent 100 completed sessions are retained; older entries are automatically purged.
// src/terminal-manager.ts – move to completedSessions
this.completedSessions.set(childProcess.pid, {/*...*/});
this.sessions.delete(childProcess.pid);
Memory-Safe Output Buffering
The 50 MiB Buffer Cap
Each session maintains an outputLines array and tracks bufferedChars to enforce a hard limit defined by MAX_BUFFERED_OUTPUT_CHARS (50 MiB). When a session’s buffered output exceeds this threshold, the manager evicts the oldest lines first, updating evictedLines and evictedChars counters to maintain data integrity for clients.
while (session.bufferedChars > MAX_BUFFERED_OUTPUT_CHARS && session.outputLines.length > 1) {
const dropped = session.outputLines.shift()!;
const droppedJoinedChars = dropped.length + 1;
session.bufferedChars -= droppedJoinedChars;
session.evictedChars += droppedJoinedChars;
session.evictedLines++;
}
This eviction strategy prevents V8 string-length limits and ensures predictable memory usage even for indefinitely running commands like log tailers or REPLs.
Detecting Interactive (Blocked) States
Prompt Pattern Matching
The manager analyzes stdout and stderr streams against regex patterns to detect when a process enters an interactive state. When patterns like >>>, >, $, or # are detected at the end of output, session.isBlocked is set to true and the command promise resolves early, allowing the UI to present an input box.
const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/;
if (quickPromptPatterns.test(text)) {
session.isBlocked = true;
resolveOnce({ pid, output, isBlocked: true });
}
Periodic Process Analysis
A setInterval periodically invokes analyzeProcessState() from src/utils/process-detection.ts to apply heuristic detection for prompts that may evade simple regex matching. This dual-layer approach catches both standard shells (Bash, Zsh, PowerShell) and custom REPL behaviors.
Output Retrieval and Pagination
Paginated Reads with readOutputPaginated()
The readOutputPaginated(pid, offset, length) method returns sliced output along with metadata including total lines, remaining lines, eviction information, exit code, and runtime. The offset parameter supports intelligent positioning:
offset = 0reads from the last-read position (the "new output" view)- Negative offsets fetch tails of the buffer (e.g.,
offset = -100returns the last 100 lines)
// src/terminal-manager.ts – pagination entry point
readOutputPaginated(pid: number, offset = 0, length = 1000): PaginatedOutputResult | null
Snapshot-Based Incremental Updates
For efficient UI polling without resending entire buffers, the manager provides snapshot-based APIs:
captureOutputSnapshot(pid)records the total character count and line count at a specific moment, including evicted characters.getOutputSinceSnapshot(pid, snapshot)returns only the new output generated after the snapshot, correctly handling cases where output was evicted between polls.
captureOutputSnapshot(pid) // src/terminal-manager.ts#L658-L670
getOutputSinceSnapshot(pid, snapshot) // src/terminal-manager.ts#L682-L690
Process Interaction and Termination
Sending Input to Running Processes
The sendInputToProcess(pid, input) method writes data to the child’s stdin, automatically appending a newline if the input lacks one. This enables the UI to respond to isBlocked states by injecting user commands into interactive shells.
// src/terminal-manager.ts – send input
sendInputToProcess(pid, input) // lines 57-71
Graceful and Forceful Termination
forceTerminate(pid) implements a two-phase shutdown strategy: it first sends SIGINT to allow the process to exit cleanly, then escalates to SIGKILL after a short delay if the process remains alive. This approach respects application cleanup hooks while preventing zombie processes.
// src/terminal-manager.ts – termination
forceTerminate(pid) // lines 117-131
RPC Layer Integration
The TerminalManager API is exposed to the frontend through src/handlers/terminal-handlers.ts, which wraps these methods in RPC endpoints such as run_process, read_process_output, and send_process_input. This abstraction allows client applications to manage long-running terminal sessions without direct process manipulation.
Summary
- PID-keyed session storage: Active sessions are indexed by process PID in
this.sessions, with completed sessions archived tothis.completedSessions(retaining only the most recent 100). - Bounded memory usage: A 50 MiB output buffer per session with automatic eviction of oldest lines and tracking of evicted content via
evictedLinesandevictedCharscounters. - Interactive state detection: Regex patterns (
/>>>\s*$|>\s*$|\$\s*$|#\s*$/) and periodic heuristics fromanalyzeProcessState()detect REPL prompts, settingisBlockedto enable user input flows. - Efficient output streaming: Snapshot-based pagination (
captureOutputSnapshot,getOutputSinceSnapshot) andreadOutputPaginateddeliver incremental updates without full buffer copies. - Process control:
sendInputToProcessandforceTerminateprovide safe interaction mechanisms and guaranteed cleanup for background tasks.
Frequently Asked Questions
How does DesktopCommanderMCP prevent memory exhaustion from verbose command output?
Each session enforces a 50 MiB limit (MAX_BUFFERED_OUTPUT_CHARS) on buffered output. When exceeded, the manager automatically evicts the oldest lines from the buffer while incrementing evictedChars and evictedLines counters. This ensures memory usage remains bounded regardless of command verbosity, while metadata preserves awareness of truncated content.
What happens to terminal sessions after the process exits?
Completed sessions move from the active sessions Map to completedSessions, which implements a fixed-size cache retaining only the 100 most recent sessions. Older completed sessions are automatically purged to prevent unbounded memory growth while maintaining reasonable history for debugging and audit purposes.
How can the UI detect when a long-running command is waiting for user input?
The manager monitors stdout/stderr for prompt patterns using the regex />>>\s*$|>\s*$|\$\s*$|#\s*$/ and periodic calls to analyzeProcessState() from src/utils/process-detection.ts. When a prompt is detected, session.isBlocked is set to true, causing executeCommand() to resolve early and signal the UI to display an input prompt.
Can I retrieve output that was generated before I started polling?
Yes. The readOutputPaginated() method supports negative offsets (e.g., -100) to fetch the last N lines of output. Additionally, captureOutputSnapshot() establishes a baseline of character and line counts, allowing getOutputSinceSnapshot() to retrieve only new content while accounting for any lines evicted between polls.
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 →