chore: bump version to 0.1438

This commit is contained in:
arkon
2026-01-30 17:20:53 +01:00
parent f4612278ef
commit 5b6dd078f1
20 changed files with 2757 additions and 182 deletions
+54
View File
@@ -0,0 +1,54 @@
# Ralph Loop Inception - Improvement Plan
This plan improves the Ralph Loop system to make it more reliable for 24+ hour autonomous runs.
## Phase 1: Stuck-State Detection & Recovery (P0 - Critical)
- [x] P0-001: Add stuck-state detection to RespawnController - detect when the same state persists for too long without progress
- [x] P0-002: Add iteration stall detection to RalphTracker - detect when iteration count stops incrementing despite respawn cycles
- [x] P0-003: Add automatic recovery action when stuck detected - escalate from soft reset to hard reset (implemented in handleStuckStateRecovery)
- [x] P0-004: Add stuck-state metrics to DetectionStatus for UI visibility (added stuckState to DetectionStatus)
## Phase 2: Enhanced Idle Detection (P0 - Critical)
- [x] P0-005: Add confidence decay over time - if no definitive signal for extended period, gradually lower confidence threshold
- [x] P0-006: Add Session.isWorking integration check before AI idle check - skip expensive AI call if session reports working
- [x] P0-007: Add RALPH_STATUS block integration with respawn controller - use EXIT_SIGNAL for more reliable completion detection (already implemented)
## Phase 3: Promise Detection Improvements (P1 - High)
- [x] P1-001: Add fuzzy matching for completion phrases - handle minor variations like whitespace or case
- [x] P1-002: Add promise phrase validation - warn if phrase is too common (likely false positives)
- [x] P1-003: Add multi-phrase support - allow multiple valid completion phrases for complex workflows
## Phase 4: Error Recovery & Resilience (P1 - High)
- [x] P1-004: Add circuit breaker reset on successful iteration - prevent permanent disabled state
- [x] P1-005: Add exponential backoff for AI check failures instead of immediate disable
- [x] P1-006: Add session health check before respawn cycle - skip if session is in error state
## Phase 5: Todo Tracking Improvements (P1 - High)
- [x] P1-007: Add todo deduplication by content similarity - prevent duplicate todos from repeated output
- [x] P1-008: Add todo priority inference from keywords - automatically set priority based on content
- [x] P1-009: Add todo progress estimation - estimate completion based on historical patterns
## Phase 6: Respawn Cycle Optimization (P2 - Medium)
- [x] P2-001: Add adaptive timing based on session behavior - adjust timeouts based on observed patterns (uses rolling 75th percentile of idle detection times)
- [x] P2-002: Add skip-clear optimization - skip /clear if context usage is low (below 30% by default)
- [ ] P2-003: Add smart kickstart prompt generation - use context to generate relevant kickstart prompts (deferred - requires AI generation)
## Phase 7: Monitoring & Observability (P2 - Medium)
- [x] P2-004: Add respawn cycle metrics - track success rate, average duration, failure reasons (RespawnCycleMetrics, RespawnAggregateMetrics types)
- [x] P2-005: Add Ralph Loop health score - aggregate metric for loop reliability (calculateHealthScore() method with 5 component scores)
- [ ] P2-006: Add automated anomaly detection - alert on unusual patterns (deferred - requires statistical analysis)
## Completion Criteria
All P0 and P1 tasks must be completed. P2 tasks are nice-to-have.
Tests must pass after each change.
Documentation must be updated.
When ALL P0 and P1 tasks are complete, output: <promise>RALPH_INCEPTION_COMPLETE</promise>
+64 -23
View File
@@ -16,7 +16,7 @@ When user says "COM":
1. Increment version in BOTH `package.json` AND `CLAUDE.md`
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
**Version**: 0.1437 (must match `package.json`)
**Version**: 0.1438 (must match `package.json`)
## Project Overview
@@ -35,10 +35,14 @@ Claudeman is a Claude Code session manager with web interface and autonomous Ral
**Default port**: `3000` (web UI at `http://localhost:3000`)
```bash
# Setup
npm install # Install dependencies
# Development
npx tsx src/index.ts web # Dev server (RECOMMENDED)
npx tsx src/index.ts web --https # With TLS (only needed for remote access)
npm run typecheck # Type check
tsc --noEmit --watch # Continuous type checking
# Testing
npx vitest run # All tests
@@ -46,6 +50,7 @@ npx vitest run test/<file>.test.ts # Single file
npx vitest run -t "pattern" # Tests matching name
npm run test:coverage # With coverage report
npm run test:e2e # Browser E2E (requires: npx playwright install chromium)
npm run test:e2e:quick # Quick E2E (just quick-start workflow)
# Production
npm run build
@@ -73,20 +78,35 @@ journalctl --user -u claudeman-web -f
| `src/subagent-watcher.ts` | Monitors Claude Code's Task tool (background agents) |
| `src/run-summary.ts` | Timeline events for "what happened while away" |
| `src/ai-idle-checker.ts` | AI-powered idle detection with `ai-checker-base.ts` |
| `src/bash-tool-parser.ts` | Parses Claude's bash tool invocations from output |
| `src/transcript-watcher.ts` | Watches Claude's transcript files for changes |
| `src/hooks-config.ts` | Manages `.claude/settings.local.json` hook configuration |
| `src/image-watcher.ts` | Watches for image file creation (screenshots, etc.) |
| `src/plan-orchestrator.ts` | Multi-agent plan generation with research and planning phases |
| `src/prompts/*.ts` | Agent prompts (research-agent, code-reviewer, planner) |
| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` |
| `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows |
| `src/types.ts` | All TypeScript interfaces |
### Config Files (`src/config/`)
| File | Purpose |
|------|---------|
| `buffer-limits.ts` | Terminal/text buffer size limits |
| `map-limits.ts` | Global limits for Maps, sessions, watchers |
### Utility Files (`src/utils/`)
| File | Purpose |
|------|---------|
| `index.ts` | Re-exports all utilities (standard import point) |
| `lru-map.ts` | LRU eviction Map for bounded caches |
| `stale-expiration-map.ts` | TTL-based Map with lazy expiration |
| `cleanup-manager.ts` | Centralized resource disposal |
| `buffer-accumulator.ts` | Chunk accumulator with size limits |
| `string-similarity.ts` | String matching utilities (fuzzy matching) |
| `token-validation.ts` | Token count parsing and validation |
| `regex-patterns.ts` | Shared regex patterns for parsing |
### Data Flow
@@ -103,15 +123,17 @@ journalctl --user -u claudeman-web -f
**Token tracking**: Interactive mode parses status line ("123.4k tokens"), estimates 60/40 input/output split.
**Memory leak prevention**: Frontend runs long; clear all Maps/timers on SSE reconnect in `handleInit()`. Backend clears `_recentTaskDescriptions` in Session.stop(), nulls promise callbacks on error, and removes watcher listeners on shutdown.
**Hook events**: Claude Code hooks trigger notifications via `/api/hook-event`. Key events: `permission_prompt` (tool approval needed), `elicitation_dialog` (Claude asking question), `idle_prompt` (waiting for input), `stop` (response complete). See `src/hooks-config.ts`.
## Adding Features
- **API endpoint**: Types in `types.ts`, route in `server.ts:buildServer()`, use `createErrorResponse()`
- **API endpoint**: Types in `types.ts`, route in `server.ts:buildServer()`, use `createErrorResponse()`. Validate request bodies with Zod schemas.
- **SSE event**: Emit via `broadcast()`, handle in `app.js:handleSSEEvent()`
- **Session setting**: Add to `SessionState` in `types.ts`, include in `session.toState()`, call `persistSessionState()`
- **New test**: Pick unique port (see below), add port comment to test file header
**Validation**: Uses Zod v4 for request validation. Define schemas near route handlers and use `.parse()` or `.safeParse()`.
## State Files
| File | Purpose |
@@ -120,13 +142,41 @@ journalctl --user -u claudeman-web -f
| `~/.claudeman/screens.json` | Screen metadata for recovery |
| `~/.claudeman/settings.json` | User preferences |
## Default Settings
UI defaults are optimized for minimal distraction. Set in `src/web/public/app.js` (using `??` operator).
**Display Settings** (default values):
| Setting | Default | Description |
|---------|---------|-------------|
| `showFontControls` | `false` | Font size controls in header |
| `showSystemStats` | `true` | CPU/memory stats in header |
| `showTokenCount` | `true` | Token counter in header |
| `showCost` | `false` | Cost display |
| `showMonitor` | `true` | Monitor panel |
| `showProjectInsights` | `false` | Project insights panel |
| `showFileBrowser` | `false` | File browser panel |
| `showSubagents` | `false` | Subagent windows panel |
**Tracking Settings**:
| Setting | Default | Description |
|---------|---------|-------------|
| `ralphTrackerEnabled` | `false` | Ralph/Todo loop tracking |
| `subagentTrackingEnabled` | `true` | Background agent monitoring |
| `subagentActiveTabOnly` | `true` | Show subagents only for active session |
| `imageWatcherEnabled` | `false` | Watch for image file creation |
**Notification Defaults**: Browser notifications enabled, audio alerts disabled. Critical events (permission prompts, questions) notify by default; info events (respawn cycles, token milestones) are silent.
To change defaults, edit the `??` fallback values in `openAppSettings()` and `apply*Visibility()` functions.
## Testing
**Port allocation**: E2E tests use centralized ports in `test/e2e/e2e.config.ts`. Unit/integration tests pick unique ports manually. Search `const PORT =` or `TEST_PORT` in test files to find used ports before adding new tests.
**E2E tests**: Use Playwright. Run `npx playwright install chromium` first. See `test/e2e/fixtures/` for helpers. E2E config (`test/e2e/e2e.config.ts`) provides ports (3183-3190), timeouts, and helpers.
**Test config**: Vitest runs with `globals: true` (no imports needed for `describe`/`it`/`expect`) and `fileParallelism: false` (files run sequentially to respect screen limits). Unit test timeout is 30s, teardown timeout is 60s. E2E tests have longer timeouts defined in `test/e2e/e2e.config.ts` (90s test, 30s session creation).
**Test config**: Vitest runs with `globals: true` (no imports needed for `describe`/`it`/`expect`/`vi`) and `fileParallelism: false` (files run sequentially to respect screen limits). Unit test timeout is 30s, teardown timeout is 60s. E2E tests have longer timeouts defined in `test/e2e/e2e.config.ts` (90s test, 30s session creation).
**Test safety**: `test/setup.ts` provides:
- Screen concurrency limiter (max 10)
@@ -183,7 +233,7 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base
| **Ralph Loop guide** | `docs/ralph-wiggum-guide.md` |
| **Claude Code hooks** | `docs/claude-code-hooks-reference.md` |
| **Browser/E2E testing** | `docs/browser-testing-guide.md` |
| **API routes** | `src/web/server.ts:buildServer()` or README.md |
| **API routes** | `src/web/server.ts:buildServer()` or README.md (full endpoint tables) |
| **SSE events** | Search `broadcast(` in `server.ts` |
| **CLI commands** | `claudeman --help` |
| **Frontend patterns** | `src/web/public/app.js` (subagent windows, notifications) |
@@ -193,6 +243,7 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base
| **Test utilities** | `test/respawn-test-utils.ts` |
| **Memory leak patterns** | `test/memory-leak-prevention.test.ts` |
| **Keyboard shortcuts** | README.md or App Settings in web UI |
| **Mobile/SSH access** | README.md (Claudeman Screens / `sc` command) |
| **Plan orchestrator** | `src/plan-orchestrator.ts` file header |
| **Agent prompts** | `src/prompts/` directory |
@@ -201,7 +252,7 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base
| Script | Purpose |
|--------|---------|
| `scripts/screen-manager.sh` | Safe screen management (use instead of direct kill commands) |
| `scripts/screen-chooser.sh` | Mobile-friendly screen session picker for Termius/iPhone |
| `scripts/screen-chooser.sh` | Claudeman Screens - mobile-friendly session picker (`sc` alias, see README for usage) |
| `scripts/monitor-respawn.sh` | Monitor respawn state machine in real-time |
| `scripts/postinstall.js` | npm postinstall hook for setup |
@@ -209,19 +260,9 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base
The TUI (Terminal UI) has been removed in favor of the web interface. Files in `src/tui/` are excluded from compilation via `tsconfig.json`.
## Recent Memory Leak Fixes (2026-01-30)
## Memory Leak Prevention
All P0 memory leak issues have been fixed in commit `e3e0d22`:
### Backend Fixes
- **Session._recentTaskDescriptions**: Now cleared in `stop()` and `clearBuffers()`
- **Session promise callbacks**: Nulled after rejection in `runPrompt()` catch block
- **Watcher listeners**: SubagentWatcher and ImageWatcher listeners stored and removed on server shutdown
### Frontend Fixes
- **Plan file windows**: Drag/resize handlers stored on elements and cleaned up via `closePlanFileWindow()`
- **Plan file manager**: Drag handler stored and cleaned up via `closePlanFileManager()`
- **cleanupAllFloatingWindows()**: Now cleans up plan file windows
Frontend runs long (24+ hour sessions); all Maps/timers must be cleaned up.
### Cleanup Patterns
When adding new event listeners or timers:
@@ -229,8 +270,8 @@ When adding new event listeners or timers:
2. Add cleanup to appropriate `stop()` or `cleanup*()` method
3. For singleton watchers, store refs in class properties and remove in server `stop()`
### Verification Tests
Memory leak prevention patterns are tested in `test/memory-leak-prevention.test.ts`. Run with:
```bash
npx vitest run test/memory-leak-prevention.test.ts
```
**Backend**: Clear Maps in `stop()`, null promise callbacks on error, remove watcher listeners on shutdown.
**Frontend**: Store drag/resize handlers on elements, clean up in `close*()` functions. SSE reconnect calls `handleInit()` which resets state.
Run `npx vitest run test/memory-leak-prevention.test.ts` to verify patterns.
+31
View File
@@ -282,6 +282,37 @@ claudeman web
---
## Mobile Access (Termius/SSH)
**Claudeman Screens** (`sc`) is a mobile-friendly screen session chooser, optimized for Termius on iPhone.
```bash
sc # Interactive chooser
sc 2 # Quick attach to session 2
sc -l # List sessions
sc -h # Help
```
**Features:**
- Single-digit selection (1-9) for fast thumb typing
- Color-coded status indicators (attached/detached/respawn)
- Token count display
- Session names from Claudeman state
- Pagination for many sessions
- Auto-refresh every 60 seconds
**Indicators:**
| Symbol | Meaning |
|--------|---------|
| `*` / `●` | Attached (someone connected) |
| `-` / `○` | Detached (available) |
| `R` | Respawn enabled |
| `45k` | Token count |
**Tip:** Detach from a screen with `Ctrl+A D`
---
## API
### Sessions
+40
View File
@@ -588,6 +588,32 @@ add_to_path() {
success "Added to $profile - restart your shell or run: source $profile"
}
setup_sc_alias() {
local profile
profile=$(detect_shell_profile)
# Check if alias already exists
if [[ -f "$profile" ]] && grep -qE "^alias sc=" "$profile" 2>/dev/null; then
info "Alias 'sc' already configured in $profile"
return 0
fi
local shell_name
shell_name="$(basename "${SHELL:-/bin/bash}")"
if [[ "$shell_name" == "fish" ]]; then
echo "" >> "$profile"
echo "# Claudeman Screens shortcut" >> "$profile"
echo "alias sc='screen-chooser'" >> "$profile"
else
echo "" >> "$profile"
echo "# Claudeman Screens shortcut" >> "$profile"
echo "alias sc='screen-chooser'" >> "$profile"
fi
info "Added 'sc' alias for screen-chooser"
}
# ============================================================================
# Screen Configuration
# ============================================================================
@@ -876,6 +902,14 @@ main() {
ln -sf "$INSTALL_DIR/dist/index.js" "$symlink_dir/claudeman"
info "Created symlink: $symlink_dir/claudeman"
# Install screen-chooser as 'screen-chooser' command
if [[ -f "$INSTALL_DIR/scripts/screen-chooser.sh" ]]; then
ln -sf "$INSTALL_DIR/scripts/screen-chooser.sh" "$symlink_dir/screen-chooser"
info "Created symlink: $symlink_dir/screen-chooser"
# Add 'sc' alias for quick access
setup_sc_alias
fi
# Add ~/.local/bin to PATH if not already there
if [[ ":$PATH:" != *":$symlink_dir:"* ]]; then
add_to_path "$symlink_dir"
@@ -919,6 +953,12 @@ main() {
echo -e " ${CYAN}# Open in browser${NC}"
echo -e " http://localhost:3000"
echo ""
echo -e " ${BOLD}Mobile Access (Termius/SSH):${NC}"
echo ""
echo -e " ${CYAN}sc${NC} # Interactive screen session chooser"
echo -e " ${CYAN}sc 2${NC} # Quick attach to session 2"
echo -e " ${CYAN}sc -h${NC} # Help"
echo ""
if [[ "$os" == "linux" ]] && [[ -f "$HOME/.config/systemd/user/claudeman-web.service" ]]; then
echo -e " ${BOLD}Systemd Service:${NC}"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claudeman",
"version": "0.1437",
"version": "0.1438",
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
+12 -12
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ============================================================================
# Screen Chooser for iPhone/Termius
# Optimized for iPhone 17 Pro (portrait ~45 chars, landscape ~95 chars)
# Claudeman Screens - Mobile-friendly Screen Session Chooser
# Optimized for iPhone/Termius (portrait ~45 chars, landscape ~95 chars)
# ============================================================================
#
# Design principles:
@@ -12,12 +12,12 @@
# - Minimal keystrokes to attach
#
# Usage:
# ./screen-chooser.sh # Interactive chooser
# ./screen-chooser.sh 1 # Quick attach to session 1
# ./screen-chooser.sh -l # List only (no interactive)
# ./screen-chooser.sh -h # Help
# screen-chooser # Interactive chooser
# screen-chooser 1 # Quick attach to session 1
# screen-chooser -l # List only (non-interactive)
# screen-chooser -h # Help
#
# Alias: alias sc='path/to/screen-chooser.sh'
# Alias (added by installer): alias sc='screen-chooser'
# Then: sc (interactive)
# sc 2 (attach session 2)
#
@@ -343,7 +343,7 @@ clear_screen() {
# Print header
print_header() {
local count=${#SCREEN_PIDS[@]}
echo -e "${B}${CYAN}${ICON_SCREEN} Screens${R} ${D}($count)${R}"
echo -e "${B}${CYAN}Claudeman Screens${R} ${D}($count)${R}"
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
}
@@ -423,7 +423,7 @@ print_footer() {
# Print no screens message
print_no_screens() {
clear_screen
echo -e "${B}${CYAN}${ICON_SCREEN} Screens${R}"
echo -e "${B}${CYAN}Claudeman Screens${R}"
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
echo ""
echo -e " ${YELLOW}No screen sessions found${R}"
@@ -631,7 +631,7 @@ quick_attach() {
show_help() {
cat << 'EOF'
Screen Chooser for iPhone/Termius
Claudeman Screens - Mobile-friendly Screen Session Chooser
USAGE:
sc Interactive chooser
@@ -653,9 +653,9 @@ INDICATORS:
45k Token count
TIPS:
- Alias: alias sc='path/to/screen-chooser.sh'
- Detach: Ctrl+A D
- Detach from screen: Ctrl+A D
- Session names from Claudeman state
- Optimized for Termius/iPhone
EOF
}
+9 -1
View File
@@ -508,7 +508,15 @@ export abstract class AiCheckerBase<
if (this.consecutiveErrors >= this.config.maxConsecutiveErrors) {
this.disable(`${this.config.maxConsecutiveErrors} consecutive errors: ${errorMsg}`);
} else {
this.startCooldown(this.config.errorCooldownMs);
// P1-005: Exponential backoff for errors
// Base cooldown * 2^(consecutiveErrors-1), capped at 5 minutes
const backoffMultiplier = Math.pow(2, this.consecutiveErrors - 1);
const backoffCooldownMs = Math.min(
this.config.errorCooldownMs * backoffMultiplier,
5 * 60 * 1000 // Max 5 minutes
);
this.log(`Exponential backoff: ${Math.round(backoffCooldownMs / 1000)}s (error #${this.consecutiveErrors})`);
this.startCooldown(backoffCooldownMs);
}
}
+52 -19
View File
@@ -70,33 +70,66 @@ const DEFAULT_AI_CHECK_CONFIG: AiIdleCheckConfig = {
/** Pattern to match IDLE or WORKING as the first word of output */
const VERDICT_PATTERN = /^\s*(IDLE|WORKING)\b/i;
/** The prompt sent to the AI checker */
const AI_CHECK_PROMPT = `Analyze this terminal output from a running Claude Code session. Determine if the session is IDLE (done working, waiting for new input) or WORKING (still actively processing).
/**
* The prompt sent to the AI idle checker.
*
* P1-005: Enhanced with more specific working pattern examples and clearer structure.
*/
const AI_CHECK_PROMPT = `You are analyzing terminal output from a Claude Code CLI session. Determine if Claude has FINISHED working (IDLE) or is STILL WORKING (WORKING).
IMPORTANT: When in doubt, answer WORKING. Brief pauses between tool executions do NOT mean the session is idle. Claude may be processing or about to output more.
CRITICAL RULE: When in doubt, ALWAYS answer WORKING. False positives (saying IDLE when Claude is working) cause session interruptions. It's safer to wait longer than to interrupt active work.
IDLE indicators (need MULTIPLE of these to confirm idle):
- Completion summary shown (e.g., "✻ Worked for 2m 46s", "Worked for 5s")
- Prompt character visible at the end (❯ or similar)
- Cost summary displayed (e.g., "$0.12 spent")
- Clear end of output with no pending work
## IDLE Indicators (need AT LEAST 2 of these together)
WORKING indicators (ANY of these means WORKING):
- Spinner characters (⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ or similar)
- Activity text: Thinking, Writing, Reading, Running, Searching, Editing, Creating, Deleting, Analyzing, Executing, Synthesizing, Compiling, Building, Processing, Loading, Generating, Testing, Checking, Validating
- Tool execution in progress (commands being run)
- Truncated or partial lines at the end
- File operations in progress
- Output that appears mid-stream or incomplete
- No completion summary visible yet
1. **Completion Summary** - The most reliable signal:
- "✻ Worked for Xm Ys" (e.g., "✻ Worked for 2m 46s")
- "Worked for Xs" (e.g., "Worked for 5s")
- Cost summary: "$X.XX spent" or "X tokens used"
Terminal output (most recent at bottom):
2. **Input Prompt Visible**:
- The ❯ prompt character at the very end
- Empty line after completion summary
- Waiting cursor position
3. **Task Completion Language**:
- "All done", "Finished", "Completed successfully"
- Explicit "waiting for input" or similar
## WORKING Indicators (ANY ONE of these = answer WORKING)
### Active Processing Indicators:
- **Spinners**: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ (Braille), ◐ ◓ ◑ ◒ (quarter), ⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷
- **Activity Words**: Thinking, Writing, Reading, Running, Searching, Editing, Creating, Deleting, Analyzing, Executing, Synthesizing, Compiling, Building, Processing, Loading, Generating, Testing, Checking, Validating, Brewing, Formatting, Linting, Installing, Fetching, Downloading
### Tool Execution in Progress:
- Bash commands with no result shown yet
- "Running: npm test", "Executing command..."
- File read/write operations incomplete
- Progress bars or percentage indicators
- Test suite running (dots appearing, "Test Suites: X passed")
### Output Structure Issues:
- Truncated lines without completion
- JSON/code blocks not closed
- Multi-line output clearly incomplete
- "..." indicating more to come
- Output ending mid-sentence or mid-word
### Claude Planning/Thinking:
- "Let me...", "I'll...", "Now I need to..."
- TodoWrite updates without completion
- Plan mode approval prompts (numbered options)
## Terminal Output to Analyze
---
{TERMINAL_BUFFER}
---
Answer with EXACTLY one word on the first line: IDLE or WORKING
If uncertain, answer WORKING. Then briefly explain why.`;
## Your Response
First line: EXACTLY "IDLE" or "WORKING" (nothing else)
Second line onwards: Brief explanation of your reasoning.
Remember: When uncertain, answer WORKING.`;
// ========== AiIdleChecker Class ==========
+1072 -64
View File
File diff suppressed because it is too large Load Diff
+896 -16
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -727,6 +727,7 @@ export class Session extends EventEmitter {
inputTokens: this._totalInputTokens,
outputTokens: this._totalOutputTokens,
ralphEnabled: this._ralphTracker.enabled,
ralphAutoEnableDisabled: this._ralphTracker.autoEnableDisabled || undefined,
ralphCompletionPhrase: this._ralphTracker.loopState.completionPhrase || undefined,
parentAgentId: this._parentAgentId || undefined,
childAgentIds: this._childAgentIds.length > 0 ? this._childAgentIds : undefined,
+214 -1
View File
@@ -166,6 +166,8 @@ export interface SessionState {
respawnConfig?: RespawnConfig & { durationMinutes?: number };
/** Ralph / Todo tracker enabled */
ralphEnabled?: boolean;
/** Ralph auto-enable disabled (user explicitly turned off Ralph) */
ralphAutoEnableDisabled?: boolean;
/** Ralph completion phrase (if set) */
ralphCompletionPhrase?: string;
/** Parent agent ID if this session is a spawned agent */
@@ -394,6 +396,159 @@ export interface RespawnConfig {
aiPlanCheckTimeoutMs?: number;
/** Cooldown after NOT_PLAN_MODE verdict in ms */
aiPlanCheckCooldownMs?: number;
// ========== P2-001: Adaptive Timing ==========
/** Whether to use adaptive timing based on historical patterns */
adaptiveTimingEnabled?: boolean;
/** Minimum value for adaptive completion confirm (ms) */
adaptiveMinConfirmMs?: number;
/** Maximum value for adaptive completion confirm (ms) */
adaptiveMaxConfirmMs?: number;
// ========== P2-002: Skip-Clear Optimization ==========
/** Whether to skip /clear when context is below threshold */
skipClearWhenLowContext?: boolean;
/** Token percentage threshold below which /clear is skipped (0-100) */
skipClearThresholdPercent?: number;
// ========== P2-004: Cycle Metrics ==========
/** Whether to track and persist cycle metrics */
trackCycleMetrics?: boolean;
}
// ========== P2-004: Respawn Cycle Metrics ==========
/**
* Outcome of a respawn cycle
*/
export type CycleOutcome =
| 'success' // Cycle completed normally
| 'stuck_recovery' // Stuck-state recovery triggered
| 'blocked' // Blocked by circuit breaker or exit signal
| 'error' // Error during cycle
| 'cancelled'; // Cancelled (e.g., controller stopped)
/**
* Metrics for a single respawn cycle.
* Persisted for post-mortem analysis of long-running loops.
*/
export interface RespawnCycleMetrics {
/** Unique cycle ID (session-id:cycle-number) */
cycleId: string;
/** Session ID this cycle belongs to */
sessionId: string;
/** Cycle number within the session */
cycleNumber: number;
/** Timestamp when cycle started */
startedAt: number;
/** Timestamp when cycle completed */
completedAt: number;
/** Total duration of cycle (ms) */
durationMs: number;
/** What triggered idle detection */
idleReason: string;
/** Time spent detecting idle (from start of watching to idle confirmed) */
idleDetectionMs: number;
/** Steps completed in this cycle */
stepsCompleted: string[];
/** Whether /clear was skipped (P2-002) */
clearSkipped: boolean;
/** Outcome of the cycle */
outcome: CycleOutcome;
/** Error message if outcome is 'error' */
errorMessage?: string;
/** Token count at start of cycle */
tokenCountAtStart?: number;
/** Token count at end of cycle */
tokenCountAtEnd?: number;
/** Completion confirm time used (may be adaptive) */
completionConfirmMsUsed: number;
}
/**
* Aggregate metrics across multiple cycles for health scoring.
*/
export interface RespawnAggregateMetrics {
/** Total cycles tracked */
totalCycles: number;
/** Successful cycles */
successfulCycles: number;
/** Cycles that required stuck-state recovery */
stuckRecoveryCycles: number;
/** Blocked cycles */
blockedCycles: number;
/** Error cycles */
errorCycles: number;
/** Average cycle duration (ms) */
avgCycleDurationMs: number;
/** Average idle detection time (ms) */
avgIdleDetectionMs: number;
/** 90th percentile cycle duration (ms) */
p90CycleDurationMs: number;
/** Success rate (0-100) */
successRate: number;
/** Last updated timestamp */
lastUpdatedAt: number;
}
// ========== P2-005: Ralph Loop Health Score ==========
/**
* Health status levels for the Ralph Loop system.
*/
export type HealthStatus = 'excellent' | 'good' | 'degraded' | 'critical';
/**
* Comprehensive health score for a Ralph Loop session.
* Aggregates multiple health signals into a single score.
*/
export interface RalphLoopHealthScore {
/** Overall health score (0-100) */
score: number;
/** Health status based on score thresholds */
status: HealthStatus;
/** Individual component scores (0-100 each) */
components: {
/** Based on recent cycle success rate */
cycleSuccess: number;
/** Based on circuit breaker state */
circuitBreaker: number;
/** Based on iteration stall metrics */
iterationProgress: number;
/** Based on AI checker error rate */
aiChecker: number;
/** Based on stuck-state recovery count */
stuckRecovery: number;
};
/** Human-readable summary of health */
summary: string;
/** Recommendations for improvement */
recommendations: string[];
/** Timestamp when score was calculated */
calculatedAt: number;
}
// ========== Timing History for Adaptive Timing ==========
/**
* Historical timing data for adaptive adjustments.
*/
export interface TimingHistory {
/** Rolling window of recent idle detection durations (ms) */
recentIdleDetectionMs: number[];
/** Rolling window of recent cycle durations (ms) */
recentCycleDurationMs: number[];
/** Calculated adaptive completion confirm value (ms) */
adaptiveCompletionConfirmMs: number;
/** Number of samples in rolling windows */
sampleCount: number;
/** Maximum samples to keep */
maxSamples: number;
/** Last updated timestamp */
lastUpdatedAt: number;
}
/**
@@ -829,13 +984,43 @@ export type RalphTodoStatus = 'pending' | 'in_progress' | 'completed';
/**
* State of per-session Ralph / Todo tracking (detected from Claude output)
*/
/**
* Confidence scoring for completion detection.
* Helps distinguish genuine completion signals from false positives.
*/
export interface CompletionConfidence {
/** Overall confidence level (0-100) */
score: number;
/** Whether score is above threshold for triggering completion */
isConfident: boolean;
/** Individual signal contributions */
signals: {
/** Promise tag detected with proper formatting */
hasPromiseTag: boolean;
/** Phrase matches expected completion phrase */
matchesExpected: boolean;
/** All todos are marked complete */
allTodosComplete: boolean;
/** EXIT_SIGNAL: true in RALPH_STATUS block */
hasExitSignal: boolean;
/** Multiple completion indicators present */
multipleIndicators: boolean;
/** Output context suggests completion (not in prompt/explanation) */
contextAppropriate: boolean;
};
/** Timestamp of last confidence calculation */
calculatedAt: number;
}
export interface RalphTrackerState {
/** Whether the tracker is actively monitoring (disabled by default) */
enabled: boolean;
/** Whether a loop is currently active */
active: boolean;
/** Detected completion phrase */
/** Detected completion phrase (primary) */
completionPhrase: string | null;
/** Additional valid completion phrases (P1-003: multi-phrase support) */
alternateCompletionPhrases?: string[];
/** Timestamp when loop started */
startedAt: number | null;
/** Number of cycles/iterations detected */
@@ -850,6 +1035,8 @@ export interface RalphTrackerState {
planVersion?: number;
/** Number of versions in history (for versioning UI) */
planHistoryLength?: number;
/** Last completion confidence assessment */
completionConfidence?: CompletionConfidence;
}
/**
@@ -872,6 +1059,32 @@ export interface RalphTodoItem {
detectedAt: number;
/** Priority level (P0=critical, P1=high, P2=normal) */
priority: RalphTodoPriority;
/** P1-009: Estimated time to complete (ms), based on historical patterns */
estimatedDurationMs?: number;
/** P1-009: Complexity category for progress estimation */
estimatedComplexity?: 'trivial' | 'simple' | 'moderate' | 'complex';
}
/**
* Progress estimation for the todo list
*/
export interface RalphTodoProgress {
/** Total number of todos */
total: number;
/** Number completed */
completed: number;
/** Number in progress */
inProgress: number;
/** Number pending */
pending: number;
/** Completion percentage (0-100) */
percentComplete: number;
/** Estimated remaining time (ms), based on historical completion rate */
estimatedRemainingMs: number | null;
/** Average time per todo completion (ms) */
avgCompletionTimeMs: number | null;
/** Projected completion timestamp (epoch ms) */
projectedCompletionAt: number | null;
}
/**
+9
View File
@@ -24,3 +24,12 @@ export {
validateTokenCounts,
validateTokensAndCost,
} from './token-validation.js';
export {
levenshteinDistance,
stringSimilarity,
isSimilar,
isSimilarByDistance,
normalizePhrase,
fuzzyPhraseMatch,
todoContentHash,
} from './string-similarity.js';
+208
View File
@@ -0,0 +1,208 @@
/**
* @fileoverview String similarity utilities for fuzzy matching.
*
* Provides Levenshtein distance and similarity scoring for:
* - Completion phrase fuzzy matching
* - Todo content deduplication
*
* @module utils/string-similarity
*/
/**
* Calculate the Levenshtein (edit) distance between two strings.
* This is the minimum number of single-character edits (insertions,
* deletions, or substitutions) required to change one string into the other.
*
* Uses Wagner-Fischer algorithm with O(min(m,n)) space optimization.
*
* @param a - First string
* @param b - Second string
* @returns The edit distance (0 = identical)
*
* @example
* levenshteinDistance('hello', 'hello') // 0
* levenshteinDistance('hello', 'helo') // 1 (one deletion)
* levenshteinDistance('COMPLETE', 'COMPLET') // 1 (one deletion)
*/
export function levenshteinDistance(a: string, b: string): number {
// Ensure a is the shorter string for space efficiency
if (a.length > b.length) {
[a, b] = [b, a];
}
const m = a.length;
const n = b.length;
// Early exit for identical strings
if (a === b) return 0;
// Early exit for empty strings
if (m === 0) return n;
// Use single array (space optimization)
let prev = new Array<number>(m + 1);
let curr = new Array<number>(m + 1);
// Initialize first row
for (let i = 0; i <= m; i++) {
prev[i] = i;
}
// Fill the matrix row by row
for (let j = 1; j <= n; j++) {
curr[0] = j;
for (let i = 1; i <= m; i++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
curr[i] = Math.min(
prev[i] + 1, // deletion
curr[i - 1] + 1, // insertion
prev[i - 1] + cost // substitution
);
}
// Swap rows
[prev, curr] = [curr, prev];
}
return prev[m];
}
/**
* Calculate similarity ratio between two strings (0 to 1).
* Uses Levenshtein distance normalized by the longer string's length.
*
* @param a - First string
* @param b - Second string
* @returns Similarity ratio (1.0 = identical, 0.0 = completely different)
*
* @example
* stringSimilarity('hello', 'hello') // 1.0
* stringSimilarity('hello', 'helo') // 0.8 (4/5 similar)
* stringSimilarity('abc', 'xyz') // 0.0 (3 edits, length 3)
*/
export function stringSimilarity(a: string, b: string): number {
if (a === b) return 1.0;
if (a.length === 0 && b.length === 0) return 1.0;
if (a.length === 0 || b.length === 0) return 0.0;
const distance = levenshteinDistance(a, b);
const maxLength = Math.max(a.length, b.length);
return 1 - distance / maxLength;
}
/**
* Check if two strings are similar within a given threshold.
*
* @param a - First string
* @param b - Second string
* @param threshold - Minimum similarity ratio (default: 0.85 = 85% similar)
* @returns True if similarity >= threshold
*
* @example
* isSimilar('COMPLETE', 'COMPLET', 0.85) // true (87.5% similar)
* isSimilar('COMPLETE', 'DONE', 0.85) // false (0% similar)
*/
export function isSimilar(a: string, b: string, threshold = 0.85): boolean {
return stringSimilarity(a, b) >= threshold;
}
/**
* Check if two strings are similar with edit distance tolerance.
* More intuitive for short strings than percentage-based threshold.
*
* @param a - First string
* @param b - Second string
* @param maxDistance - Maximum allowed edit distance (default: 2)
* @returns True if edit distance <= maxDistance
*
* @example
* isSimilarByDistance('COMPLETE', 'COMPLET', 2) // true (distance 1)
* isSimilarByDistance('COMPLETE', 'COMP', 2) // false (distance 4)
*/
export function isSimilarByDistance(a: string, b: string, maxDistance = 2): boolean {
return levenshteinDistance(a, b) <= maxDistance;
}
/**
* Normalize a completion phrase for comparison.
* Handles variations in case, whitespace, and separators.
*
* @param phrase - Raw completion phrase
* @returns Normalized phrase (uppercase, no separators)
*
* @example
* normalizePhrase('task_done') // 'TASKDONE'
* normalizePhrase('TASK-DONE') // 'TASKDONE'
* normalizePhrase('Task Done') // 'TASKDONE'
*/
export function normalizePhrase(phrase: string): string {
return phrase
.toUpperCase()
.replace(/[\s_\-\.]+/g, '') // Remove whitespace, underscores, hyphens, dots
.trim();
}
/**
* Check if two completion phrases match with fuzzy tolerance.
*
* First normalizes both phrases, then checks:
* 1. Exact match after normalization
* 2. Edit distance <= maxDistance for typo tolerance
*
* @param phrase1 - First phrase to compare
* @param phrase2 - Second phrase to compare
* @param maxDistance - Maximum edit distance for fuzzy match (default: 2)
* @returns True if phrases match (exact or fuzzy)
*
* @example
* fuzzyPhraseMatch('COMPLETE', 'COMPLETE') // true (exact)
* fuzzyPhraseMatch('COMPLETE', 'COMPLET') // true (typo)
* fuzzyPhraseMatch('TASK_DONE', 'TASKDONE') // true (separator)
* fuzzyPhraseMatch('COMPLETE', 'FINISHED') // false (different word)
*/
export function fuzzyPhraseMatch(
phrase1: string,
phrase2: string,
maxDistance = 2
): boolean {
const norm1 = normalizePhrase(phrase1);
const norm2 = normalizePhrase(phrase2);
// Exact match after normalization
if (norm1 === norm2) return true;
// For short phrases (< 6 chars), require exact match to avoid false positives
// e.g., "DONE" shouldn't match "DENY"
if (norm1.length < 6 || norm2.length < 6) {
return false;
}
// Fuzzy match with edit distance
return isSimilarByDistance(norm1, norm2, maxDistance);
}
/**
* Generate a content hash for todo deduplication.
* Normalizes content and generates a simple hash.
*
* @param content - Todo item content
* @returns Normalized hash string for comparison
*/
export function todoContentHash(content: string): string {
// Normalize: lowercase, collapse whitespace, remove punctuation
const normalized = content
.toLowerCase()
.replace(/\s+/g, ' ')
.replace(/[^\w\s]/g, '')
.trim();
// Simple hash using reduce (fast, good enough for deduplication)
let hash = 0;
for (let i = 0; i < normalized.length; i++) {
const char = normalized.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(36);
}
+29 -17
View File
@@ -6504,8 +6504,14 @@ class ClaudemanApp {
if (total > 0) {
tokensEl.style.display = '';
const tokenStr = this.formatTokens(total);
const estimatedCost = this.estimateCost(input, output);
tokensEl.textContent = `${tokenStr} tokens · $${estimatedCost.toFixed(2)}`;
const settings = this.loadAppSettingsFromStorage();
const showCost = settings.showCost ?? false;
if (showCost) {
const estimatedCost = this.estimateCost(input, output);
tokensEl.textContent = `${tokenStr} tokens · $${estimatedCost.toFixed(2)}`;
} else {
tokensEl.textContent = `${tokenStr} tokens`;
}
} else {
tokensEl.style.display = 'none';
}
@@ -6865,10 +6871,14 @@ class ClaudemanApp {
const estimatedCost = this.estimateCost(totalInput, totalOutput);
const tokenEl = this.$('headerTokens');
if (tokenEl) {
tokenEl.textContent = total > 0 ? `${display} tokens · $${estimatedCost.toFixed(2)}` : '0 tokens';
const settings = this.loadAppSettingsFromStorage();
const showCost = settings.showCost ?? false;
tokenEl.textContent = total > 0
? (showCost ? `${display} tokens · $${estimatedCost.toFixed(2)}` : `${display} tokens`)
: '0 tokens';
tokenEl.title = this.globalStats
? `Lifetime: ${this.globalStats.totalSessionsCreated} sessions created\nEstimated cost based on Claude Opus pricing`
: 'Token usage across active sessions\nEstimated cost based on Claude Opus pricing';
? `Lifetime: ${this.globalStats.totalSessionsCreated} sessions created${showCost ? '\nEstimated cost based on Claude Opus pricing' : ''}`
: `Token usage across active sessions${showCost ? '\nEstimated cost based on Claude Opus pricing' : ''}`;
}
}
@@ -7781,16 +7791,17 @@ class ClaudemanApp {
document.getElementById('appSettingsDefaultDir').value = settings.defaultWorkingDir || '';
document.getElementById('appSettingsRalphEnabled').checked = settings.ralphTrackerEnabled ?? false;
// Header visibility settings (default to true/enabled)
document.getElementById('appSettingsShowFontControls').checked = settings.showFontControls ?? true;
document.getElementById('appSettingsShowFontControls').checked = settings.showFontControls ?? false;
document.getElementById('appSettingsShowSystemStats').checked = settings.showSystemStats ?? true;
document.getElementById('appSettingsShowTokenCount').checked = settings.showTokenCount ?? true;
document.getElementById('appSettingsShowCost').checked = settings.showCost ?? false;
document.getElementById('appSettingsShowMonitor').checked = settings.showMonitor ?? true;
document.getElementById('appSettingsShowProjectInsights').checked = settings.showProjectInsights ?? true;
document.getElementById('appSettingsShowProjectInsights').checked = settings.showProjectInsights ?? false;
document.getElementById('appSettingsShowFileBrowser').checked = settings.showFileBrowser ?? false;
document.getElementById('appSettingsShowSubagents').checked = settings.showSubagents ?? true;
document.getElementById('appSettingsShowSubagents').checked = settings.showSubagents ?? false;
document.getElementById('appSettingsSubagentTracking').checked = settings.subagentTrackingEnabled ?? true;
document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? false;
document.getElementById('appSettingsImageWatcherEnabled').checked = settings.imageWatcherEnabled ?? true;
document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? true;
document.getElementById('appSettingsImageWatcherEnabled').checked = settings.imageWatcherEnabled ?? false;
// Claude CLI settings
const claudeModeSelect = document.getElementById('appSettingsClaudeMode');
const allowedToolsRow = document.getElementById('allowedToolsRow');
@@ -7906,6 +7917,7 @@ class ClaudemanApp {
showFontControls: document.getElementById('appSettingsShowFontControls').checked,
showSystemStats: document.getElementById('appSettingsShowSystemStats').checked,
showTokenCount: document.getElementById('appSettingsShowTokenCount').checked,
showCost: document.getElementById('appSettingsShowCost').checked,
showMonitor: document.getElementById('appSettingsShowMonitor').checked,
showProjectInsights: document.getElementById('appSettingsShowProjectInsights').checked,
showFileBrowser: document.getElementById('appSettingsShowFileBrowser').checked,
@@ -8107,7 +8119,7 @@ class ClaudemanApp {
applyHeaderVisibilitySettings() {
const settings = this.loadAppSettingsFromStorage();
// Default all to true (enabled) if not set
const showFontControls = settings.showFontControls ?? true;
const showFontControls = settings.showFontControls ?? false;
const showSystemStats = settings.showSystemStats ?? true;
const showTokenCount = settings.showTokenCount ?? true;
@@ -8141,7 +8153,7 @@ class ClaudemanApp {
applyMonitorVisibility() {
const settings = this.loadAppSettingsFromStorage();
const showMonitor = settings.showMonitor ?? true;
const showSubagents = settings.showSubagents ?? true;
const showSubagents = settings.showSubagents ?? false;
const showFileBrowser = settings.showFileBrowser ?? false;
const monitorPanel = document.getElementById('monitorPanel');
@@ -10679,7 +10691,7 @@ class ClaudemanApp {
*/
updateSubagentWindowVisibility() {
const settings = this.loadAppSettingsFromStorage();
const activeTabOnly = settings.subagentActiveTabOnly ?? false;
const activeTabOnly = settings.subagentActiveTabOnly ?? true;
for (const [agentId, windowInfo] of this.subagentWindows) {
// Get parent from PERSISTENT map (THE source of truth)
@@ -10722,7 +10734,7 @@ class ClaudemanApp {
const existing = this.subagentWindows.get(agentId);
const agent = this.subagents.get(agentId);
const settings = this.loadAppSettingsFromStorage();
const activeTabOnly = settings.subagentActiveTabOnly ?? false;
const activeTabOnly = settings.subagentActiveTabOnly ?? true;
// If window is hidden (different tab) and activeTabOnly is enabled, switch to parent tab
if (existing.hidden && agent?.parentSessionId && activeTabOnly) {
@@ -10883,7 +10895,7 @@ class ClaudemanApp {
// Check if this window should be visible based on settings
// Use the PERSISTENT parent map for accurate tab-based visibility
const settings = this.loadAppSettingsFromStorage();
const activeTabOnly = settings.subagentActiveTabOnly ?? false;
const activeTabOnly = settings.subagentActiveTabOnly ?? true;
let shouldHide = false;
if (activeTabOnly) {
const storedParent = this.subagentParentMap.get(agentId);
@@ -11132,7 +11144,7 @@ class ClaudemanApp {
if (windowData) {
const settings = this.loadAppSettingsFromStorage();
const activeTabOnly = settings.subagentActiveTabOnly ?? false;
const activeTabOnly = settings.subagentActiveTabOnly ?? true;
// Get parent from PERSISTENT map (THE source of truth)
const storedParent = this.subagentParentMap.get(agentId);
@@ -11559,7 +11571,7 @@ class ClaudemanApp {
// Check if panel is enabled in settings
const settings = this.loadAppSettingsFromStorage();
const showProjectInsights = settings.showProjectInsights ?? true;
const showProjectInsights = settings.showProjectInsights ?? false;
if (!showProjectInsights) {
panel.classList.remove('visible');
this.projectInsightsPanelVisible = false;
+7
View File
@@ -731,6 +731,13 @@
<span class="slider"></span>
</label>
</div>
<div class="settings-item" title="Show estimated cost next to token count">
<span class="settings-item-label">Show Cost ($)</span>
<label class="switch switch-sm">
<input type="checkbox" id="appSettingsShowCost" checked>
<span class="slider"></span>
</label>
</div>
<!-- Panels Section -->
<div class="settings-section-header">Panels</div>
+24 -20
View File
@@ -5118,22 +5118,24 @@ kbd {
}
.connection-line {
stroke: #00ff41;
stroke-width: 2.5;
stroke: #3b82f6;
stroke-width: 3;
stroke-dasharray: 5 3;
fill: none;
opacity: 0.75;
/* Dark outline for contrast, subtle glow */
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.7))
drop-shadow(0 0 3px rgba(0, 255, 65, 0.5));
opacity: 0.9;
/* Dark outline for contrast, vibrant blue glow */
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
drop-shadow(0 0 4px rgba(59, 130, 246, 0.8))
drop-shadow(0 0 8px rgba(59, 130, 246, 0.5));
transition: opacity 0.2s, stroke-width 0.2s, filter 0.2s;
}
.connection-line:hover {
opacity: 0.9;
stroke-width: 3;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
drop-shadow(0 0 5px rgba(0, 255, 65, 0.7));
opacity: 1;
stroke-width: 3.5;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9))
drop-shadow(0 0 6px rgba(59, 130, 246, 1))
drop-shadow(0 0 12px rgba(59, 130, 246, 0.7));
}
.connection-line.spawning-line {
@@ -5160,24 +5162,26 @@ kbd {
/* Plan subagent to regular subagent connection lines (Opus → Haiku) */
.connection-line.plan-to-subagent-line {
stroke: #00ff41;
stroke-width: 2.5;
/* Dark outline for contrast, subtle glow */
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.7))
drop-shadow(0 0 3px rgba(0, 255, 65, 0.5));
stroke: #3b82f6;
stroke-width: 3;
/* Dark outline for contrast, vibrant blue glow */
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
drop-shadow(0 0 4px rgba(59, 130, 246, 0.8))
drop-shadow(0 0 8px rgba(59, 130, 246, 0.5));
stroke-dasharray: 5 3;
animation: plan-subagent-pulse 1.2s ease-in-out infinite;
}
.connection-line.plan-to-subagent-line:hover {
stroke-width: 3;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
drop-shadow(0 0 5px rgba(0, 255, 65, 0.7));
stroke-width: 3.5;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9))
drop-shadow(0 0 6px rgba(59, 130, 246, 1))
drop-shadow(0 0 12px rgba(59, 130, 246, 0.7));
}
@keyframes plan-subagent-pulse {
0%, 100% { opacity: 0.65; }
50% { opacity: 0.85; }
0%, 100% { opacity: 0.8; }
50% { opacity: 1; }
}
/* ========== Project Insights Panel (Bash File Viewers) ========== */
+18 -6
View File
@@ -1121,8 +1121,12 @@ export class WebServer extends EventEmitter {
if (enabled !== undefined) {
if (enabled) {
session.ralphTracker.enable();
// Allow re-enabling on restart if user explicitly enabled
session.ralphTracker.enableAutoEnable();
} else {
session.ralphTracker.disable();
// Prevent re-enabling on restart when user explicitly disabled
session.ralphTracker.disableAutoEnable();
}
// Persist Ralph enabled state
this.screenManager.updateRalphEnabled(id, enabled);
@@ -1369,8 +1373,8 @@ export class WebServer extends EventEmitter {
}
try {
// Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled)
if (this.store.getConfig().ralphEnabled) {
// Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled and not explicitly disabled by user)
if (this.store.getConfig().ralphEnabled && !session.ralphTracker.autoEnableDisabled) {
autoConfigureRalph(session, session.workingDir, () => {});
if (!session.ralphTracker.enabled) {
session.ralphTracker.enable();
@@ -1688,8 +1692,8 @@ export class WebServer extends EventEmitter {
}
try {
// Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled)
if (this.store.getConfig().ralphEnabled) {
// Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled and not explicitly disabled by user)
if (this.store.getConfig().ralphEnabled && !session.ralphTracker.autoEnableDisabled) {
autoConfigureRalph(session, session.workingDir, () => {});
if (!session.ralphTracker.enabled) {
session.ralphTracker.enable();
@@ -2288,6 +2292,7 @@ export class WebServer extends EventEmitter {
autoConfigureRalph(session, casePath, () => {}); // no broadcast yet
if (!session.ralphTracker.enabled) {
session.ralphTracker.enable();
session.ralphTracker.enableAutoEnable(); // Allow re-enabling on restart
}
}
@@ -4561,6 +4566,13 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
}
}
// Ralph / Todo tracker
if (savedState.ralphAutoEnableDisabled) {
session.ralphTracker.disableAutoEnable();
console.log(`[Server] Restored Ralph auto-enable disabled for session ${session.id}`);
} else if (savedState.ralphEnabled) {
// If Ralph was enabled and not explicitly disabled, allow re-enabling on restart
session.ralphTracker.enableAutoEnable();
}
if (savedState.ralphEnabled) {
session.ralphTracker.enable();
if (savedState.ralphCompletionPhrase) {
@@ -4594,8 +4606,8 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
}
}
// Fallback: restore Ralph state from state-inner.json if not already set
if (!session.ralphTracker.enabled) {
// Fallback: restore Ralph state from state-inner.json if not already set and not explicitly disabled
if (!session.ralphTracker.enabled && !session.ralphTracker.autoEnableDisabled) {
const ralphState = this.store.getRalphState(screen.sessionId);
if (ralphState?.loop?.enabled) {
session.ralphTracker.restoreState(ralphState.loop, ralphState.todos);
+14 -2
View File
@@ -295,6 +295,12 @@ describe('AiIdleChecker', () => {
});
it('should disable after maxConsecutiveErrors', async () => {
// With P1-005 exponential backoff, cooldowns increase:
// Error 1: 1000ms * 2^0 = 1000ms
// Error 2: 1000ms * 2^1 = 2000ms
// Error 3: disabled (no cooldown)
const cooldowns = [1100, 2100]; // Wait slightly longer than each cooldown
for (let i = 0; i < 3; i++) {
mockedReadFileSync.mockReturnValueOnce('')
.mockReturnValueOnce('garbage\n__AICHECK_DONE__');
@@ -305,7 +311,7 @@ describe('AiIdleChecker', () => {
// Clear cooldown for next check (except after the last one which disables)
if (i < 2) {
await vi.advanceTimersByTimeAsync(1100);
await vi.advanceTimersByTimeAsync(cooldowns[i]);
}
}
@@ -464,13 +470,19 @@ describe('AiIdleChecker', () => {
const handler = vi.fn();
checker.on('disabled', handler);
// With exponential backoff (P1-005), cooldown increases:
// Error 1: 1000ms * 2^0 = 1000ms
// Error 2: 1000ms * 2^1 = 2000ms
// Error 3: disabled (no cooldown needed)
const cooldowns = [1100, 2100]; // Wait longer than exponential backoff
for (let i = 0; i < 3; i++) {
mockedReadFileSync.mockReturnValueOnce('')
.mockReturnValueOnce('garbage\n__AICHECK_DONE__');
const checkPromise = checker.check('output');
await vi.advanceTimersByTimeAsync(1000);
await checkPromise;
if (i < 2) await vi.advanceTimersByTimeAsync(1100);
if (i < 2) await vi.advanceTimersByTimeAsync(cooldowns[i]);
}
expect(handler).toHaveBeenCalledWith(expect.stringContaining('consecutive errors'));
+2
View File
@@ -15,6 +15,8 @@ class MockSession extends EventEmitter {
id = 'mock-session-id';
workingDir = '/tmp';
status = 'idle';
pid = 12345; // Mock PID for P1-006 health check
isWorking = false; // P0-006 Session.isWorking integration
writeBuffer: string[] = [];
write(data: string): void {