feat: add CPU limiting and Ralph Loop wizard plan generation

CPU Limiting:
- Add CpuLimitConfig type and settings in App Settings
- Use nice command for process priority (-20 to 19)
- Optional cpulimit integration for hard CPU limits (if installed)
- Applied to new sessions via screen-manager and session modules

Ralph Loop Wizard:
- 3-step wizard: Describe → Plan → Launch
- New /api/generate-plan endpoint generates implementation steps via Claude
- Plan editor with checkboxes, priorities (P0/P1/P2), reordering
- Skip plan option for manual workflows
- Compact wizard modal layout

UI Updates:
- Form section headers for grouped settings
- CPU limit status indicator (cpulimit availability)
- Wizard modal wider layout and inline fields

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-27 01:58:14 +01:00
co-authored by Claude Opus 4.5
parent 7cb7bc96e9
commit 59b406f73f
7 changed files with 1074 additions and 52 deletions
+48 -3
View File
@@ -21,7 +21,7 @@ import { existsSync } from 'node:fs';
import { dirname } from 'node:path';
import { v4 as uuidv4 } from 'uuid';
import * as pty from 'node-pty';
import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem, ActiveBashTool } from './types.js';
import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem, ActiveBashTool, CpuLimitConfig, DEFAULT_CPU_LIMIT_CONFIG } from './types.js';
import { TaskTracker, type BackgroundTask } from './task-tracker.js';
import { RalphTracker } from './ralph-tracker.js';
import { BashToolParser } from './bash-tool-parser.js';
@@ -435,6 +435,9 @@ export class Session extends EventEmitter {
private _parentAgentId: string | null = null;
private _childAgentIds: string[] = [];
// CPU limiting configuration
private _cpuLimitConfig: CpuLimitConfig = { ...DEFAULT_CPU_LIMIT_CONFIG };
// Store handler references for cleanup (prevents memory leaks)
private _taskTrackerHandlers: {
taskCreated: (task: BackgroundTask) => void;
@@ -472,6 +475,7 @@ export class Session extends EventEmitter {
screenManager?: ScreenManager;
useScreen?: boolean;
screenSession?: ScreenSession; // For restored sessions - pass the existing screen
cpuLimitConfig?: CpuLimitConfig; // CPU limiting configuration
}) {
super();
this.id = config.id || uuidv4();
@@ -484,6 +488,11 @@ export class Session extends EventEmitter {
this._useScreen = config.useScreen ?? (this._screenManager !== null && ScreenManager.isScreenAvailable());
this._screenSession = config.screenSession || null; // Use existing screen if provided
// Apply CPU limit configuration if provided
if (config.cpuLimitConfig) {
this._cpuLimitConfig = { ...config.cpuLimitConfig };
}
// Initialize task tracker and forward events (store handlers for cleanup)
this._taskTracker = new TaskTracker();
this._taskTrackerHandlers = {
@@ -644,6 +653,32 @@ export class Session extends EventEmitter {
if (idx >= 0) this._childAgentIds.splice(idx, 1);
}
// CPU limit config getters and setters
get cpuLimitConfig(): CpuLimitConfig {
return { ...this._cpuLimitConfig };
}
/**
* Set CPU limit configuration.
* Note: This only affects new sessions; existing running processes won't be limited.
*/
setCpuLimit(config: Partial<CpuLimitConfig>): void {
if (config.enabled !== undefined) {
this._cpuLimitConfig.enabled = config.enabled;
}
if (config.niceValue !== undefined) {
// Clamp to valid range
this._cpuLimitConfig.niceValue = Math.max(-20, Math.min(19, config.niceValue));
}
if (config.cpuLimitPercent !== undefined) {
// Clamp to valid range
this._cpuLimitConfig.cpuLimitPercent = Math.max(1, Math.min(100, config.cpuLimitPercent));
}
if (config.useCpulimitIfAvailable !== undefined) {
this._cpuLimitConfig.useCpulimitIfAvailable = config.useCpulimitIfAvailable;
}
}
// Token tracking getters and setters
get totalTokens(): number {
return this._totalInputTokens + this._totalOutputTokens;
@@ -759,6 +794,9 @@ export class Session extends EventEmitter {
ralphCompletionPhrase: this._ralphTracker.loopState.completionPhrase || undefined,
parentAgentId: this._parentAgentId || undefined,
childAgentIds: this._childAgentIds.length > 0 ? this._childAgentIds : undefined,
cpuLimitEnabled: this._cpuLimitConfig.enabled,
cpuLimitNiceValue: this._cpuLimitConfig.niceValue,
cpuLimitPercent: this._cpuLimitConfig.cpuLimitPercent,
};
}
@@ -796,6 +834,13 @@ export class Session extends EventEmitter {
enabled: this._autoClearEnabled,
threshold: this._autoClearThreshold,
},
// CPU limit configuration
cpuLimit: {
enabled: this._cpuLimitConfig.enabled,
niceValue: this._cpuLimitConfig.niceValue,
cpuLimitPercent: this._cpuLimitConfig.cpuLimitPercent,
useCpulimitIfAvailable: this._cpuLimitConfig.useCpulimitIfAvailable,
},
// Ralph tracking state
ralphLoop: this._ralphTracker.loopState,
ralphTodos: this._ralphTracker.todos,
@@ -845,7 +890,7 @@ export class Session extends EventEmitter {
console.log('[Session] Attaching to existing screen session:', this._screenSession!.screenName);
} else {
// Create a new screen session
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'claude', this._name);
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'claude', this._name, this._cpuLimitConfig);
console.log('[Session] Created screen session:', this._screenSession.screenName);
// Wait a moment for screen to fully start
@@ -1051,7 +1096,7 @@ export class Session extends EventEmitter {
console.log('[Session] Attaching to existing screen session:', this._screenSession!.screenName);
} else {
// Create a new screen session
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'shell', this._name);
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'shell', this._name, this._cpuLimitConfig);
console.log('[Session] Created screen session:', this._screenSession.screenName);
// Wait a moment for screen to fully start