From d4120966497ca7cf527e6ba735ccf834eb2079a3 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 19 Jan 2026 15:00:49 +0100 Subject: [PATCH] fix(respawn): wait for idle timeout before triggering cycle - Fixed respawn controller immediately sending commands when Claude becomes idle, which prevented users from typing. Now waits for idleTimeoutMs (default 10s) before triggering the respawn cycle. - Removed Directory field from Session Options modal for cleaner UI - Compacted CLAUDE.md documentation for better readability Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 137 ++++++++------------------------------ src/respawn-controller.ts | 8 +-- src/web/public/app.js | 1 - src/web/public/index.html | 4 -- 4 files changed, 32 insertions(+), 118 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0c4c839e..8f006b84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,7 @@ src/ ├── screen-manager.ts # GNU screen persistence + process stats ├── respawn-controller.ts # Auto-respawn state machine ├── ralph-loop.ts # Autonomous task assignment +├── task.ts # Task class implementation ├── task-queue.ts # Priority queue with dependencies ├── task-tracker.ts # Background task detection from terminal output ├── inner-loop-tracker.ts # Detect Ralph loops and todos inside Claude sessions @@ -77,24 +78,13 @@ src/ ### Key Components -- **Session** (`src/session.ts`): Wraps Claude CLI as PTY subprocess. Two modes: `runPrompt(prompt)` for one-shot, `startInteractive()` for persistent terminal. Emits events: `output`, `terminal`, `message`, `completion`, `exit`, `idle`, `working`, `autoClear`, `autoCompact`, `clearTerminal`, `innerLoopUpdate`, `innerTodoUpdate`, `innerCompletionDetected`. - -- **RespawnController** (`src/respawn-controller.ts`): State machine that keeps sessions productive. Detects idle → sends update prompt → optionally `/clear` → optionally `/init` → optionally kickstart prompt → repeats. State flow: `WATCHING → SENDING_UPDATE → WAITING_UPDATE → SENDING_CLEAR → WAITING_CLEAR → SENDING_INIT → WAITING_INIT → MONITORING_INIT → (optional) SENDING_KICKSTART → WAITING_KICKSTART → WATCHING` - -- **ScreenManager** (`src/screen-manager.ts`): Wraps sessions in GNU screen for persistence across server restarts. On startup, reconciles with `screen -ls` to restore sessions and discovers unknown "ghost" screens. Uses 4-strategy kill process to prevent orphaned claude processes. - -- **WebServer** (`src/web/server.ts`): Fastify server with REST API (`/api/*`) + SSE (`/api/events`). Wires session events to SSE broadcast. - -- **InnerLoopTracker** (`src/inner-loop-tracker.ts`): Detects Ralph Wiggum loops and todo lists running inside Claude Code sessions by parsing terminal output. Emits `loopUpdate`, `todoUpdate`, `completionDetected` events. Detection patterns: - - Completion phrases: `PHRASE` - - Todo items: checkbox format (`- [ ]`/`- [x]`), indicator icons (`☐`/`◐`/`✓`), status parentheses - - Loop status: cycle counts, elapsed time, start/completion indicators - -### Session Modes - -- **One-Shot** (`runPrompt(prompt)`): Single prompt execution, emits completion, exits -- **Interactive** (`startInteractive()`): Persistent PTY terminal with resize support -- **Shell** (`startShell()`): Plain bash/zsh terminal without Claude +| Component | File | Purpose | +|-----------|------|---------| +| Session | `session.ts` | PTY wrapper for Claude CLI. Modes: `runPrompt()`, `startInteractive()`, `startShell()` | +| RespawnController | `respawn-controller.ts` | State machine: watching → update → clear → init → monitor → kickstart → repeat | +| ScreenManager | `screen-manager.ts` | GNU screen persistence, ghost discovery, 4-strategy kill | +| WebServer | `web/server.ts` | Fastify REST + SSE at `/api/events` | +| InnerLoopTracker | `inner-loop-tracker.ts` | Detects `PHRASE`, todos, loop status in output | ## Code Patterns @@ -137,12 +127,8 @@ pty.spawn('claude', ['--dangerously-skip-permissions'], { ... }) ### Idle Detection -**RespawnController** uses a hybrid approach: -1. Primary: Looks for `↵ send` indicator (Claude's suggestion prompt) -2. Fallback: Prompt characters (`❯`, `\u276f`, `⏵`) + timeout (10s default) -3. Working detection: Patterns like `Thinking`, `Writing`, `Running`, etc. - -**Session** emits `idle` and `working` events based on prompt detection and activity timeout (2s after prompt character). +**RespawnController**: Primary `↵ send` indicator, fallback prompt chars (`❯`, `⏵`) + 10s timeout. Working patterns: `Thinking`, `Writing`, `Running`. +**Session**: emits `idle`/`working` events on prompt detection + 2s activity timeout. ### Token Tracking @@ -151,58 +137,20 @@ pty.spawn('claude', ['--dangerously-skip-permissions'], { ... }) ### Auto-Compact & Auto-Clear -Sessions support automatic context management when token thresholds are reached: +| Feature | Default Threshold | Action | +|---------|------------------|--------| +| Auto-Compact | 110k tokens | `/compact` with optional prompt | +| Auto-Clear | 140k tokens | `/clear` to reset context | -- **Auto-Compact** (default: 110k tokens): Sends `/compact` command with optional prompt to summarize context -- **Auto-Clear** (default: 140k tokens): Sends `/clear` to reset context entirely - -Both wait for Claude to be idle before executing. Auto-compact runs first if both are enabled. Configured via `session.setAutoCompact(enabled, threshold?, prompt?)` and `session.setAutoClear(enabled, threshold?)`. +Both wait for idle. Configure via `session.setAutoCompact()` / `session.setAutoClear()`. ### Inner Loop Tracking -When Claude Code runs inside a claudeman session, it may run its own Ralph Wiggum loops or use the TodoWrite tool. The **InnerLoopTracker** parses terminal output to detect: +Detects Ralph loops and todos inside Claude sessions. Patterns: `PHRASE`, checkbox todos (`- [ ]`/`- [x]`), icons (`☐`/`◐`/`✓`), cycle counts. API: `GET /api/sessions/:id/inner-state`. UI: collapsible panel below tabs, auto-hides when empty. -**Completion Phrases:** -``` -COMPLETE -TIME_COMPLETE -CUSTOM_PHRASE -``` +### Terminal Display Fix -**Todo Items (multiple formats):** -``` -- [ ] Pending task # Checkbox format -- [x] Completed task -Todo: ☐ Pending task # Indicator format -Todo: ◐ In progress task -Todo: ✓ Completed task -- Task name (in_progress) # Status parentheses -``` - -**Loop Status:** -``` -Loop started at 2024-01-15 -Elapsed: 2.5 hours -cycle #5 -``` - -**API Endpoint:** -```bash -curl localhost:3000/api/sessions/:id/inner-state -# Returns: { loop: InnerLoopState, todos: InnerTodoItem[], todoStats: {...} } -``` - -**UI:** Collapsible panel below session tabs shows loop status and todo progress. Auto-hides when no inner state is detected. - -### Terminal Display Fix (Tab Switch & New Session) - -When switching tabs or creating new sessions, terminal may be rendered at wrong size. Fix sequence: -1. Clear and reset xterm -2. Write terminal buffer -3. Send resize to update PTY dimensions -4. Send Ctrl+L (`\x0c`) to trigger Claude CLI redraw - -Uses `pendingCtrlL` Set to track sessions needing the fix. Waits for `session:idle` or `session:working` SSE event before sending resize + Ctrl+L. +Tab switch/new session fix: clear xterm → write buffer → resize PTY → Ctrl+L redraw. Uses `pendingCtrlL` Set, triggered on `session:idle`/`session:working` events. ### SSE Events @@ -212,14 +160,11 @@ Event prefixes: `session:`, `task:`, `respawn:`, `scheduled:`, `case:`, `screen: ### Frontend (app.js) -The frontend uses vanilla JS with xterm.js. Key patterns: -- **SSE handling**: `handleSSEEvent()` switch statement dispatches all event types -- **Tab management**: `switchToSession()` handles terminal buffer restore + resize -- **60fps rendering**: Server batches at 16ms intervals, client uses `requestAnimationFrame` +Vanilla JS + xterm.js. `handleSSEEvent()` dispatches events, `switchToSession()` manages tabs. 60fps: server batches 16ms, client uses `requestAnimationFrame`. -### State Store Debouncing +### State Store -State writes to `~/.claudeman/state.json` are debounced (100ms) to prevent excessive disk I/O during rapid updates. The `StateStore` class batches rapid state changes and writes once after activity settles. +Writes debounced (100ms) to `~/.claudeman/state.json`. Batches rapid changes. ## Adding New Features @@ -240,27 +185,10 @@ State writes to `~/.claudeman/state.json` are debounced (100ms) to prevent exces ## Session Lifecycle & Cleanup -### Session Limits -- `MAX_CONCURRENT_SESSIONS = 50` prevents unbounded session creation -- Limit enforced on `/api/sessions`, `/api/run`, `/api/quick-start`, and scheduled runs - -### Kill Process (4 Strategies) -When killing a screen session, `killScreen()` uses multiple strategies to ensure no orphaned processes: -1. **Child PIDs**: Recursively find and kill all child processes (SIGTERM then SIGKILL) -2. **Process Group**: Kill entire process group (`kill -TERM -$PID`) to catch orphans -3. **Screen Name**: `screen -S -X quit` to cleanly terminate screen -4. **Direct Kill**: SIGKILL the screen PID as final fallback - -### Ghost Screen Discovery -On server startup, `reconcileScreens()` discovers unknown claudeman screens from `screen -ls` output. This prevents "ghost" screens that persist if `screens.json` is lost or corrupted. - -### Cleanup Function -`cleanupSession()` in server.ts provides comprehensive cleanup: -- Stops and removes respawn controller + listeners -- Clears respawn timers -- Clears terminal/output/task batches -- Removes session event listeners -- Stops session and kills screen +- **Limit**: `MAX_CONCURRENT_SESSIONS = 50` +- **Kill** (`killScreen()`): child PIDs → process group → screen quit → SIGKILL +- **Ghost discovery**: `reconcileScreens()` finds orphaned screens on startup +- **Cleanup** (`cleanupSession()`): stops respawn, clears buffers/timers, kills screen ## Buffer Limits @@ -274,23 +202,14 @@ Long-running sessions are supported with automatic trimming: | Line buffer | 64KB | (flushed every 100ms) | | Respawn buffer | 1MB | 512KB | -## E2E Testing (agent-browser) +## E2E Testing -Browser automation for testing the web UI. See README.md for full setup. +Uses `agent-browser` for web UI automation. Full test plan: `.claude/skills/e2e-test.md` ```bash -# Quick test sequence -npx agent-browser open http://localhost:3000 -npx agent-browser wait --load networkidle -npx agent-browser snapshot -npx agent-browser find text "Run Claude" click -npx agent-browser wait 2000 -npx agent-browser snapshot -npx agent-browser close +npx agent-browser open http://localhost:3000 && npx agent-browser snapshot ``` -Full test plan available at `.claude/skills/e2e-test.md`. - ## API Routes Quick Reference | Method | Endpoint | Description | diff --git a/src/respawn-controller.ts b/src/respawn-controller.ts index 75254c5c..f0678bd1 100644 --- a/src/respawn-controller.ts +++ b/src/respawn-controller.ts @@ -225,18 +225,18 @@ export class RespawnController extends EventEmitter { return; } - // Detect ready state (↵ send indicator) - immediate response + // Detect ready state (↵ send indicator) if (isReady) { this.promptDetected = true; this.workingDetected = false; this.lastActivityTime = Date.now(); this.log('Ready indicator detected (↵ send)'); - // Handle based on current state - immediate action + // Handle based on current state switch (this._state) { case 'watching': - this.clearIdleTimer(); - this.onIdleDetected(); + // Start idle timer instead of immediate action - gives user time to type + this.startIdleTimer(); break; case 'waiting_update': this.checkUpdateComplete(); diff --git a/src/web/public/app.js b/src/web/public/app.js index 1145660b..62293fd0 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1274,7 +1274,6 @@ class ClaudemanApp { this.editingSessionId = sessionId; document.getElementById('sessionNameInput').value = session.name || ''; - document.getElementById('sessionDirDisplay').textContent = session.workingDir || 'Unknown'; // Update respawn status display and buttons const respawnStatus = document.getElementById('sessionRespawnStatus'); diff --git a/src/web/public/index.html b/src/web/public/index.html index 3fea64c4..af87c2ec 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -193,10 +193,6 @@ -
- -
/path/to/dir
-