Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions apps/desktop/docs/PTY_SUPERVISOR_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# PTY Supervisor Implementation Plan

## Problem

When the app updates, the terminal daemon restarts, killing all PTYs and shell processes. Users lose running commands (e.g., `npm install`, servers).

## Solution

Split the daemon into two processes:

- **Supervisor**: Owns PTYs and sessions, rarely updated
- **Daemon**: Protocol bridge to app, frequently updated

```
CURRENT:
App → Daemon → pty-subprocess → PTY
(dies on update, kills PTYs)

NEW:
App → Daemon → Supervisor → pty-subprocess → PTY
↓ ↓
(can restart) (stays alive, PTYs survive)
```
Comment on lines +14 to +24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add fenced code block languages to satisfy markdownlint (MD040).
This is flagged by static analysis; add text (or a more specific language if applicable) to each fenced block.

Suggested fix
-```
+```text
 CURRENT:
 App → Daemon → pty-subprocess → PTY
        ↓
    (dies on update, kills PTYs)

 NEW:
 App → Daemon → Supervisor → pty-subprocess → PTY
        ↓            ↓
    (can restart)  (stays alive, PTYs survive)
-```
+```

-```
+```text
 ┌─────────────────────────────────────────────────────────────┐
 │ Electron App                                                │
 │   └── TerminalHostClient                                    │
 └──────────────┬──────────────────────────────────────────────┘
                │ Unix socket: ~/.superset/terminal-host.sock
 ┌──────────────▼──────────────────────────────────────────────┐
 │ Terminal Host Daemon         ← Can restart on app update    │
 │   - Client authentication                                   │
 │   - Protocol versioning                                     │
 │   - Event routing                                           │
 │   └── SupervisorClient                                      │
 └──────────────┬──────────────────────────────────────────────┘
                │ Unix socket: ~/.superset/pty-supervisor.sock
 ┌──────────────▼──────────────────────────────────────────────┐
 │ PTY Supervisor               ← Rarely restarts              │
 │   - Session lifecycle                                       │
 │   - HeadlessEmulator state                                  │
 │   - pty-subprocess management                               │
 │   └── Session                                               │
 │         └── pty-subprocess                                  │
 │               └── node-pty PTY                              │
 └─────────────────────────────────────────────────────────────┘
-```
+```

-```
+```text
 apps/desktop/src/main/pty-supervisor/
 ├── index.ts          # Entry point, socket server
 ├── supervisor.ts     # Session management (from terminal-host.ts)
 ├── session.ts        # Session class (from terminal-host/session.ts)
 └── types.ts          # Supervisor IPC protocol
-```
+```

-```
+```text
 apps/desktop/src/main/terminal-host/
 ├── index.ts              # Simplified - proxy only
 └── supervisor-client.ts  # NEW - connection to supervisor
-```
+```

-```
+```text
 1. App v2 launches, detects daemon v1 running

 2. App sends "shutdown" to daemon v1
    ┌────────┐     ┌──────────┐     ┌────────────┐
    │ App v2 │────▶│ Daemon v1│     │ Supervisor │
    └────────┘     └────┬─────┘     └────────────┘
                        │ shutdown        │
                        ▼                 │ (stays alive)
                       💀                 │

 3. App spawns daemon v2, connects to existing supervisor
    ┌────────┐     ┌──────────┐     ┌────────────┐
    │ App v2 │────▶│ Daemon v2│────▶│ Supervisor │
    └────────┘     └──────────┘     └────────────┘

 4. Daemon v2 sends "hello", gets session list

 5. Daemon v2 attaches to sessions, gets snapshots

 6. App resubscribes - terminals continue seamlessly
    (Running shells never noticed anything)
-```
+```

Also applies to: 28-51, 66-72, 85-89, 154-176

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

14-14: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@apps/desktop/docs/PTY_SUPERVISOR_PLAN.md` around lines 14 - 24, The fenced
code blocks in PTY_SUPERVISOR_PLAN.md are missing language identifiers (tripping
MD040); update each triple-backtick fence that contains the ASCII diagrams and
listings (e.g., the blocks starting with "CURRENT:/NEW:" diagram, the large
Terminal/Daemon/PTY diagram, the apps/desktop/src/main/pty-supervisor/ tree, the
apps/desktop/src/main/terminal-host/ tree, and the numbered upgrade flow block)
by adding "text" (or another appropriate language) immediately after the opening
``` so each fenced block becomes ```text; ensure every fenced block in the
ranges flagged (lines with the diagrams and trees) is updated consistently.


## Architecture

```
┌─────────────────────────────────────────────────────────────┐
│ Electron App │
│ └── TerminalHostClient │
└──────────────┬──────────────────────────────────────────────┘
│ Unix socket: ~/.superset/terminal-host.sock
┌──────────────▼──────────────────────────────────────────────┐
│ Terminal Host Daemon ← Can restart on app update │
│ - Client authentication │
│ - Protocol versioning │
│ - Event routing │
│ └── SupervisorClient │
└──────────────┬──────────────────────────────────────────────┘
│ Unix socket: ~/.superset/pty-supervisor.sock
Comment on lines +33 to +41
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Call out Windows transport explicitly.
The socket paths are Unix‑specific; please add a note about Windows named pipes (and any path convention) so the plan is unambiguous across platforms.

🤖 Prompt for AI Agents
In `@apps/desktop/docs/PTY_SUPERVISOR_PLAN.md` around lines 33 - 41, The document
currently lists Unix socket paths (~/.superset/terminal-host.sock and
~/.superset/pty-supervisor.sock) for the Terminal Host Daemon / SupervisorClient
but doesn’t mention Windows; update the PTY_SUPERVISOR_PLAN to explicitly call
out Windows transport by adding a note that on Windows the daemon and
SupervisorClient will use named pipes (e.g., \\.\pipe\superset\terminal-host and
\\.\pipe\superset\pty-supervisor or a documented app-specific pipe namespace),
include any path/namespace conventions and permission/authentication
considerations, and mention fallback behavior or detection logic so the
implementation (Terminal Host Daemon and SupervisorClient) can choose the
appropriate transport per-OS.

┌──────────────▼──────────────────────────────────────────────┐
│ PTY Supervisor ← Rarely restarts │
│ - Session lifecycle │
│ - HeadlessEmulator state │
│ - pty-subprocess management │
│ └── Session │
│ └── pty-subprocess │
│ └── node-pty PTY │
Comment on lines +33 to +49
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Define explicit IPC access-control requirements between daemon and supervisor.
Right now the plan mentions “client authentication” but doesn’t spell out how the supervisor socket is protected. This is a security risk (any local process could attach to or write into PTYs if permissions are lax). Please add explicit requirements: socket file permissions/owner/umask, credential verification (uid/pid), and/or a shared token or signed handshake.

Also applies to: 117-150

🤖 Prompt for AI Agents
In `@apps/desktop/docs/PTY_SUPERVISOR_PLAN.md` around lines 33 - 49, Add explicit
IPC access-control requirements to the plan: specify socket file ownership and
restrictive permissions/umask for the supervisor socket
(~/.superset/pty-supervisor.sock) and terminal-host socket
(~/.superset/terminal-host.sock), require credential verification (validate peer
UID/PID) on accept, and define an optional shared secret or signed handshake
exchanged by SupervisorClient <-> Terminal Host Daemon before any session
operations; document where checks occur (PTY Supervisor, Session,
HeadlessEmulator, pty-subprocess lifecycle and node-pty PTY creation) and
failure-handling policy (reject/reset connection and log security events).

└─────────────────────────────────────────────────────────────┘
```

## Responsibility Split

| Component | Owns | Update Frequency |
|-----------|------|------------------|
| **Daemon** | Client auth, protocol, routing | Every app release |
| **Supervisor** | Sessions, PTYs, emulator state | Rarely (bugs only) |
| **pty-subprocess** | Single PTY instance | Never |

## Implementation Phases

### Phase 1: Create Supervisor Process

**New files:**
```
apps/desktop/src/main/pty-supervisor/
├── index.ts # Entry point, socket server
├── supervisor.ts # Session management (from terminal-host.ts)
├── session.ts # Session class (from terminal-host/session.ts)
└── types.ts # Supervisor IPC protocol
```

**Tasks:**
- [ ] Define supervisor IPC protocol types
- [ ] Create supervisor socket server on `~/.superset/pty-supervisor.sock`
- [ ] Move `Session` class to supervisor
- [ ] Move `TerminalHost` class to supervisor
- [ ] Supervisor spawns pty-subprocesses (existing logic)
- [ ] Supervisor owns HeadlessEmulator state

### Phase 2: Update Daemon as Proxy

**Modified files:**
```
apps/desktop/src/main/terminal-host/
├── index.ts # Simplified - proxy only
└── supervisor-client.ts # NEW - connection to supervisor
```

**Tasks:**
- [ ] Create `SupervisorClient` class in daemon
- [ ] Daemon spawns supervisor if not running
- [ ] Remove session state from daemon (stateless proxy)
- [ ] Forward all session operations to supervisor
- [ ] Forward events from supervisor to app clients

### Phase 3: Handle Daemon Restart

**Tasks:**
- [ ] Supervisor detects daemon disconnect
- [ ] Supervisor buffers terminal output while daemon disconnected
- [ ] On daemon reconnect: supervisor sends existing session list
- [ ] Daemon re-attaches to sessions, flushes buffered output
- [ ] App clients see seamless continuation

### Phase 4: Supervisor Upgrade Path (Optional)

For the rare case when supervisor itself needs an update:

**Tasks:**
- [ ] Add `prepareUpgrade` IPC command
- [ ] Serialize all session state (scrollback, cwd, env)
- [ ] New supervisor reads serialized state on startup
- [ ] Respawn PTYs with restored scrollback (cold restore)

## Supervisor IPC Protocol

```typescript
// ~/.superset/pty-supervisor.sock

// Requests (Daemon → Supervisor)
type SupervisorRequest =
| { type: "hello"; daemonPid: number; daemonVersion: string }
| { type: "createSession"; sessionId: string; workspaceId: string; paneId: string; cwd: string; shell: string; env: Record<string, string>; cols: number; rows: number }
| { type: "attachSession"; sessionId: string }
| { type: "detachSession"; sessionId: string }
| { type: "write"; sessionId: string; data: string }
| { type: "resize"; sessionId: string; cols: number; rows: number }
| { type: "getSnapshot"; sessionId: string }
| { type: "killSession"; sessionId: string; signal?: string }
| { type: "listSessions" }

// Responses (Supervisor → Daemon)
type HelloResponse = {
supervisorPid: number;
supervisorVersion: string;
sessions: SessionInfo[]; // Existing sessions for reconnection
}

type AttachResponse = {
snapshot: TerminalSnapshot; // Current state for render
}

// Events (Supervisor → Daemon, unsolicited)
type SupervisorEvent =
| { type: "data"; sessionId: string; data: string }
| { type: "exit"; sessionId: string; exitCode: number; signal?: number }
| { type: "error"; sessionId: string; error: string; code?: string }
```

## Daemon Upgrade Flow

```
1. App v2 launches, detects daemon v1 running

2. App sends "shutdown" to daemon v1
┌────────┐ ┌──────────┐ ┌────────────┐
│ App v2 │────▶│ Daemon v1│ │ Supervisor │
└────────┘ └────┬─────┘ └────────────┘
│ shutdown │
▼ │ (stays alive)
💀 │

3. App spawns daemon v2, connects to existing supervisor
┌────────┐ ┌──────────┐ ┌────────────┐
│ App v2 │────▶│ Daemon v2│────▶│ Supervisor │
└────────┘ └──────────┘ └────────────┘

4. Daemon v2 sends "hello", gets session list

5. Daemon v2 attaches to sessions, gets snapshots

6. App resubscribes - terminals continue seamlessly
(Running shells never noticed anything)
```

## File Changes Summary

| File | Change |
|------|--------|
| `pty-supervisor/index.ts` | **New** - Supervisor entry point |
| `pty-supervisor/supervisor.ts` | **New** - Session management |
| `pty-supervisor/session.ts` | **Moved** from terminal-host/ |
| `pty-supervisor/types.ts` | **New** - IPC types |
| `terminal-host/index.ts` | **Simplified** - Proxy only |
| `terminal-host/supervisor-client.ts` | **New** - Supervisor connection |
| `terminal-host/session.ts` | **Deleted** - Moved to supervisor |
| `terminal-host/terminal-host.ts` | **Deleted** - Moved to supervisor |
| `lib/terminal-host/client.ts` | **Modified** - Spawn supervisor |

## Estimated Effort

| Category | Lines |
|----------|-------|
| New code (supervisor) | ~500 |
| Modified code (daemon proxy) | ~300 |
| Moved code (session management) | ~800 |

Mostly reorganization of existing code, minimal new logic.

## Tradeoffs

**Benefits:**
- Daemon can update with every app release
- Shell processes survive daemon restarts
- Users don't lose running commands

**Costs:**
- Additional process (supervisor)
- Additional IPC hop (minor latency)
- Supervisor updates still kill shells (but rare)

## Alternative: Enhanced Cold Restore

If supervisor complexity isn't worth it, improve cold restore instead:

1. Continuously save scrollback (already have HistoryWriter)
2. Save shell CWD and env vars
3. On daemon restart, show "Restoring terminals..."
4. Respawn shells in previous CWD with scrollback

This is the tmux-resurrect approach - shells restart but state is preserved.
Loading