diff --git a/openspec/changes/persistent-terminal-sessions/.openspec.yaml b/openspec/changes/persistent-terminal-sessions/.openspec.yaml new file mode 100644 index 0000000..6894814 --- /dev/null +++ b/openspec/changes/persistent-terminal-sessions/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-24 diff --git a/openspec/changes/persistent-terminal-sessions/design.md b/openspec/changes/persistent-terminal-sessions/design.md new file mode 100644 index 0000000..85c0e51 --- /dev/null +++ b/openspec/changes/persistent-terminal-sessions/design.md @@ -0,0 +1,73 @@ +## Context + +Currently, terminal sessions are ephemeral. Each WebSocket connection to `/ws/tool-instances/{instance_id}/terminal` spawns a new `docker exec` process via `TerminalManager.create_session()`. When the WebSocket disconnects, the session is cleaned up and the `docker exec` process is killed. This means users lose their shell state, running processes, and command history every time they disconnect. + +The current architecture: +- `TerminalManager` tracks sessions by `session_id` (UUID) in a dictionary +- Each session creates a new PTY and `docker exec` process +- WebSocket I/O loops are tied to the session lifecycle +- No concept of reconnection or session persistence + +## Goals / Non-Goals + +**Goals:** +- Terminal sessions persist across WebSocket disconnections/reconnections +- Users reconnect to the same shell process, maintaining state and history +- Add explicit "Reset Terminal" functionality to kill and restart the session +- Graceful handling of idle timeouts to clean up abandoned sessions +- Buffer recent output for replay on reconnect + +**Non-Goals:** +- Multi-user shared terminal sessions (one session per instance, but only one active WebSocket at a time) +- Session persistence across container restarts or instance stops +- Full terminal scrollback history persistence (only recent buffer) +- Automatic session restoration after instance restart + +## Decisions + +**1. Session tracking by instance_id** +- Rationale: One persistent session per tool instance is the simplest model +- Alternative: Track by user_id + instance_id — rejected because it's overkill; users don't need multiple terminals to the same container +- Trade-off: Only one user can have an active terminal at a time per instance + +**2. Detach WebSocket from session on disconnect** +- Rationale: Keep the `docker exec` process alive, just remove the WebSocket reference +- Implementation: Session stores a list of WebSocket connections (initially just one) +- On disconnect: remove WebSocket from session, don't kill process +- On reconnect: attach new WebSocket to existing session + +**3. Circular buffer for output replay** +- Rationale: Users should see what happened while disconnected +- Size: 10KB buffer (configurable) — enough for ~100 lines of typical output +- Implementation: Buffer stores raw bytes, replayed on WebSocket attach + +**4. Reset terminal via WebSocket message** +- Rationale: Users need a way to kill a stuck or corrupted session +- Implementation: JSON control message `{"type": "reset"}` kills process and starts fresh +- Alternative: HTTP endpoint — rejected because it's more complex and less intuitive + +**5. Idle timeout cleanup** +- Rationale: Prevent resource leaks from abandoned sessions +- Timeout: 30 minutes of no WebSocket connections +- Implementation: Background task checks last_activity timestamp + +## Risks / Trade-offs + +- **[Risk] Zombie sessions**: Users disconnect and never reconnect, leaving `docker exec` processes running + - Mitigation: Idle timeout of 30 minutes cleans up abandoned sessions +- **[Risk] Session corruption**: If the shell process crashes, the session is dead but still tracked + - Mitigation: Health check on `docker exec` process; auto-reset on next connect if dead +- **[Risk] Concurrent connections**: Multiple tabs trying to connect to the same instance + - Mitigation: Only allow one active WebSocket per session; new connection closes old one with a message + +## Migration Plan + +1. Deploy updated backend services (TerminalManager, TerminalSession, terminal endpoint) +2. Deploy frontend changes (reset button, reconnection handling) +3. No database migration needed +4. Rollback: Revert to previous code; existing sessions will be killed on disconnect as before + +## Open Questions + +- Should we show a "session resumed" indicator in the UI? +- Should we persist the last N commands for command history? diff --git a/openspec/changes/persistent-terminal-sessions/proposal.md b/openspec/changes/persistent-terminal-sessions/proposal.md new file mode 100644 index 0000000..55f7356 --- /dev/null +++ b/openspec/changes/persistent-terminal-sessions/proposal.md @@ -0,0 +1,29 @@ +## Why + +Currently, each WebSocket connection to a terminal spawns a new `docker exec` process. When the user disconnects (e.g., closes the browser tab, navigates away, or loses network), their shell session is killed and all state is lost. This is frustrating for users who expect their terminal session to persist like a traditional SSH session. We need persistent terminal sessions that survive reconnections. + +## What Changes + +- **Backend**: Refactor `TerminalManager` to track sessions by `instance_id` instead of generating a new session per WebSocket connection +- **Backend**: Support reattaching to an existing `docker exec` process when a WebSocket reconnects +- **Backend**: Add session reset functionality (kill existing shell and start fresh) +- **Backend**: Add idle timeout-based cleanup for abandoned sessions +- **Frontend**: Add "Reset Terminal" button in the UI +- **Frontend**: Handle reconnection gracefully with buffer replay of recent output + +## Capabilities + +### New Capabilities +- `persistent-terminal`: Terminal sessions persist across WebSocket reconnections, maintaining shell state and history + +### Modified Capabilities +- `terminal-session-management`: Existing terminal session creation and lifecycle behavior changes to support reconnection instead of always creating new sessions + +## Impact + +- `apps/api/src/services/terminal_manager.py`: Major refactoring to support instance-keyed sessions +- `apps/api/src/services/terminal_session.py`: Support multiple/detached WebSocket connections +- `apps/api/src/api/terminal.py`: Attach to existing session logic +- `apps/web/src/components/terminal.tsx` or related: Add reset button, handle reconnection +- `apps/web/src/hooks/use-terminal.ts` or related: Buffer replay on reconnect + diff --git a/openspec/changes/persistent-terminal-sessions/specs/persistent-terminal/spec.md b/openspec/changes/persistent-terminal-sessions/specs/persistent-terminal/spec.md new file mode 100644 index 0000000..d4d42b4 --- /dev/null +++ b/openspec/changes/persistent-terminal-sessions/specs/persistent-terminal/spec.md @@ -0,0 +1,70 @@ +## ADDED Requirements + +### Requirement: Terminal sessions persist across reconnections +The system SHALL maintain a terminal session for a tool instance even when the WebSocket connection is closed. When a new WebSocket connection is established for the same instance, the system SHALL reattach to the existing terminal session instead of creating a new one. + +#### Scenario: Reconnect to existing session +- **WHEN** a user disconnects from a terminal session +- **THEN** the underlying docker exec process continues running +- **AND** when the user reconnects to the same instance +- **THEN** they are attached to the same shell process + +#### Scenario: New connection creates session +- **WHEN** a user connects to an instance with no existing terminal session +- **THEN** a new terminal session is created + +## ADDED Requirements + +### Requirement: Terminal session output buffer +The system SHALL maintain a circular buffer of recent terminal output (minimum 10KB) for each persistent session. When a WebSocket reconnects, the system SHALL replay the buffered output to bring the client up to date. + +#### Scenario: Output replay on reconnect +- **WHEN** a user reconnects to an existing terminal session +- **THEN** the recent output buffer is sent to the WebSocket +- **AND** the user sees the terminal state as it was before disconnect + +## ADDED Requirements + +### Requirement: Terminal session reset +The system SHALL support resetting a terminal session. When a reset is requested, the system SHALL kill the existing docker exec process, clean up the session, and create a new one. + +#### Scenario: Reset terminal session +- **WHEN** a user sends a reset command via WebSocket +- **THEN** the existing terminal session is terminated +- **AND** a new terminal session is created +- **AND** the user is connected to the fresh session + +#### Scenario: Reset from API +- **WHEN** a user sends a POST request to reset a terminal session +- **THEN** the existing terminal session is terminated +- **AND** a new terminal session is created + +## ADDED Requirements + +### Requirement: Terminal session idle timeout +The system SHALL automatically clean up terminal sessions that have had no active WebSocket connections for 30 minutes. This prevents resource leaks from abandoned sessions. + +#### Scenario: Idle session cleanup +- **WHEN** a terminal session has no WebSocket connections for 30 minutes +- **THEN** the session is terminated and cleaned up + +#### Scenario: Active session not cleaned up +- **WHEN** a terminal session has an active WebSocket connection +- **THEN** it is not cleaned up regardless of duration + +## MODIFIED Requirements + +### Requirement: Terminal session creation +The system SHALL create a terminal session when a WebSocket connects to a tool instance. The session SHALL be associated with the instance and SHALL persist until explicitly reset, the instance stops, or an idle timeout occurs. + +#### Scenario: Create persistent session +- **WHEN** a user connects to a running instance via WebSocket +- **THEN** if no session exists for that instance, a new session is created +- **AND** if a session already exists, the WebSocket is attached to it +- **AND** recent output is replayed + +## REMOVED Requirements + +### Requirement: Terminal session cleanup on disconnect +**Reason**: Sessions now persist across disconnections +**Migration**: Sessions are cleaned up on idle timeout or explicit reset instead diff --git a/openspec/changes/persistent-terminal-sessions/tasks.md b/openspec/changes/persistent-terminal-sessions/tasks.md new file mode 100644 index 0000000..4449a30 --- /dev/null +++ b/openspec/changes/persistent-terminal-sessions/tasks.md @@ -0,0 +1,64 @@ +## 1. Backend - TerminalSession Refactoring + +- [ ] 1.1 Add circular output buffer to TerminalSession (10KB, stores raw bytes) +- [ ] 1.2 Add WebSocket connection tracking (support multiple connections, detach without closing) +- [ ] 1.3 Add last_activity timestamp and idle timeout support +- [ ] 1.4 Add reset() method to kill process and prepare for restart +- [ ] 1.5 Add health check for docker exec process +- [ ] 1.6 Modify read_output to also write to circular buffer + +## 2. Backend - TerminalManager Refactoring + +- [ ] 2.1 Change session tracking from session_id to instance_id +- [ ] 2.2 Add get_or_create_session() method (reattach if exists, create if not) +- [ ] 2.3 Modify create_session to support reconnection (don't always create new) +- [ ] 2.4 Add reset_session() method (kill existing, create new) +- [ ] 2.5 Add attach_websocket() method (add WebSocket to existing session, replay buffer) +- [ ] 2.6 Add detach_websocket() method (remove WebSocket, keep session alive) +- [ ] 2.7 Add idle timeout background task (check every minute, cleanup after 30min) +- [ ] 2.8 Handle concurrent connections (close old WebSocket when new one connects) + +## 3. Backend - Terminal WebSocket Endpoint + +- [ ] 3.1 Modify endpoint to check for existing session first +- [ ] 3.2 Add reconnection logic (attach to existing vs create new) +- [ ] 3.3 Handle reset command from WebSocket (JSON message type: "reset") +- [ ] 3.4 Add buffer replay on WebSocket attach +- [ ] 3.5 Add proper cleanup on WebSocket disconnect (detach, don't kill) + +## 4. Backend - API Reset Endpoint + +- [ ] 4.1 Add POST /api/projects/{project_id}/repositories/{repo_id}/instances/{instance_id}/terminal/reset endpoint +- [ ] 4.2 Add authorization checks +- [ ] 4.3 Call TerminalManager.reset_session() +- [ ] 4.4 Return success/error response + +## 5. Frontend - Terminal Component + +- [ ] 5.1 Add "Reset Terminal" button to terminal UI +- [ ] 5.2 Handle WebSocket reconnection gracefully +- [ ] 5.3 Display "Reconnecting..." indicator +- [ ] 5.4 Handle reset confirmation dialog +- [ ] 5.5 Display session status (connected, reconnecting, reset) + +## 6. Frontend - Terminal Hook + +- [ ] 6.1 Add reconnection logic with exponential backoff +- [ ] 6.2 Handle buffer replay on reconnect (process incoming bytes) +- [ ] 6.3 Add reset function (send WebSocket message or call API) +- [ ] 6.4 Add heartbeat/ping to detect disconnections faster + +## 7. Testing + +- [ ] 7.1 Test terminal session persistence across reconnections +- [ ] 7.2 Test output buffer replay +- [ ] 7.3 Test reset functionality +- [ ] 7.4 Test idle timeout cleanup +- [ ] 7.5 Test concurrent connection handling +- [ ] 7.6 Verify existing functionality still works (create, stop, delete instances) + +## 8. Documentation + +- [ ] 8.1 Update API documentation with new reset endpoint +- [ ] 8.2 Update user documentation about persistent terminals +- [ ] 8.3 Add troubleshooting guide for terminal issues