diff --git a/CLAUDE.md b/CLAUDE.md index 73b00c05..62e00667 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -196,7 +196,7 @@ WATCHING → CONFIRMING_IDLE → SENDING_UPDATE → WAITING_UPDATE → SENDING_C Steps can be skipped via config (`sendClear: false`, `sendInit: false`). Optional `kickstartPrompt` triggers if `/init` doesn't start work. Multi-layer idle detection triggers state transitions. -**Step confirmation**: After sending each step (update, clear, init, kickstart), the controller waits for `completionConfirmMs` (5s) of output silence before proceeding to the next step. This prevents sending commands while Claude is still processing. +**Step confirmation**: After sending each step (update, clear, init, kickstart), the controller waits for `completionConfirmMs` (10s) of output silence before proceeding to the next step. This prevents sending commands while Claude is still processing. ### Spawn1337 Protocol (Autonomous Agents) @@ -386,7 +386,7 @@ curl -X POST localhost:3000/api/sessions/:id/input \ **RespawnController (Claude Code 2024+)**: Multi-layer detection with confidence scoring: 1. **Completion message**: Primary signal - detects "for Xm Xs" time patterns (e.g., "Worked for 2m 46s") -2. **Output silence**: Confirms idle after `completionConfirmMs` (5s) of no new output +2. **Output silence**: Confirms idle after `completionConfirmMs` (10s) of no new output 3. **Token stability**: Tokens haven't changed 4. **Working patterns absent**: No `Thinking`, `Writing`, spinner chars @@ -572,7 +572,7 @@ Writes debounced (500ms) to `~/.claudeman/state.json`. The web server persists f | Input batch interval | 16ms | `app.js` (60fps) | | Idle activity timeout | 2s | `session.ts` | | Respawn idle timeout | 10s default | `RespawnConfig.idleTimeoutMs` | -| Respawn completion confirm | 5s | `RespawnConfig.completionConfirmMs` | +| Respawn completion confirm | 10s | `RespawnConfig.completionConfirmMs` | | Respawn auto-accept delay | 8s | `RespawnConfig.autoAcceptDelayMs` | | Respawn no-output fallback | 30s | `RespawnConfig.noOutputTimeoutMs` | | Spawn event debounce | 50ms | `spawn-detector.ts` | diff --git a/src/respawn-controller.ts b/src/respawn-controller.ts index cc5586de..f1de4415 100644 --- a/src/respawn-controller.ts +++ b/src/respawn-controller.ts @@ -28,7 +28,7 @@ * - `sendClear`: Whether to send /clear after update (default: true) * - `sendInit`: Whether to send /init after clear (default: true) * - `kickstartPrompt`: Optional prompt if /init doesn't trigger work - * - `completionConfirmMs`: Time to wait after completion message (default: 5000) + * - `completionConfirmMs`: Time to wait after completion message (default: 10000) * - `noOutputTimeoutMs`: Fallback timeout with no output at all (default: 30000) * * @module respawn-controller @@ -54,11 +54,14 @@ const RESPAWN_BUFFER_TRIM_SIZE = 512 * 1024; // 512KB // ========== Constants ========== /** - * Pattern to detect completion messages from Claude. - * Matches "for Xh Xm Xs" time duration patterns that appear at end of work. - * Examples: "for 2m 46s", "for 46s", "for 1h 2m 3s", "for 5m" + * Pattern to detect completion messages from Claude Code. + * Requires "Worked for" prefix to avoid false positives from bare time durations + * in regular text (e.g., "wait for 5s", "run for 2m"). + * + * Matches: "✻ Worked for 2m 46s", "Worked for 46s", "Worked for 1h 2m 3s" + * Does NOT match: "wait for 5s", "run for 2m", "for 3s the system..." */ -const COMPLETION_TIME_PATTERN = /\bfor\s+\d+[hms](\s*\d+[hms])*/i; +const COMPLETION_TIME_PATTERN = /\bWorked\s+for\s+\d+[hms](\s*\d+[hms])*/i; /** * Pattern to extract token count from Claude's status line. @@ -257,7 +260,7 @@ export interface RespawnConfig { /** * Time to wait after completion message before confirming idle (ms). * After seeing "for Xm Xs" pattern, waits this long with no new output. - * @default 5000 (5 seconds) + * @default 10000 (10 seconds) */ completionConfirmMs: number; @@ -329,7 +332,7 @@ const DEFAULT_CONFIG: RespawnConfig = { enabled: true, sendClear: true, // send /clear after update prompt sendInit: true, // send /init after /clear - completionConfirmMs: 5000, // 5 seconds of silence after completion message + completionConfirmMs: 10000, // 10 seconds of silence after completion message noOutputTimeoutMs: 30000, // 30 seconds fallback if no output at all autoAcceptPrompts: true, // auto-accept plan mode prompts (not questions) autoAcceptDelayMs: 8000, // 8 seconds before auto-accepting @@ -357,7 +360,7 @@ const DEFAULT_CONFIG: RespawnConfig = { * The pattern "for Xm Xs" indicates Claude finished work and reports duration. * * Confirmation: After seeing completion message, waits for output silence. - * If no new output for `completionConfirmMs` (default 5s), confirms idle. + * If no new output for `completionConfirmMs` (default 10s), confirms idle. * * Fallback: If no output at all for `noOutputTimeoutMs` (default 30s), assumes idle. * @@ -377,7 +380,7 @@ const DEFAULT_CONFIG: RespawnConfig = { * ```typescript * const respawn = new RespawnController(session, { * updatePrompt: 'continue working on the task', - * completionConfirmMs: 5000, // Wait 5s after completion message + * completionConfirmMs: 10000, // Wait 10s after completion message * }); * * respawn.on('respawnCycleCompleted', (cycle) => { @@ -839,14 +842,16 @@ export class RespawnController extends EventEmitter { return; } - // In confirming_idle state, any output (except completion) resets confirmation + // In confirming_idle state, substantial output cancels the confirmation. + // This prevents false triggers when Claude pauses briefly mid-work. if (this._state === 'confirming_idle') { - // Check if enough time has passed since completion message - const msSinceCompletion = this.completionMessageTime ? now - this.completionMessageTime : 0; - if (msSinceCompletion > 1000) { - // New output more than 1s after completion message - might be new work - this.log('New output during confirmation, checking if work resumed...'); - // Don't immediately cancel - the confirmation timer will handle it + // Strip ANSI escape codes to check if there's real content + const stripped = data.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').trim(); + if (stripped.length > 2) { + // Real content (not just escape codes or single chars) - cancel confirmation + this.log(`Substantial output during confirmation ("${stripped.substring(0, 40)}..."), cancelling idle detection`); + this.cancelCompletionConfirm(); + return; } }