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 TerminalManager to persist session state across HTTP requests
  • Each command runs in a dedicated TerminalSession stored in a PID-keyed Map in src/terminal-manager.ts
  • Circular line buffering with automatic eviction prevents memory leaks during long executions
  • readOutputPaginated supports 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →