diff --git a/CLAUDE.md b/CLAUDE.md index 9c96402e..1516d340 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ Claudeman is a Claude Code session manager with a web interface and autonomous R When adding new features, always ask: "Will this maintain responsiveness with 20 sessions and 50 agent windows?" -**Version**: 0.1395 (must match `package.json`) +**Version**: 0.1396 (must match `package.json`) **Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty diff --git a/package.json b/package.json index bd82ac59..2160de8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1395", + "version": "0.1396", "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", diff --git a/src/screen-manager.ts b/src/screen-manager.ts index 84f58592..f4fc5dd6 100644 --- a/src/screen-manager.ts +++ b/src/screen-manager.ts @@ -19,7 +19,7 @@ import { spawn, execSync } from 'node:child_process'; import { existsSync, readFileSync, mkdirSync, writeFile } from 'node:fs'; import { dirname, join } from 'node:path'; import { homedir } from 'node:os'; -import { ScreenSession, ProcessStats, ScreenSessionWithStats, PersistedRespawnConfig, getErrorMessage, CpuLimitConfig, DEFAULT_CPU_LIMIT_CONFIG } from './types.js'; +import { ScreenSession, ProcessStats, ScreenSessionWithStats, PersistedRespawnConfig, getErrorMessage, NiceConfig, DEFAULT_NICE_CONFIG } from './types.js'; // ============================================================================ // Claude CLI PATH Resolution @@ -56,68 +56,13 @@ const DEFAULT_STATS_INTERVAL_MS = 2000; /** Maximum retry attempts for carriage return (3) */ const CR_MAX_ATTEMPTS = 3; -/** Cached result for cpulimit availability check */ -let _cpulimitAvailable: boolean | null = null; - /** - * Checks if cpulimit is available on the system. - * Result is cached after the first check. - * - * @returns true if cpulimit is installed and accessible + * Wraps a command with `nice` for priority adjustment. */ -export function cpulimitAvailable(): boolean { - if (_cpulimitAvailable !== null) return _cpulimitAvailable; - - try { - execSync('which cpulimit', { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }); - _cpulimitAvailable = true; - } catch { - _cpulimitAvailable = false; - } - - return _cpulimitAvailable; -} - -/** - * Wraps a command with CPU limiting options. - * - * Uses `nice` to set process priority and optionally `cpulimit` for - * hard CPU usage limits (if installed). - * - * @param cmd - The command to wrap - * @param config - CPU limit configuration - * @returns The wrapped command string - * - * @example - * ```typescript - * // With cpulimit installed: - * wrapWithCpuLimit('claude --dangerously-skip-permissions', { enabled: true, niceValue: 10, cpuLimitPercent: 80 }) - * // Returns: 'cpulimit -l 80 -f -- nice -n 10 claude --dangerously-skip-permissions' - * - * // Without cpulimit: - * wrapWithCpuLimit('claude --dangerously-skip-permissions', { enabled: true, niceValue: 10, cpuLimitPercent: 80 }) - * // Returns: 'nice -n 10 claude --dangerously-skip-permissions' - * ``` - */ -export function wrapWithCpuLimit(cmd: string, config: CpuLimitConfig): string { +export function wrapWithNice(cmd: string, config: NiceConfig): string { if (!config.enabled) return cmd; - - // Clamp nice value to valid range (-20 to 19) const niceValue = Math.max(-20, Math.min(19, config.niceValue)); - - // Clamp CPU limit to valid range (1-100) - const cpuLimit = Math.max(1, Math.min(100, config.cpuLimitPercent)); - - // Always apply nice for priority reduction - let wrappedCmd = `nice -n ${niceValue} ${cmd}`; - - // Add cpulimit if available and requested - if (config.useCpulimitIfAvailable && cpulimitAvailable()) { - // -l: limit percentage, -f: follow forked processes - wrappedCmd = `cpulimit -l ${cpuLimit} -f -- ${wrappedCmd}`; - } - - return wrappedCmd; + return `nice -n ${niceValue} ${cmd}`; } @@ -292,11 +237,9 @@ export class ScreenManager extends EventEmitter { * @param workingDir - Working directory for the screen session * @param mode - 'claude' for Claude CLI or 'shell' for bash * @param name - Optional display name for the session - * @param cpuLimitConfig - Optional CPU limiting configuration - * @returns The created screen session metadata - * @throws {Error} If screen creation fails + * @param niceConfig - Optional nice priority configuration */ - async createScreen(sessionId: string, workingDir: string, mode: 'claude' | 'shell', name?: string, cpuLimitConfig?: CpuLimitConfig): Promise { + async createScreen(sessionId: string, workingDir: string, mode: 'claude' | 'shell', name?: string, niceConfig?: NiceConfig): Promise { const screenName = `claudeman-${sessionId.slice(0, 8)}`; // Security: Validate screenName and workingDir to prevent command injection @@ -313,9 +256,8 @@ export class ScreenManager extends EventEmitter { const claudeDir = findClaudeDir(); const pathExport = claudeDir ? `export PATH="${claudeDir}:$PATH" && ` : ''; - // Environment variables must be exported, not passed inline to nice/cpulimit - // Using inline VAR=value before cpulimit/nice doesn't work because they try to - // execute VAR=value as a command + // Environment variables must be exported, not passed inline to nice + // Using inline VAR=value before nice doesn't work correctly const envExports = [ 'export CLAUDEMAN_SCREEN=1', `export CLAUDEMAN_SESSION_ID=${sessionId}`, @@ -328,9 +270,9 @@ export class ScreenManager extends EventEmitter { ? 'claude --dangerously-skip-permissions' : '$SHELL'; - // Apply CPU limiting if configured - const cpuConfig = cpuLimitConfig || DEFAULT_CPU_LIMIT_CONFIG; - const cmd = wrapWithCpuLimit(baseCmd, cpuConfig); + // Apply nice priority if configured + const config = niceConfig || DEFAULT_NICE_CONFIG; + const cmd = wrapWithNice(baseCmd, config); try { // Start screen in detached mode @@ -815,11 +757,6 @@ export class ScreenManager extends EventEmitter { } } - // Check if cpulimit is available on the system (for hard CPU limiting) - static isCpulimitAvailable(): boolean { - return cpulimitAvailable(); - } - // Send input directly to screen session using screen -X stuff // This bypasses the attached PTY and sends input directly to the screen sendInput(sessionId: string, input: string): boolean { diff --git a/src/session.ts b/src/session.ts index a4b8e653..9c50a867 100644 --- a/src/session.ts +++ b/src/session.ts @@ -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, CpuLimitConfig, DEFAULT_CPU_LIMIT_CONFIG } from './types.js'; +import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem, ActiveBashTool, NiceConfig, DEFAULT_NICE_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,8 +435,8 @@ export class Session extends EventEmitter { private _parentAgentId: string | null = null; private _childAgentIds: string[] = []; - // CPU limiting configuration - private _cpuLimitConfig: CpuLimitConfig = { ...DEFAULT_CPU_LIMIT_CONFIG }; + // Nice prioritying configuration + private _niceConfig: NiceConfig = { ...DEFAULT_NICE_CONFIG }; // Store handler references for cleanup (prevents memory leaks) private _taskTrackerHandlers: { @@ -475,7 +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 + niceConfig?: NiceConfig; // Nice prioritying configuration }) { super(); this.id = config.id || uuidv4(); @@ -488,9 +488,9 @@ 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 }; + // Apply Nice priority configuration if provided + if (config.niceConfig) { + this._niceConfig = { ...config.niceConfig }; } // Initialize task tracker and forward events (store handlers for cleanup) @@ -653,29 +653,22 @@ 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 }; + // Nice priority config getters and setters + get niceConfig(): NiceConfig { + return { ...this._niceConfig }; } /** - * Set CPU limit configuration. - * Note: This only affects new sessions; existing running processes won't be limited. + * Set CPU priority configuration. + * Note: This only affects new sessions; existing running processes won't be changed. */ - setCpuLimit(config: Partial): void { + setNice(config: Partial): void { if (config.enabled !== undefined) { - this._cpuLimitConfig.enabled = config.enabled; + this._niceConfig.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; + this._niceConfig.niceValue = Math.max(-20, Math.min(19, config.niceValue)); } } @@ -794,9 +787,8 @@ 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, + niceEnabled: this._niceConfig.enabled, + niceValue: this._niceConfig.niceValue, }; } @@ -834,12 +826,10 @@ 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, + // CPU priority configuration + nice: { + enabled: this._niceConfig.enabled, + niceValue: this._niceConfig.niceValue, }, // Ralph tracking state ralphLoop: this._ralphTracker.loopState, @@ -890,7 +880,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._cpuLimitConfig); + this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'claude', this._name, this._niceConfig); console.log('[Session] Created screen session:', this._screenSession.screenName); // Wait a moment for screen to fully start @@ -1103,7 +1093,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._cpuLimitConfig); + this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'shell', this._name, this._niceConfig); console.log('[Session] Created screen session:', this._screenSession.screenName); // Wait a moment for screen to fully start diff --git a/src/types.ts b/src/types.ts index 23ac601d..856f4281 100644 --- a/src/types.ts +++ b/src/types.ts @@ -86,12 +86,10 @@ export interface SessionState { parentAgentId?: string; /** Child agent IDs spawned by this session */ childAgentIds?: string[]; - /** CPU limiting enabled */ - cpuLimitEnabled?: boolean; - /** Nice value for CPU priority (-20 to 19) */ - cpuLimitNiceValue?: number; - /** CPU limit percentage (1-100) */ - cpuLimitPercent?: number; + /** Nice priority enabled */ + niceEnabled?: boolean; + /** Nice value (-20 to 19) */ + niceValue?: number; } // ========== Global Stats Types ========== @@ -239,33 +237,22 @@ export interface AppState { tokenStats?: TokenStats; } -// ========== CPU Limiting Types ========== +// ========== Nice Priority Types ========== /** - * Configuration for CPU limiting of Claude sessions. - * - * Uses `nice` (always available) to lower process priority and - * optionally `cpulimit` (if installed) for hard CPU usage limits. + * Configuration for process priority using `nice`. + * Lower priority reduces CPU contention with other processes. */ -export interface CpuLimitConfig { - /** Whether CPU limiting is enabled */ +export interface NiceConfig { + /** Whether nice priority is enabled */ enabled: boolean; /** Nice value (-20 to 19, default: 10 = lower priority) */ niceValue: number; - /** CPU limit percentage (1-100, default: 80) */ - cpuLimitPercent: number; - /** Whether to use cpulimit if available (default: true) */ - useCpulimitIfAvailable: boolean; } -/** - * Default CPU limit configuration - */ -export const DEFAULT_CPU_LIMIT_CONFIG: CpuLimitConfig = { +export const DEFAULT_NICE_CONFIG: NiceConfig = { enabled: false, niceValue: 10, - cpuLimitPercent: 80, - useCpulimitIfAvailable: true, }; // ========== Respawn Controller Types ========== diff --git a/src/web/public/app.js b/src/web/public/app.js index b4b5e73f..2da62855 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -4886,13 +4886,10 @@ class ClaudemanApp { claudeModeSelect.onchange = () => { allowedToolsRow.style.display = claudeModeSelect.value === 'allowedTools' ? '' : 'none'; }; - // CPU Limiting settings - const cpuLimitSettings = settings.cpuLimit || {}; - document.getElementById('appSettingsCpuLimitEnabled').checked = cpuLimitSettings.enabled ?? false; - document.getElementById('appSettingsCpuNiceValue').value = cpuLimitSettings.niceValue ?? 10; - document.getElementById('appSettingsCpuLimitPercent').value = cpuLimitSettings.cpuLimitPercent ?? 80; - // Check cpulimit availability and update status indicator - this.checkCpulimitAvailability(); + // CPU Priority settings + const niceSettings = settings.nice || {}; + document.getElementById('appSettingsNiceEnabled').checked = niceSettings.enabled ?? false; + document.getElementById('appSettingsNiceValue').value = niceSettings.niceValue ?? 10; // Notification settings const notifPrefs = this.notificationManager?.preferences || {}; document.getElementById('appSettingsNotifEnabled').checked = notifPrefs.enabled ?? true; @@ -4954,12 +4951,10 @@ class ClaudemanApp { // Claude CLI settings claudeMode: document.getElementById('appSettingsClaudeMode').value, allowedTools: document.getElementById('appSettingsAllowedTools').value.trim(), - // CPU Limiting settings - cpuLimit: { - enabled: document.getElementById('appSettingsCpuLimitEnabled').checked, - niceValue: parseInt(document.getElementById('appSettingsCpuNiceValue').value) || 10, - cpuLimitPercent: parseInt(document.getElementById('appSettingsCpuLimitPercent').value) || 80, - useCpulimitIfAvailable: true, + // CPU Priority settings + nice: { + enabled: document.getElementById('appSettingsNiceEnabled').checked, + niceValue: parseInt(document.getElementById('appSettingsNiceValue').value) || 10, }, }; @@ -5021,36 +5016,6 @@ class ClaudemanApp { return {}; } - // Check if cpulimit is available and update the UI status indicator - async checkCpulimitAvailability() { - const statusEl = document.getElementById('cpuLimitStatus'); - const percentInput = document.getElementById('appSettingsCpuLimitPercent'); - if (!statusEl) return; - - try { - const res = await fetch('/api/system/cpu-limit-status'); - if (res.ok) { - const data = await res.json(); - if (data.cpulimitAvailable) { - statusEl.textContent = '\u2713'; // Checkmark - statusEl.title = 'cpulimit is installed'; - statusEl.classList.add('available'); - statusEl.classList.remove('unavailable'); - if (percentInput) percentInput.disabled = false; - } else { - statusEl.textContent = '\u2717'; // X mark - statusEl.title = 'cpulimit not installed (install with: apt install cpulimit)'; - statusEl.classList.add('unavailable'); - statusEl.classList.remove('available'); - // Don't disable input, still allow setting the value for when it gets installed - } - } - } catch (err) { - statusEl.textContent = '?'; - statusEl.title = 'Could not check cpulimit availability'; - } - } - applyHeaderVisibilitySettings() { const settings = this.loadAppSettingsFromStorage(); // Default all to true (enabled) if not set diff --git a/src/web/public/index.html b/src/web/public/index.html index 437678e1..2712aa0c 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -782,29 +782,21 @@ Auto-enable for new sessions (otherwise auto-enables on Ralph pattern detection) - -
CPU Limiting
+ +
Nice Priority
- + - Limit CPU usage for Claude sessions (reduces system impact, only affects new sessions) + Lower priority of Claude sessions (reduces system impact, only affects new sessions)
-
+
- + Process priority (-20 to 19, higher = lower priority, default: 10)
-
- -
- - -
- Hard CPU limit (requires cpulimit installed, default: 80%) -