Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
39d4b72
feat: add IdleWatchdog with state machine, stuck stream protection, e…
jrenaldi79 Mar 14, 2026
706ab80
feat: crash handler deletes session lock file on uncaught exception
jrenaldi79 Mar 14, 2026
d83b974
fix: update start.test.js to expect pid in metadata
jrenaldi79 Mar 14, 2026
e84a093
feat: integrate IdleWatchdog into interactive mode (60-min timeout)
jrenaldi79 Mar 14, 2026
7fbd629
fix: preserve existing pid from MCP handler in createSessionMetadata
jrenaldi79 Mar 14, 2026
80afc25
feat: integrate IdleWatchdog with opencode-client sendPrompt
jrenaldi79 Mar 14, 2026
ca9ae1c
feat: integrate IdleWatchdog into headless mode (15-min timeout)
jrenaldi79 Mar 14, 2026
edae761
feat: add atomic session lock file mechanism
jrenaldi79 Mar 14, 2026
8297914
feat: add SharedServerManager with lazy start, session tracking, supe…
jrenaldi79 Mar 14, 2026
2f7663f
feat: replace per-process MCP spawning with shared server
jrenaldi79 Mar 14, 2026
153f2fa
feat: add session locks and dead-process detection to resume/start/co…
jrenaldi79 Mar 14, 2026
e2f94e5
docs: update CLAUDE.md with process lifecycle modules and env vars
jrenaldi79 Mar 14, 2026
b111b0a
docs: add process lifecycle documentation to user-facing docs
jrenaldi79 Mar 14, 2026
25fc8b3
test: add shared server e2e integration test with memory monitoring
jrenaldi79 Mar 14, 2026
e416388
fix: correct sendPrompt format in MCP shared server path
jrenaldi79 Mar 14, 2026
44da49f
docs: add MCP polling integration design spec
jrenaldi79 Mar 14, 2026
4699bb6
docs: address reviewer feedback on MCP polling spec
jrenaldi79 Mar 14, 2026
e75e294
docs: incorporate Gemini/GPT review feedback into MCP polling spec
jrenaldi79 Mar 14, 2026
22e2da3
docs: add MCP polling integration implementation plan
jrenaldi79 Mar 14, 2026
6f80753
feat: MCP shared server path delegates to runHeadless with context an…
jrenaldi79 Mar 14, 2026
590501e
feat: add external server guard points to runHeadless for shared serv…
jrenaldi79 Mar 14, 2026
26098b3
feat: add eval monitoring script for process/memory tracking
jrenaldi79 Mar 14, 2026
efc3159
fix: increase eval runner timeout from 5 min to 10 min
jrenaldi79 Mar 14, 2026
426e8eb
fix: resolve model alias in MCP shared server path
jrenaldi79 Mar 14, 2026
8b9e6c7
docs: add MCP input validation design spec
jrenaldi79 Mar 14, 2026
04a384c
feat: add MCP input validation with structured error responses
jrenaldi79 Mar 14, 2026
b4ea478
test: add MCP error response format and descriptive message tests
jrenaldi79 Mar 14, 2026
7ccf6fd
fix: allow Chat agent with noUi (handler auto-converts to Build)
jrenaldi79 Mar 14, 2026
1f22b18
fix: prevent orphaned sessions on shared server fallback and PID misu…
jrenaldi79 Mar 14, 2026
fb36e90
fix: troubleshooting path, eval script exit handling, e2e test hardening
jrenaldi79 Mar 14, 2026
4fd1417
fix: watchdog leak, resume lock scope, null metadata guard, model err…
jrenaldi79 Mar 14, 2026
10ddca8
fix: CodeRabbit round 2 - docs clarifications and eval script robustness
jrenaldi79 Mar 14, 2026
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
33 changes: 33 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,17 @@ src/
│ ├── api-key-validation.js # Validation endpoints per provider
│ ├── auth-json.js # Known provider IDs that map to sidecar's PROVIDER_ENV_MAP
│ ├── config.js # Default model alias map — short names to full OpenRouter model identifiers
│ ├── idle-watchdog.js # @type {Object.<string, number>} Default timeouts per mode in milliseconds
│ ├── input-validators.js # MCP input validation with structured error responses.
│ ├── logger.js # Structured Logger Module
│ ├── mcp-discovery.js # MCP Discovery - Discovers MCP servers from parent LLM configuration
│ ├── mcp-validators.js # MCP Validators
│ ├── model-fetcher.js # Hardcoded Anthropic models (no public listing endpoint)
│ ├── model-validator.js # Alias-to-search-term mapping for filtering provider model lists
│ ├── path-setup.js # Ensures that the project's node_modules/.bin directory is included in the PATH.
│ ├── server-setup.js # Server Setup Utilities
│ ├── session-lock.js # Atomic session lock files to prevent concurrent resume/continue.
│ ├── shared-server.js # Manages a single shared OpenCode server for MCP sessions.
│ ├── start-helpers.js # Start Command Helpers
│ ├── thinking-validators.js # Thinking Level Validators
│ ├── updater.js # @type {import('update-notifier').UpdateNotifier|null}
Expand Down Expand Up @@ -158,6 +162,7 @@ scripts/
├── check-secrets.js # Secret detection script for pre-commit hook.
├── check-ui.js
├── debug-cdp.js
├── eval-with-monitoring.sh
├── generate-docs-helpers.js # Helper functions for generate-docs.js.
├── generate-docs.js # @param {string} dirPath @returns {string[]} Sorted .md filenames
├── generate-icon.js # Generate app icon PNG from SVG source.
Expand Down Expand Up @@ -224,13 +229,17 @@ evals/
| `utils/api-key-validation.js` | Validation endpoints per provider | `validateApiKey()`, `validateOpenRouterKey()`, `VALIDATION_ENDPOINTS()` |
| `utils/auth-json.js` | Known provider IDs that map to sidecar's PROVIDER_ENV_MAP | `readAuthJsonKeys()`, `importFromAuthJson()`, `checkAuthJson()`, `removeFromAuthJson()`, `AUTH_JSON_PATH()` |
| `utils/config.js` | Default model alias map — short names to full OpenRouter model identifiers | `getConfigDir()`, `getConfigPath()`, `loadConfig()`, `saveConfig()`, `getDefaultAliases()` |
| `utils/idle-watchdog.js` | @type {Object.<string, number>} Default timeouts per mode in milliseconds | `IdleWatchdog()`, `resolveTimeout()` |
| `utils/input-validators.js` | MCP input validation with structured error responses. | `validateStartInputs()`, `findSimilar()` |
| `utils/logger.js` | Structured Logger Module | `logger()`, `LOG_LEVELS()` |
| `utils/mcp-discovery.js` | MCP Discovery - Discovers MCP servers from parent LLM configuration | `discoverParentMcps()`, `discoverClaudeCodeMcps()`, `discoverCoworkMcps()`, `normalizeMcpJson()` |
| `utils/mcp-validators.js` | MCP Validators | `validateMcpSpec()`, `validateMcpConfigFile()` |
| `utils/model-fetcher.js` | Hardcoded Anthropic models (no public listing endpoint) | `fetchModelsFromProvider()`, `fetchAllModels()`, `groupModelsByFamily()`, `ANTHROPIC_MODELS()`, `PROVIDER_FAMILY_NAMES()` |
| `utils/model-validator.js` | Alias-to-search-term mapping for filtering provider model lists | `validateDirectModel()`, `filterRelevantModels()`, `normalizeModelId()` |
| `utils/path-setup.js` | Ensures that the project's node_modules/.bin directory is included in the PATH. | `ensureNodeModulesBinInPath()` |
| `utils/server-setup.js` | Server Setup Utilities | `DEFAULT_PORT()`, `isPortInUse()`, `getPortPid()`, `killPortProcess()`, `ensurePortAvailable()` |
| `utils/session-lock.js` | Atomic session lock files to prevent concurrent resume/continue. | `acquireLock()`, `releaseLock()`, `isLockStale()`, `isPidAlive()` |
| `utils/shared-server.js` | Manages a single shared OpenCode server for MCP sessions. | `SharedServerManager()` |
| `utils/start-helpers.js` | Start Command Helpers | `resolveModelFromArgs()`, `validateFallbackModel()` |
| `utils/thinking-validators.js` | Thinking Level Validators | `MODEL_THINKING_SUPPORT()`, `getSupportedThinkingLevels()`, `validateThinkingLevel()` |
| `utils/updater.js` | @type {import('update-notifier').UpdateNotifier|null} | `initUpdateCheck()`, `getUpdateInfo()`, `notifyUpdate()`, `performUpdate()` |
Expand Down Expand Up @@ -350,6 +359,30 @@ The pre-commit hook runs this automatically. See [docs/doc-system.md](docs/doc-s

---

## Process Lifecycle Management

Sidecar processes self-terminate after inactivity via IdleWatchdog. MCP sessions use a shared multiplexed server instead of per-session processes.

### Environment Variables

| Env Var | Default | Description |
|---------|---------|-------------|
| `SIDECAR_IDLE_TIMEOUT` | (mode-dependent) | Blanket override for idle timeout in minutes (all modes). 0 = disabled (Infinity). |
| `SIDECAR_IDLE_TIMEOUT_HEADLESS` | 15 | Headless mode idle timeout in minutes |
| `SIDECAR_IDLE_TIMEOUT_INTERACTIVE` | 60 | Interactive mode idle timeout in minutes |
| `SIDECAR_IDLE_TIMEOUT_SERVER` | 30 | Shared server "no sessions" timeout in minutes |
| `SIDECAR_MAX_SESSIONS` | 20 | Max concurrent sessions on shared server |
| `SIDECAR_REQUEST_TIMEOUT` | 5 | Stuck-stream timeout in minutes |
| `SIDECAR_SHARED_SERVER` | 1 | Set to 0 to disable shared server (fall back to per-process) |

### Gotchas

- `SIDECAR_IDLE_TIMEOUT=0` means `Infinity` (timer never set), not zero-ms timeout
- Session lock files live at `<session_dir>/session.lock`. Delete manually if stuck with "session already active" error
- `SIDECAR_SHARED_SERVER=0` disables shared server and falls back to per-process spawning

---

## Agent Documentation

GEMINI.md and AGENTS.md are symlinks to CLAUDE.md -- no sync needed.
Expand Down
26 changes: 26 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,32 @@ When the user clicks **Fold** (or presses `Cmd+Shift+F`) in interactive mode:

In headless mode, the agent outputs `[SIDECAR_FOLD]` autonomously when done, and `headless.js` extracts everything before the marker.

## Shared Server Architecture

Multiple sidecar invocations share a single OpenCode Go binary when `SIDECAR_SHARED_SERVER=1` (the default). This eliminates per-invocation cold-start latency and reduces memory overhead.

```
Before (per-process): After (shared server):
MCP Server MCP Server
+-- sidecar CLI (port 4096) +-- Shared OpenCode Server (port 4096)
| +-- OpenCode Go binary +-- Session A
+-- sidecar CLI (port 4097) +-- Session B
| +-- OpenCode Go binary +-- Session C
+-- sidecar CLI (port 4098)
+-- OpenCode Go binary
```

The shared server restarts automatically on crash, up to 3 times within any 5-minute window. After 3 restarts the server is considered unstable and will not restart again; use `SIDECAR_SHARED_SERVER=0` to fall back to per-process mode.

## IdleWatchdog State Machine

Each sidecar process runs an `IdleWatchdog` that transitions between two states:

- **BUSY**: A prompt is in flight or a session was recently active. Idle timer is paused.
- **IDLE**: No active requests for the configured idle period. Process (or shared server) self-terminates.

Transitions: `BUSY → IDLE` when the last active session goes quiet; `IDLE → BUSY` on any new incoming request. The idle clock resets on each BUSY→IDLE transition. Set `SIDECAR_IDLE_TIMEOUT=0` to disable self-termination entirely.

## Electron BrowserView Architecture

The Electron shell (`electron/main.js`) uses a **BrowserView** to avoid CSS conflicts between the OpenCode SPA and the sidecar toolbar:
Expand Down
23 changes: 23 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,29 @@ SIDECAR_MOCK_UPDATE=available # Mock update UI state for testing

---

## Process Lifecycle

These environment variables control how sidecar processes self-terminate and share resources.

```bash
# Idle timeout overrides (values in minutes, 0 = disabled)
SIDECAR_IDLE_TIMEOUT=0 # Blanket override for all modes (0 = disabled)
SIDECAR_IDLE_TIMEOUT_HEADLESS=15 # Headless mode idle timeout (default: 15 min)
SIDECAR_IDLE_TIMEOUT_INTERACTIVE=60 # Interactive mode idle timeout (default: 60 min)
SIDECAR_IDLE_TIMEOUT_SERVER=30 # Shared server idle timeout (default: 30 min)

# Resource limits
SIDECAR_MAX_SESSIONS=20 # Max concurrent sessions on shared server (default: 20)
SIDECAR_REQUEST_TIMEOUT=5 # Per-request timeout in minutes (default: 5 min)

# Shared server
SIDECAR_SHARED_SERVER=1 # Use shared OpenCode server (default: 1, set 0 to disable)
```

Sidecar processes self-terminate after the configured idle period. The shared server (`SIDECAR_SHARED_SERVER=1`) allows multiple sidecar sessions to reuse a single OpenCode Go binary process rather than spawning one per invocation.

---

## Dependencies

| Package | Version | Purpose |
Expand Down
41 changes: 41 additions & 0 deletions docs/superpowers/plans/2026-03-14-mcp-polling-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# MCP Shared Server Polling Integration - Handoff

**Date:** 2026-03-14
**Branch:** feature/memory-leak
**Worktree:** /Users/john_renaldi/claude-code-projects/sidecar-memory-leak

## Problem

The shared server MCP path (`sidecar_start` handler in `src/mcp-server.js`) creates sessions on the shared OpenCode server and sends prompts via `sendPromptAsync`, but lacks the polling/finalization loop that the per-process spawn path provides.

**Result:** Sessions start, the LLM processes them, but `sidecar_status` never sees completion because:
1. No background polling writes conversation data to disk
2. No progress tracking (messages, stage, latest activity)
3. No session finalization (status never transitions from 'running' to 'complete')
4. No summary extraction or fold detection

The per-process spawn path works by spawning a CLI process that runs `runHeadless()`, which handles ALL of this internally. The shared server path bypasses `runHeadless()` entirely.

## What's Done

All 3 layers of process lifecycle management are implemented:
- Layer 1 (Shared Server): `SharedServerManager` class works, lazy start, session tracking, supervisor
- Layer 2 (IdleWatchdog): Fully integrated into headless, interactive, opencode-client
- Layer 3 (Resume/Locks): Session locks, dead-process detection, crash handler cleanup

The gap is specifically in the MCP `sidecar_start` handler's shared server path.

## Options to Explore

### Option A: Background Polling Task per Session
Run a background polling loop (like `runHeadless()` does) for each session on the shared server. Would live in the MCP handler or a new module.

### Option B: Delegate to runHeadless() with Shared Server
Modify `runHeadless()` to accept an existing client/server instead of starting its own. The MCP handler would call `runHeadless()` but pass the shared server's client.

### Option C: SDK-Level Event Streaming
If the OpenCode SDK supports event streaming or completion callbacks, use those instead of polling.

## Key Constraint

The MCP handler must return immediately after `sidecar_start` (fire-and-forget). The polling must happen in the background, writing results to disk so `sidecar_status` can read them.
Loading