mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
chore: bump version to 0.1438
This commit is contained in:
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+896
-16
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,3 +24,12 @@ export {
|
||||
validateTokenCounts,
|
||||
validateTokensAndCost,
|
||||
} from './token-validation.js';
|
||||
export {
|
||||
levenshteinDistance,
|
||||
stringSimilarity,
|
||||
isSimilar,
|
||||
isSimilarByDistance,
|
||||
normalizePhrase,
|
||||
fuzzyPhraseMatch,
|
||||
todoContentHash,
|
||||
} from './string-similarity.js';
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user