How Desktop Commander Manages Sessions for Long-Running Terminal Commands Across Requests
Desktop Commander maintains long-running terminal sessions by spawning child processes in a singleton TerminalManager that stores session state in memory-mapped objects, enabling clients to poll for incremental output across discrete HTTP requests.
Desktop Commander, an MCP (Model Context Protocol) server by wonderwhy-er, solves the challenge of persistent terminal execution through a sophisticated session management system. The implementation in src/terminal-manager.ts creates isolated TerminalSession objects for each process, allowing commands to outlive individual request cycles. This architecture ensures that long-running tasks like log monitoring or build processes remain accessible even after the initial connection closes.
Session Lifecycle and Process Management
Creating a New Terminal Session
When a client invokes executeCommand, the TerminalManager instantiates a fresh TerminalSession and registers it in the private sessions Map (lines 69-78 in src/terminal-manager.ts). Each session receives a unique PID that serves as the lookup key for future interactions.
// Start a long-running command and obtain the session identifier
const result = await terminalManager.executeCommand('tail -f /var/log/syslog');
const pid = result.pid; // e.g., 12345
Singleton Persistence Across Requests
The manager exports a singleton instance (export const terminalManager = new TerminalManager(); at line 753) that persists for the server's lifetime. Because the sessions Map (declared at lines 48-49 as private sessions: Map<number, TerminalSession>) lives in this singleton, session data survives across separate HTTP requests, enabling asynchronous polling patterns.
Buffered Output Handling
Line-Based Buffer with Automatic Eviction
To prevent memory exhaustion during extended executions, Desktop Commander implements a capped line buffer. The appendToLineBuffer method enforces MAX_BUFFERED_OUTPUT_CHARS (defined at line 56), automatically evicting the oldest lines when the threshold is exceeded (lines 97-103). This circular buffer approach ensures that commands running for hours or days do not crash the server.
Reading Output with Pagination and Offsets
Clients retrieve output through readOutputPaginated(pid, offset, length) (lines 514-530). This method supports three read modes:
- Zero offset: Returns new output since the last read using the internal
lastReadIndex - Positive offset: Reads from an absolute line number
- Negative offset: Performs a "tail" read from the buffer end
The response includes evictedLines, allowing clients to adjust their line number calculations when content has been dropped from the buffer.
// Later, in a different request, read only new output
const newOutput = terminalManager.readOutputPaginated(pid, 0, 200);
console.log(newOutput.lines.join('\n'));
console.log(`Lines evicted since last read: ${newOutput.evictedLines}`);
REPL-Style Interactions with Snapshots
Capturing Output Snapshots
For interactive REPL workflows, Desktop Commander provides captureOutputSnapshot(pid) (lines 558-569). This records the current character count and line count into a snapshot object before sending new input to the process.
Retrieving Delta Output
After injecting input via sendInputToProcess, clients call getOutputSinceSnapshot(pid, snapshot) (lines 578-595) to receive only the output generated after the snapshot point. This eliminates the need to re-scan or re-transfer the entire buffer, optimizing network efficiency for chat-style interactions with Python, Node.js, or database shells.
// Take a snapshot before sending REPL input
const snap = terminalManager.captureOutputSnapshot(pid);
terminalManager.sendInputToProcess(pid, 'status');
// After a short wait, get only the output generated since the snapshot
const delta = terminalManager.getOutputSinceSnapshot(pid, snap);
console.log('New REPL output:', delta);
Session Cleanup and History
Active vs. Completed Sessions
When a child process exits, its session transitions from the active sessions Map to the completedSessions Map (lines 22-34). The system maintains a rolling history of the last 100 completed sessions (lines 34-38), allowing clients to retrieve final output and exit codes even after process termination.
Accessing Historical Results
The API exposes listActiveSessions() and listCompletedSessions() (lines 390-446) to enumerate available sessions. These methods return metadata including pid, isBlocked status, and runtime duration, enabling clients to implement intelligent polling strategies and resource cleanup.
// When the process finishes, fetch the completed buffer
const completed = terminalManager.readOutputPaginated(pid, 0, 1000);
if (completed.isComplete) {
console.log('Process exited with code', completed.exitCode);
}
Summary
- Desktop Commander uses a singleton
TerminalManagerto persist session state across HTTP requests - Each command runs in a dedicated
TerminalSessionstored in a PID-keyed Map insrc/terminal-manager.ts - Circular line buffering with automatic eviction prevents memory leaks during long executions
readOutputPaginatedsupports incremental, absolute, and tail-based reading with eviction tracking- Snapshot APIs enable efficient REPL interactions by isolating new output since the last interaction
- Completed session history retains the last 100 finished processes for post-mortem analysis
Frequently Asked Questions
How does Desktop Commander keep terminal sessions alive between HTTP requests?
The server maintains a singleton TerminalManager instance that stores active sessions in a JavaScript Map indexed by process ID. Because this object persists in the Node.js runtime for the server's lifetime, the underlying child processes continue executing even after the initial HTTP response completes, allowing subsequent requests to query the same PID through the sessions Map (lines 48-49).
What happens when the output buffer reaches its size limit?
When the buffer exceeds MAX_BUFFERED_OUTPUT_CHARS (line 56), the appendToLineBuffer method automatically removes the oldest lines (lines 97-103). The readOutputPaginated response includes an evictedLines count so clients can detect when content has been dropped and adjust their line offset calculations accordingly, ensuring the system remains stable during multi-day executions.
Can I interact with a REPL process incrementally?
Yes. Use captureOutputSnapshot(pid) to mark the current buffer position, then send input via sendInputToProcess. After a brief delay, call getOutputSinceSnapshot(pid, snapshot) (lines 578-595) to retrieve only the output generated by that specific input, making it ideal for interactive shells without retransmitting the entire session history.
How long are completed session results available?
Desktop Commander retains completed sessions in a separate Map with a hard limit of 100 entries (lines 34-38). Once this limit is reached or the server restarts, the historical data is purged, so clients should retrieve final results promptly after detecting process completion via listCompletedSessions() or readOutputPaginated.
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 →