mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
fix: remove cpulimit, use nice only for CPU priority
cpulimit -f interfered with Claude's Ink terminal UI causing duplicate prompt rendering. Replaced with nice-only approach. - Remove cpulimit entirely (was causing display issues) - Rename CpuLimitConfig -> NiceConfig for clean naming - Simplify wrapWithCpuLimit -> wrapWithNice - Update UI labels and settings key (cpuLimit -> nice) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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?"
|
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
|
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudeman",
|
"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",
|
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
+11
-74
@@ -19,7 +19,7 @@ import { spawn, execSync } from 'node:child_process';
|
|||||||
import { existsSync, readFileSync, mkdirSync, writeFile } from 'node:fs';
|
import { existsSync, readFileSync, mkdirSync, writeFile } from 'node:fs';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { homedir } from 'node:os';
|
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
|
// Claude CLI PATH Resolution
|
||||||
@@ -56,68 +56,13 @@ const DEFAULT_STATS_INTERVAL_MS = 2000;
|
|||||||
/** Maximum retry attempts for carriage return (3) */
|
/** Maximum retry attempts for carriage return (3) */
|
||||||
const CR_MAX_ATTEMPTS = 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.
|
* Wraps a command with `nice` for priority adjustment.
|
||||||
* Result is cached after the first check.
|
|
||||||
*
|
|
||||||
* @returns true if cpulimit is installed and accessible
|
|
||||||
*/
|
*/
|
||||||
export function cpulimitAvailable(): boolean {
|
export function wrapWithNice(cmd: string, config: NiceConfig): string {
|
||||||
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 {
|
|
||||||
if (!config.enabled) return cmd;
|
if (!config.enabled) return cmd;
|
||||||
|
|
||||||
// Clamp nice value to valid range (-20 to 19)
|
|
||||||
const niceValue = Math.max(-20, Math.min(19, config.niceValue));
|
const niceValue = Math.max(-20, Math.min(19, config.niceValue));
|
||||||
|
return `nice -n ${niceValue} ${cmd}`;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -292,11 +237,9 @@ export class ScreenManager extends EventEmitter {
|
|||||||
* @param workingDir - Working directory for the screen session
|
* @param workingDir - Working directory for the screen session
|
||||||
* @param mode - 'claude' for Claude CLI or 'shell' for bash
|
* @param mode - 'claude' for Claude CLI or 'shell' for bash
|
||||||
* @param name - Optional display name for the session
|
* @param name - Optional display name for the session
|
||||||
* @param cpuLimitConfig - Optional CPU limiting configuration
|
* @param niceConfig - Optional nice priority configuration
|
||||||
* @returns The created screen session metadata
|
|
||||||
* @throws {Error} If screen creation fails
|
|
||||||
*/
|
*/
|
||||||
async createScreen(sessionId: string, workingDir: string, mode: 'claude' | 'shell', name?: string, cpuLimitConfig?: CpuLimitConfig): Promise<ScreenSession> {
|
async createScreen(sessionId: string, workingDir: string, mode: 'claude' | 'shell', name?: string, niceConfig?: NiceConfig): Promise<ScreenSession> {
|
||||||
const screenName = `claudeman-${sessionId.slice(0, 8)}`;
|
const screenName = `claudeman-${sessionId.slice(0, 8)}`;
|
||||||
|
|
||||||
// Security: Validate screenName and workingDir to prevent command injection
|
// Security: Validate screenName and workingDir to prevent command injection
|
||||||
@@ -313,9 +256,8 @@ export class ScreenManager extends EventEmitter {
|
|||||||
const claudeDir = findClaudeDir();
|
const claudeDir = findClaudeDir();
|
||||||
const pathExport = claudeDir ? `export PATH="${claudeDir}:$PATH" && ` : '';
|
const pathExport = claudeDir ? `export PATH="${claudeDir}:$PATH" && ` : '';
|
||||||
|
|
||||||
// Environment variables must be exported, not passed inline to nice/cpulimit
|
// Environment variables must be exported, not passed inline to nice
|
||||||
// Using inline VAR=value before cpulimit/nice doesn't work because they try to
|
// Using inline VAR=value before nice doesn't work correctly
|
||||||
// execute VAR=value as a command
|
|
||||||
const envExports = [
|
const envExports = [
|
||||||
'export CLAUDEMAN_SCREEN=1',
|
'export CLAUDEMAN_SCREEN=1',
|
||||||
`export CLAUDEMAN_SESSION_ID=${sessionId}`,
|
`export CLAUDEMAN_SESSION_ID=${sessionId}`,
|
||||||
@@ -328,9 +270,9 @@ export class ScreenManager extends EventEmitter {
|
|||||||
? 'claude --dangerously-skip-permissions'
|
? 'claude --dangerously-skip-permissions'
|
||||||
: '$SHELL';
|
: '$SHELL';
|
||||||
|
|
||||||
// Apply CPU limiting if configured
|
// Apply nice priority if configured
|
||||||
const cpuConfig = cpuLimitConfig || DEFAULT_CPU_LIMIT_CONFIG;
|
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
||||||
const cmd = wrapWithCpuLimit(baseCmd, cpuConfig);
|
const cmd = wrapWithNice(baseCmd, config);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Start screen in detached mode
|
// 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
|
// Send input directly to screen session using screen -X stuff
|
||||||
// This bypasses the attached PTY and sends input directly to the screen
|
// This bypasses the attached PTY and sends input directly to the screen
|
||||||
sendInput(sessionId: string, input: string): boolean {
|
sendInput(sessionId: string, input: string): boolean {
|
||||||
|
|||||||
+23
-33
@@ -21,7 +21,7 @@ import { existsSync } from 'node:fs';
|
|||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import * as pty from 'node-pty';
|
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 { TaskTracker, type BackgroundTask } from './task-tracker.js';
|
||||||
import { RalphTracker } from './ralph-tracker.js';
|
import { RalphTracker } from './ralph-tracker.js';
|
||||||
import { BashToolParser } from './bash-tool-parser.js';
|
import { BashToolParser } from './bash-tool-parser.js';
|
||||||
@@ -435,8 +435,8 @@ export class Session extends EventEmitter {
|
|||||||
private _parentAgentId: string | null = null;
|
private _parentAgentId: string | null = null;
|
||||||
private _childAgentIds: string[] = [];
|
private _childAgentIds: string[] = [];
|
||||||
|
|
||||||
// CPU limiting configuration
|
// Nice prioritying configuration
|
||||||
private _cpuLimitConfig: CpuLimitConfig = { ...DEFAULT_CPU_LIMIT_CONFIG };
|
private _niceConfig: NiceConfig = { ...DEFAULT_NICE_CONFIG };
|
||||||
|
|
||||||
// Store handler references for cleanup (prevents memory leaks)
|
// Store handler references for cleanup (prevents memory leaks)
|
||||||
private _taskTrackerHandlers: {
|
private _taskTrackerHandlers: {
|
||||||
@@ -475,7 +475,7 @@ export class Session extends EventEmitter {
|
|||||||
screenManager?: ScreenManager;
|
screenManager?: ScreenManager;
|
||||||
useScreen?: boolean;
|
useScreen?: boolean;
|
||||||
screenSession?: ScreenSession; // For restored sessions - pass the existing screen
|
screenSession?: ScreenSession; // For restored sessions - pass the existing screen
|
||||||
cpuLimitConfig?: CpuLimitConfig; // CPU limiting configuration
|
niceConfig?: NiceConfig; // Nice prioritying configuration
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.id = config.id || uuidv4();
|
this.id = config.id || uuidv4();
|
||||||
@@ -488,9 +488,9 @@ export class Session extends EventEmitter {
|
|||||||
this._useScreen = config.useScreen ?? (this._screenManager !== null && ScreenManager.isScreenAvailable());
|
this._useScreen = config.useScreen ?? (this._screenManager !== null && ScreenManager.isScreenAvailable());
|
||||||
this._screenSession = config.screenSession || null; // Use existing screen if provided
|
this._screenSession = config.screenSession || null; // Use existing screen if provided
|
||||||
|
|
||||||
// Apply CPU limit configuration if provided
|
// Apply Nice priority configuration if provided
|
||||||
if (config.cpuLimitConfig) {
|
if (config.niceConfig) {
|
||||||
this._cpuLimitConfig = { ...config.cpuLimitConfig };
|
this._niceConfig = { ...config.niceConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize task tracker and forward events (store handlers for cleanup)
|
// 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);
|
if (idx >= 0) this._childAgentIds.splice(idx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU limit config getters and setters
|
// Nice priority config getters and setters
|
||||||
get cpuLimitConfig(): CpuLimitConfig {
|
get niceConfig(): NiceConfig {
|
||||||
return { ...this._cpuLimitConfig };
|
return { ...this._niceConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set CPU limit configuration.
|
* Set CPU priority configuration.
|
||||||
* Note: This only affects new sessions; existing running processes won't be limited.
|
* Note: This only affects new sessions; existing running processes won't be changed.
|
||||||
*/
|
*/
|
||||||
setCpuLimit(config: Partial<CpuLimitConfig>): void {
|
setNice(config: Partial<NiceConfig>): void {
|
||||||
if (config.enabled !== undefined) {
|
if (config.enabled !== undefined) {
|
||||||
this._cpuLimitConfig.enabled = config.enabled;
|
this._niceConfig.enabled = config.enabled;
|
||||||
}
|
}
|
||||||
if (config.niceValue !== undefined) {
|
if (config.niceValue !== undefined) {
|
||||||
// Clamp to valid range
|
// Clamp to valid range
|
||||||
this._cpuLimitConfig.niceValue = Math.max(-20, Math.min(19, config.niceValue));
|
this._niceConfig.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,9 +787,8 @@ export class Session extends EventEmitter {
|
|||||||
ralphCompletionPhrase: this._ralphTracker.loopState.completionPhrase || undefined,
|
ralphCompletionPhrase: this._ralphTracker.loopState.completionPhrase || undefined,
|
||||||
parentAgentId: this._parentAgentId || undefined,
|
parentAgentId: this._parentAgentId || undefined,
|
||||||
childAgentIds: this._childAgentIds.length > 0 ? this._childAgentIds : undefined,
|
childAgentIds: this._childAgentIds.length > 0 ? this._childAgentIds : undefined,
|
||||||
cpuLimitEnabled: this._cpuLimitConfig.enabled,
|
niceEnabled: this._niceConfig.enabled,
|
||||||
cpuLimitNiceValue: this._cpuLimitConfig.niceValue,
|
niceValue: this._niceConfig.niceValue,
|
||||||
cpuLimitPercent: this._cpuLimitConfig.cpuLimitPercent,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,12 +826,10 @@ export class Session extends EventEmitter {
|
|||||||
enabled: this._autoClearEnabled,
|
enabled: this._autoClearEnabled,
|
||||||
threshold: this._autoClearThreshold,
|
threshold: this._autoClearThreshold,
|
||||||
},
|
},
|
||||||
// CPU limit configuration
|
// CPU priority configuration
|
||||||
cpuLimit: {
|
nice: {
|
||||||
enabled: this._cpuLimitConfig.enabled,
|
enabled: this._niceConfig.enabled,
|
||||||
niceValue: this._cpuLimitConfig.niceValue,
|
niceValue: this._niceConfig.niceValue,
|
||||||
cpuLimitPercent: this._cpuLimitConfig.cpuLimitPercent,
|
|
||||||
useCpulimitIfAvailable: this._cpuLimitConfig.useCpulimitIfAvailable,
|
|
||||||
},
|
},
|
||||||
// Ralph tracking state
|
// Ralph tracking state
|
||||||
ralphLoop: this._ralphTracker.loopState,
|
ralphLoop: this._ralphTracker.loopState,
|
||||||
@@ -890,7 +880,7 @@ export class Session extends EventEmitter {
|
|||||||
console.log('[Session] Attaching to existing screen session:', this._screenSession!.screenName);
|
console.log('[Session] Attaching to existing screen session:', this._screenSession!.screenName);
|
||||||
} else {
|
} else {
|
||||||
// Create a new screen session
|
// 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);
|
console.log('[Session] Created screen session:', this._screenSession.screenName);
|
||||||
|
|
||||||
// Wait a moment for screen to fully start
|
// 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);
|
console.log('[Session] Attaching to existing screen session:', this._screenSession!.screenName);
|
||||||
} else {
|
} else {
|
||||||
// Create a new screen session
|
// 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);
|
console.log('[Session] Created screen session:', this._screenSession.screenName);
|
||||||
|
|
||||||
// Wait a moment for screen to fully start
|
// Wait a moment for screen to fully start
|
||||||
|
|||||||
+10
-23
@@ -86,12 +86,10 @@ export interface SessionState {
|
|||||||
parentAgentId?: string;
|
parentAgentId?: string;
|
||||||
/** Child agent IDs spawned by this session */
|
/** Child agent IDs spawned by this session */
|
||||||
childAgentIds?: string[];
|
childAgentIds?: string[];
|
||||||
/** CPU limiting enabled */
|
/** Nice priority enabled */
|
||||||
cpuLimitEnabled?: boolean;
|
niceEnabled?: boolean;
|
||||||
/** Nice value for CPU priority (-20 to 19) */
|
/** Nice value (-20 to 19) */
|
||||||
cpuLimitNiceValue?: number;
|
niceValue?: number;
|
||||||
/** CPU limit percentage (1-100) */
|
|
||||||
cpuLimitPercent?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Global Stats Types ==========
|
// ========== Global Stats Types ==========
|
||||||
@@ -239,33 +237,22 @@ export interface AppState {
|
|||||||
tokenStats?: TokenStats;
|
tokenStats?: TokenStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== CPU Limiting Types ==========
|
// ========== Nice Priority Types ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for CPU limiting of Claude sessions.
|
* Configuration for process priority using `nice`.
|
||||||
*
|
* Lower priority reduces CPU contention with other processes.
|
||||||
* Uses `nice` (always available) to lower process priority and
|
|
||||||
* optionally `cpulimit` (if installed) for hard CPU usage limits.
|
|
||||||
*/
|
*/
|
||||||
export interface CpuLimitConfig {
|
export interface NiceConfig {
|
||||||
/** Whether CPU limiting is enabled */
|
/** Whether nice priority is enabled */
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/** Nice value (-20 to 19, default: 10 = lower priority) */
|
/** Nice value (-20 to 19, default: 10 = lower priority) */
|
||||||
niceValue: number;
|
niceValue: number;
|
||||||
/** CPU limit percentage (1-100, default: 80) */
|
|
||||||
cpuLimitPercent: number;
|
|
||||||
/** Whether to use cpulimit if available (default: true) */
|
|
||||||
useCpulimitIfAvailable: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const DEFAULT_NICE_CONFIG: NiceConfig = {
|
||||||
* Default CPU limit configuration
|
|
||||||
*/
|
|
||||||
export const DEFAULT_CPU_LIMIT_CONFIG: CpuLimitConfig = {
|
|
||||||
enabled: false,
|
enabled: false,
|
||||||
niceValue: 10,
|
niceValue: 10,
|
||||||
cpuLimitPercent: 80,
|
|
||||||
useCpulimitIfAvailable: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== Respawn Controller Types ==========
|
// ========== Respawn Controller Types ==========
|
||||||
|
|||||||
+8
-43
@@ -4886,13 +4886,10 @@ class ClaudemanApp {
|
|||||||
claudeModeSelect.onchange = () => {
|
claudeModeSelect.onchange = () => {
|
||||||
allowedToolsRow.style.display = claudeModeSelect.value === 'allowedTools' ? '' : 'none';
|
allowedToolsRow.style.display = claudeModeSelect.value === 'allowedTools' ? '' : 'none';
|
||||||
};
|
};
|
||||||
// CPU Limiting settings
|
// CPU Priority settings
|
||||||
const cpuLimitSettings = settings.cpuLimit || {};
|
const niceSettings = settings.nice || {};
|
||||||
document.getElementById('appSettingsCpuLimitEnabled').checked = cpuLimitSettings.enabled ?? false;
|
document.getElementById('appSettingsNiceEnabled').checked = niceSettings.enabled ?? false;
|
||||||
document.getElementById('appSettingsCpuNiceValue').value = cpuLimitSettings.niceValue ?? 10;
|
document.getElementById('appSettingsNiceValue').value = niceSettings.niceValue ?? 10;
|
||||||
document.getElementById('appSettingsCpuLimitPercent').value = cpuLimitSettings.cpuLimitPercent ?? 80;
|
|
||||||
// Check cpulimit availability and update status indicator
|
|
||||||
this.checkCpulimitAvailability();
|
|
||||||
// Notification settings
|
// Notification settings
|
||||||
const notifPrefs = this.notificationManager?.preferences || {};
|
const notifPrefs = this.notificationManager?.preferences || {};
|
||||||
document.getElementById('appSettingsNotifEnabled').checked = notifPrefs.enabled ?? true;
|
document.getElementById('appSettingsNotifEnabled').checked = notifPrefs.enabled ?? true;
|
||||||
@@ -4954,12 +4951,10 @@ class ClaudemanApp {
|
|||||||
// Claude CLI settings
|
// Claude CLI settings
|
||||||
claudeMode: document.getElementById('appSettingsClaudeMode').value,
|
claudeMode: document.getElementById('appSettingsClaudeMode').value,
|
||||||
allowedTools: document.getElementById('appSettingsAllowedTools').value.trim(),
|
allowedTools: document.getElementById('appSettingsAllowedTools').value.trim(),
|
||||||
// CPU Limiting settings
|
// CPU Priority settings
|
||||||
cpuLimit: {
|
nice: {
|
||||||
enabled: document.getElementById('appSettingsCpuLimitEnabled').checked,
|
enabled: document.getElementById('appSettingsNiceEnabled').checked,
|
||||||
niceValue: parseInt(document.getElementById('appSettingsCpuNiceValue').value) || 10,
|
niceValue: parseInt(document.getElementById('appSettingsNiceValue').value) || 10,
|
||||||
cpuLimitPercent: parseInt(document.getElementById('appSettingsCpuLimitPercent').value) || 80,
|
|
||||||
useCpulimitIfAvailable: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -5021,36 +5016,6 @@ class ClaudemanApp {
|
|||||||
return {};
|
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() {
|
applyHeaderVisibilitySettings() {
|
||||||
const settings = this.loadAppSettingsFromStorage();
|
const settings = this.loadAppSettingsFromStorage();
|
||||||
// Default all to true (enabled) if not set
|
// Default all to true (enabled) if not set
|
||||||
|
|||||||
@@ -782,29 +782,21 @@
|
|||||||
</label>
|
</label>
|
||||||
<span class="form-hint">Auto-enable for new sessions (otherwise auto-enables on Ralph pattern detection)</span>
|
<span class="form-hint">Auto-enable for new sessions (otherwise auto-enables on Ralph pattern detection)</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- CPU Limiting Section -->
|
<!-- Nice Priority Section -->
|
||||||
<div class="form-section-header">CPU Limiting</div>
|
<div class="form-section-header">Nice Priority</div>
|
||||||
<div class="form-row form-row-switch">
|
<div class="form-row form-row-switch">
|
||||||
<label>Enable CPU Limiting</label>
|
<label>Enable Nice Priority Reduction</label>
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input type="checkbox" id="appSettingsCpuLimitEnabled">
|
<input type="checkbox" id="appSettingsNiceEnabled">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
<span class="form-hint">Limit CPU usage for Claude sessions (reduces system impact, only affects new sessions)</span>
|
<span class="form-hint">Lower priority of Claude sessions (reduces system impact, only affects new sessions)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" id="cpuLimitNiceRow">
|
<div class="form-row" id="niceValueRow">
|
||||||
<label>Nice Value</label>
|
<label>Nice Value</label>
|
||||||
<input type="number" id="appSettingsCpuNiceValue" min="-20" max="19" value="10" style="width: 80px;">
|
<input type="number" id="appSettingsNiceValue" min="-20" max="19" value="10" style="width: 80px;">
|
||||||
<span class="form-hint">Process priority (-20 to 19, higher = lower priority, default: 10)</span>
|
<span class="form-hint">Process priority (-20 to 19, higher = lower priority, default: 10)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" id="cpuLimitPercentRow">
|
|
||||||
<label>CPU Limit %</label>
|
|
||||||
<div class="settings-item-actions">
|
|
||||||
<input type="number" id="appSettingsCpuLimitPercent" min="1" max="100" value="80" style="width: 80px;">
|
|
||||||
<span class="cpu-limit-status" id="cpuLimitStatus" title="cpulimit availability status"></span>
|
|
||||||
</div>
|
|
||||||
<span class="form-hint">Hard CPU limit (requires cpulimit installed, default: 80%)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Paths Tab -->
|
<!-- Paths Tab -->
|
||||||
<div class="modal-tab-content hidden" id="settings-paths">
|
<div class="modal-tab-content hidden" id="settings-paths">
|
||||||
|
|||||||
+29
-46
@@ -24,7 +24,7 @@ import { fileStreamManager } from '../file-stream-manager.js';
|
|||||||
import { RespawnController, RespawnConfig, RespawnState } from '../respawn-controller.js';
|
import { RespawnController, RespawnConfig, RespawnState } from '../respawn-controller.js';
|
||||||
import { SpawnOrchestrator, type SessionCreator } from '../spawn-orchestrator.js';
|
import { SpawnOrchestrator, type SessionCreator } from '../spawn-orchestrator.js';
|
||||||
import type { SpawnOrchestratorConfig } from '../spawn-types.js';
|
import type { SpawnOrchestratorConfig } from '../spawn-types.js';
|
||||||
import { ScreenManager, cpulimitAvailable } from '../screen-manager.js';
|
import { ScreenManager } from '../screen-manager.js';
|
||||||
import { getStore } from '../state-store.js';
|
import { getStore } from '../state-store.js';
|
||||||
import { generateClaudeMd } from '../templates/claude-md.js';
|
import { generateClaudeMd } from '../templates/claude-md.js';
|
||||||
import { parseRalphLoopConfig, extractCompletionPhrase } from '../ralph-config.js';
|
import { parseRalphLoopConfig, extractCompletionPhrase } from '../ralph-config.js';
|
||||||
@@ -56,8 +56,8 @@ import {
|
|||||||
type QuickStartResponse,
|
type QuickStartResponse,
|
||||||
type CaseInfo,
|
type CaseInfo,
|
||||||
type PersistedRespawnConfig,
|
type PersistedRespawnConfig,
|
||||||
type CpuLimitConfig,
|
type NiceConfig,
|
||||||
DEFAULT_CPU_LIMIT_CONFIG,
|
DEFAULT_NICE_CONFIG,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -526,14 +526,14 @@ export class WebServer extends EventEmitter {
|
|||||||
|
|
||||||
const body = req.body as CreateSessionRequest & { mode?: 'claude' | 'shell'; name?: string };
|
const body = req.body as CreateSessionRequest & { mode?: 'claude' | 'shell'; name?: string };
|
||||||
const workingDir = body.workingDir || process.cwd();
|
const workingDir = body.workingDir || process.cwd();
|
||||||
const globalCpuLimit = this.getGlobalCpuLimitConfig();
|
const globalNice = this.getGlobalNiceConfig();
|
||||||
const session = new Session({
|
const session = new Session({
|
||||||
workingDir,
|
workingDir,
|
||||||
mode: body.mode || 'claude',
|
mode: body.mode || 'claude',
|
||||||
name: body.name || '',
|
name: body.name || '',
|
||||||
screenManager: this.screenManager,
|
screenManager: this.screenManager,
|
||||||
useScreen: true,
|
useScreen: true,
|
||||||
cpuLimitConfig: globalCpuLimit,
|
niceConfig: globalNice,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sessions.set(session.id, session);
|
this.sessions.set(session.id, session);
|
||||||
@@ -2112,14 +2112,14 @@ export class WebServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new session with the case as working directory
|
// Create a new session with the case as working directory
|
||||||
// Apply global CPU limit config if enabled in settings
|
// Apply global Nice priority config if enabled in settings
|
||||||
const cpuLimitConfig = this.getGlobalCpuLimitConfig();
|
const niceConfig = this.getGlobalNiceConfig();
|
||||||
const session = new Session({
|
const session = new Session({
|
||||||
workingDir: casePath,
|
workingDir: casePath,
|
||||||
screenManager: this.screenManager,
|
screenManager: this.screenManager,
|
||||||
useScreen: true,
|
useScreen: true,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
cpuLimitConfig: cpuLimitConfig,
|
niceConfig: niceConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
|
// Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
|
||||||
@@ -2392,17 +2392,9 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ CPU Limit Endpoints ============
|
// ============ CPU Priority Endpoints ============
|
||||||
|
|
||||||
// Get CPU limit system status (whether cpulimit is installed)
|
// Get Nice priority config for a session
|
||||||
this.app.get('/api/system/cpu-limit-status', async () => {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
cpulimitAvailable: cpulimitAvailable(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get CPU limit config for a session
|
|
||||||
this.app.get('/api/sessions/:id/cpu-limit', async (req) => {
|
this.app.get('/api/sessions/:id/cpu-limit', async (req) => {
|
||||||
const { id } = req.params as { id: string };
|
const { id } = req.params as { id: string };
|
||||||
const session = this.sessions.get(id);
|
const session = this.sessions.get(id);
|
||||||
@@ -2411,12 +2403,11 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
cpuLimit: session.cpuLimitConfig,
|
nice: session.niceConfig,
|
||||||
cpulimitAvailable: cpulimitAvailable(),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update CPU limit config for a session
|
// Update Nice priority config for a session
|
||||||
// Note: Changes only apply to NEW sessions, not running ones
|
// Note: Changes only apply to NEW sessions, not running ones
|
||||||
this.app.post('/api/sessions/:id/cpu-limit', async (req) => {
|
this.app.post('/api/sessions/:id/cpu-limit', async (req) => {
|
||||||
const { id } = req.params as { id: string };
|
const { id } = req.params as { id: string };
|
||||||
@@ -2425,7 +2416,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
|||||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = req.body as Partial<CpuLimitConfig>;
|
const body = req.body as Partial<NiceConfig>;
|
||||||
|
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
if (body.niceValue !== undefined) {
|
if (body.niceValue !== undefined) {
|
||||||
@@ -2433,20 +2424,15 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
|||||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Nice value must be between -20 and 19');
|
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Nice value must be between -20 and 19');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (body.cpuLimitPercent !== undefined) {
|
|
||||||
if (typeof body.cpuLimitPercent !== 'number' || body.cpuLimitPercent < 1 || body.cpuLimitPercent > 100) {
|
|
||||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'CPU limit percent must be between 1 and 100');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
session.setCpuLimit(body);
|
session.setNice(body);
|
||||||
this.persistSessionState(session);
|
this.persistSessionState(session);
|
||||||
this.broadcast('session:updated', { session: session.toDetailedState() });
|
this.broadcast('session:updated', { session: session.toDetailedState() });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
cpuLimit: session.cpuLimitConfig,
|
nice: session.niceConfig,
|
||||||
note: 'CPU limiting only affects newly created screen sessions, not currently running ones.',
|
note: 'Nice priority only affects newly created screen sessions, not currently running ones.',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3319,14 +3305,14 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
|||||||
private setupSpawnOrchestratorListeners(): void {
|
private setupSpawnOrchestratorListeners(): void {
|
||||||
const sessionCreator: SessionCreator = {
|
const sessionCreator: SessionCreator = {
|
||||||
createAgentSession: async (workingDir: string, name: string) => {
|
createAgentSession: async (workingDir: string, name: string) => {
|
||||||
const globalCpuLimit = this.getGlobalCpuLimitConfig();
|
const globalNice = this.getGlobalNiceConfig();
|
||||||
const session = new Session({
|
const session = new Session({
|
||||||
workingDir,
|
workingDir,
|
||||||
screenManager: this.screenManager,
|
screenManager: this.screenManager,
|
||||||
useScreen: true,
|
useScreen: true,
|
||||||
mode: 'claude',
|
mode: 'claude',
|
||||||
name: `spawn:${name}`,
|
name: `spawn:${name}`,
|
||||||
cpuLimitConfig: globalCpuLimit,
|
niceConfig: globalNice,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sessions.set(session.id, session);
|
this.sessions.set(session.id, session);
|
||||||
@@ -3510,25 +3496,23 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to get global CPU limit config from settings
|
// Helper to get global Nice priority config from settings
|
||||||
private getGlobalCpuLimitConfig(): CpuLimitConfig | undefined {
|
private getGlobalNiceConfig(): NiceConfig | undefined {
|
||||||
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
|
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (existsSync(settingsPath)) {
|
if (existsSync(settingsPath)) {
|
||||||
const content = readFileSync(settingsPath, 'utf-8');
|
const content = readFileSync(settingsPath, 'utf-8');
|
||||||
const settings = JSON.parse(content);
|
const settings = JSON.parse(content);
|
||||||
if (settings.cpuLimit && settings.cpuLimit.enabled) {
|
if (settings.nice && settings.nice.enabled) {
|
||||||
return {
|
return {
|
||||||
enabled: settings.cpuLimit.enabled ?? false,
|
enabled: settings.nice.enabled ?? false,
|
||||||
niceValue: settings.cpuLimit.niceValue ?? DEFAULT_CPU_LIMIT_CONFIG.niceValue,
|
niceValue: settings.nice.niceValue ?? DEFAULT_NICE_CONFIG.niceValue,
|
||||||
cpuLimitPercent: settings.cpuLimit.cpuLimitPercent ?? DEFAULT_CPU_LIMIT_CONFIG.cpuLimitPercent,
|
|
||||||
useCpulimitIfAvailable: settings.cpuLimit.useCpulimitIfAvailable ?? DEFAULT_CPU_LIMIT_CONFIG.useCpulimitIfAvailable,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to read CPU limit settings:', err);
|
console.error('Failed to read Nice priority settings:', err);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -4128,12 +4112,11 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
|||||||
}
|
}
|
||||||
console.log(`[Server] Restored Ralph tracker for session ${session.id} (phrase: ${savedState.ralphCompletionPhrase || 'none'})`);
|
console.log(`[Server] Restored Ralph tracker for session ${session.id} (phrase: ${savedState.ralphCompletionPhrase || 'none'})`);
|
||||||
}
|
}
|
||||||
// CPU limit config
|
// Nice priority config
|
||||||
if (savedState.cpuLimitEnabled !== undefined) {
|
if (savedState.niceEnabled !== undefined) {
|
||||||
session.setCpuLimit({
|
session.setNice({
|
||||||
enabled: savedState.cpuLimitEnabled,
|
enabled: savedState.niceEnabled,
|
||||||
niceValue: savedState.cpuLimitNiceValue,
|
niceValue: savedState.niceValue,
|
||||||
cpuLimitPercent: savedState.cpuLimitPercent,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Respawn controller
|
// Respawn controller
|
||||||
|
|||||||
Reference in New Issue
Block a user