mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,7 @@ src/
|
|||||||
├── screen-manager.ts # GNU screen persistence + process stats
|
├── screen-manager.ts # GNU screen persistence + process stats
|
||||||
├── respawn-controller.ts # Auto-respawn state machine
|
├── respawn-controller.ts # Auto-respawn state machine
|
||||||
├── ralph-loop.ts # Autonomous task assignment
|
├── ralph-loop.ts # Autonomous task assignment
|
||||||
|
├── task.ts # Task class implementation
|
||||||
├── task-queue.ts # Priority queue with dependencies
|
├── task-queue.ts # Priority queue with dependencies
|
||||||
├── task-tracker.ts # Background task detection from terminal output
|
├── task-tracker.ts # Background task detection from terminal output
|
||||||
├── inner-loop-tracker.ts # Detect Ralph loops and todos inside Claude sessions
|
├── inner-loop-tracker.ts # Detect Ralph loops and todos inside Claude sessions
|
||||||
@@ -77,24 +78,13 @@ src/
|
|||||||
|
|
||||||
### Key Components
|
### 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`.
|
| Component | File | Purpose |
|
||||||
|
|-----------|------|---------|
|
||||||
- **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`
|
| 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** (`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.
|
| ScreenManager | `screen-manager.ts` | GNU screen persistence, ghost discovery, 4-strategy kill |
|
||||||
|
| WebServer | `web/server.ts` | Fastify REST + SSE at `/api/events` |
|
||||||
- **WebServer** (`src/web/server.ts`): Fastify server with REST API (`/api/*`) + SSE (`/api/events`). Wires session events to SSE broadcast.
|
| InnerLoopTracker | `inner-loop-tracker.ts` | Detects `<promise>PHRASE</promise>`, todos, loop status in output |
|
||||||
|
|
||||||
- **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: `<promise>PHRASE</promise>`
|
|
||||||
- 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
|
|
||||||
|
|
||||||
## Code Patterns
|
## Code Patterns
|
||||||
|
|
||||||
@@ -137,12 +127,8 @@ pty.spawn('claude', ['--dangerously-skip-permissions'], { ... })
|
|||||||
|
|
||||||
### Idle Detection
|
### Idle Detection
|
||||||
|
|
||||||
**RespawnController** uses a hybrid approach:
|
**RespawnController**: Primary `↵ send` indicator, fallback prompt chars (`❯`, `⏵`) + 10s timeout. Working patterns: `Thinking`, `Writing`, `Running`.
|
||||||
1. Primary: Looks for `↵ send` indicator (Claude's suggestion prompt)
|
**Session**: emits `idle`/`working` events on prompt detection + 2s activity timeout.
|
||||||
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).
|
|
||||||
|
|
||||||
### Token Tracking
|
### Token Tracking
|
||||||
|
|
||||||
@@ -151,58 +137,20 @@ pty.spawn('claude', ['--dangerously-skip-permissions'], { ... })
|
|||||||
|
|
||||||
### Auto-Compact & Auto-Clear
|
### 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
|
Both wait for idle. Configure via `session.setAutoCompact()` / `session.setAutoClear()`.
|
||||||
- **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?)`.
|
|
||||||
|
|
||||||
### Inner Loop Tracking
|
### 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: `<promise>PHRASE</promise>`, checkbox todos (`- [ ]`/`- [x]`), icons (`☐`/`◐`/`✓`), cycle counts. API: `GET /api/sessions/:id/inner-state`. UI: collapsible panel below tabs, auto-hides when empty.
|
||||||
|
|
||||||
**Completion Phrases:**
|
### Terminal Display Fix
|
||||||
```
|
|
||||||
<promise>COMPLETE</promise>
|
|
||||||
<promise>TIME_COMPLETE</promise>
|
|
||||||
<promise>CUSTOM_PHRASE</promise>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Todo Items (multiple formats):**
|
Tab switch/new session fix: clear xterm → write buffer → resize PTY → Ctrl+L redraw. Uses `pendingCtrlL` Set, triggered on `session:idle`/`session:working` events.
|
||||||
```
|
|
||||||
- [ ] 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.
|
|
||||||
|
|
||||||
### SSE Events
|
### SSE Events
|
||||||
|
|
||||||
@@ -212,14 +160,11 @@ Event prefixes: `session:`, `task:`, `respawn:`, `scheduled:`, `case:`, `screen:
|
|||||||
|
|
||||||
### Frontend (app.js)
|
### Frontend (app.js)
|
||||||
|
|
||||||
The frontend uses vanilla JS with xterm.js. Key patterns:
|
Vanilla JS + xterm.js. `handleSSEEvent()` dispatches events, `switchToSession()` manages tabs. 60fps: server batches 16ms, client uses `requestAnimationFrame`.
|
||||||
- **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`
|
|
||||||
|
|
||||||
### 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
|
## Adding New Features
|
||||||
|
|
||||||
@@ -240,27 +185,10 @@ State writes to `~/.claudeman/state.json` are debounced (100ms) to prevent exces
|
|||||||
|
|
||||||
## Session Lifecycle & Cleanup
|
## Session Lifecycle & Cleanup
|
||||||
|
|
||||||
### Session Limits
|
- **Limit**: `MAX_CONCURRENT_SESSIONS = 50`
|
||||||
- `MAX_CONCURRENT_SESSIONS = 50` prevents unbounded session creation
|
- **Kill** (`killScreen()`): child PIDs → process group → screen quit → SIGKILL
|
||||||
- Limit enforced on `/api/sessions`, `/api/run`, `/api/quick-start`, and scheduled runs
|
- **Ghost discovery**: `reconcileScreens()` finds orphaned screens on startup
|
||||||
|
- **Cleanup** (`cleanupSession()`): stops respawn, clears buffers/timers, kills screen
|
||||||
### 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 <name> -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
|
|
||||||
|
|
||||||
## Buffer Limits
|
## Buffer Limits
|
||||||
|
|
||||||
@@ -274,23 +202,14 @@ Long-running sessions are supported with automatic trimming:
|
|||||||
| Line buffer | 64KB | (flushed every 100ms) |
|
| Line buffer | 64KB | (flushed every 100ms) |
|
||||||
| Respawn buffer | 1MB | 512KB |
|
| 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
|
```bash
|
||||||
# Quick test sequence
|
npx agent-browser open http://localhost:3000 && npx agent-browser snapshot
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Full test plan available at `.claude/skills/e2e-test.md`.
|
|
||||||
|
|
||||||
## API Routes Quick Reference
|
## API Routes Quick Reference
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|
|||||||
@@ -225,18 +225,18 @@ export class RespawnController extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect ready state (↵ send indicator) - immediate response
|
// Detect ready state (↵ send indicator)
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
this.promptDetected = true;
|
this.promptDetected = true;
|
||||||
this.workingDetected = false;
|
this.workingDetected = false;
|
||||||
this.lastActivityTime = Date.now();
|
this.lastActivityTime = Date.now();
|
||||||
this.log('Ready indicator detected (↵ send)');
|
this.log('Ready indicator detected (↵ send)');
|
||||||
|
|
||||||
// Handle based on current state - immediate action
|
// Handle based on current state
|
||||||
switch (this._state) {
|
switch (this._state) {
|
||||||
case 'watching':
|
case 'watching':
|
||||||
this.clearIdleTimer();
|
// Start idle timer instead of immediate action - gives user time to type
|
||||||
this.onIdleDetected();
|
this.startIdleTimer();
|
||||||
break;
|
break;
|
||||||
case 'waiting_update':
|
case 'waiting_update':
|
||||||
this.checkUpdateComplete();
|
this.checkUpdateComplete();
|
||||||
|
|||||||
@@ -1274,7 +1274,6 @@ class ClaudemanApp {
|
|||||||
this.editingSessionId = sessionId;
|
this.editingSessionId = sessionId;
|
||||||
|
|
||||||
document.getElementById('sessionNameInput').value = session.name || '';
|
document.getElementById('sessionNameInput').value = session.name || '';
|
||||||
document.getElementById('sessionDirDisplay').textContent = session.workingDir || 'Unknown';
|
|
||||||
|
|
||||||
// Update respawn status display and buttons
|
// Update respawn status display and buttons
|
||||||
const respawnStatus = document.getElementById('sessionRespawnStatus');
|
const respawnStatus = document.getElementById('sessionRespawnStatus');
|
||||||
|
|||||||
@@ -193,10 +193,6 @@
|
|||||||
<label>Session Name</label>
|
<label>Session Name</label>
|
||||||
<input type="text" id="sessionNameInput" placeholder="Enter session name...">
|
<input type="text" id="sessionNameInput" placeholder="Enter session name...">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
|
||||||
<label>Directory</label>
|
|
||||||
<div class="session-dir-display" id="sessionDirDisplay">/path/to/dir</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Respawn Settings Section -->
|
<!-- Respawn Settings Section -->
|
||||||
<div class="session-respawn-section" id="sessionRespawnSection">
|
<div class="session-respawn-section" id="sessionRespawnSection">
|
||||||
|
|||||||
Reference in New Issue
Block a user