mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
refactor: extract magic numbers to named constants
Low severity code quality improvements: - session.ts: Add timing constants (SCREEN_STARTUP_DELAY_MS, etc.) - screen-manager.ts: Add EXEC_TIMEOUT_MS, CR_MAX_ATTEMPTS, etc. - subagent-watcher.ts: Add display length constants - server.ts: Add STATS_COLLECTION_INTERVAL_MS, SESSION_LIMIT_WAIT_MS, etc. - spawn-orchestrator.ts: Add budget ratio constants - respawn-controller.ts: Use pre-compiled ANSI_ESCAPE_PATTERN - state-store.ts: Use ES6 import for unlinkSync instead of dynamic require - app.js: Add notification timing constants Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ When user says "COM": 1) Increment version in BOTH `package.json` AND `CLAUDE.md
|
|||||||
|
|
||||||
Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs.
|
Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs.
|
||||||
|
|
||||||
**Version**: 0.1383 (must match `package.json`)
|
**Version**: 0.1384 (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.1383",
|
"version": "0.1384",
|
||||||
"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",
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ const PLAN_MODE_OPTION_PATTERN = /\d+\.\s+(Yes|No|Type|Cancel|Skip|Proceed|Appro
|
|||||||
/** Pre-filter: selection indicator arrow for plan mode detection */
|
/** Pre-filter: selection indicator arrow for plan mode detection */
|
||||||
const PLAN_MODE_SELECTOR_PATTERN = /[❯>]\s*\d+\./;
|
const PLAN_MODE_SELECTOR_PATTERN = /[❯>]\s*\d+\./;
|
||||||
|
|
||||||
|
/** Pattern to strip ANSI escape codes from terminal output */
|
||||||
|
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
|
||||||
|
|
||||||
// Note: The old '↵ send' indicator is no longer reliable in Claude Code 2024+
|
// Note: The old '↵ send' indicator is no longer reliable in Claude Code 2024+
|
||||||
// Detection now uses completion message patterns ("for Xm Xs") instead.
|
// Detection now uses completion message patterns ("for Xm Xs") instead.
|
||||||
|
|
||||||
@@ -1277,7 +1280,8 @@ export class RespawnController extends EventEmitter {
|
|||||||
// This prevents false triggers when Claude pauses briefly mid-work.
|
// This prevents false triggers when Claude pauses briefly mid-work.
|
||||||
if (this._state === 'confirming_idle' || this._state === 'ai_checking') {
|
if (this._state === 'confirming_idle' || this._state === 'ai_checking') {
|
||||||
// Strip ANSI escape codes to check if there's real content
|
// Strip ANSI escape codes to check if there's real content
|
||||||
const stripped = data.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').trim();
|
ANSI_ESCAPE_PATTERN.lastIndex = 0;
|
||||||
|
const stripped = data.replace(ANSI_ESCAPE_PATTERN, '').trim();
|
||||||
if (stripped.length > 2) {
|
if (stripped.length > 2) {
|
||||||
if (this._state === 'ai_checking') {
|
if (this._state === 'ai_checking') {
|
||||||
this.log(`Substantial output during AI check ("${stripped.substring(0, 40)}..."), cancelling`);
|
this.log(`Substantial output during AI check ("${stripped.substring(0, 40)}..."), cancelling`);
|
||||||
@@ -1924,7 +1928,8 @@ export class RespawnController extends EventEmitter {
|
|||||||
const tail = buffer.slice(-2000);
|
const tail = buffer.slice(-2000);
|
||||||
|
|
||||||
// Strip ANSI codes for pattern matching
|
// Strip ANSI codes for pattern matching
|
||||||
const stripped = tail.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
ANSI_ESCAPE_PATTERN.lastIndex = 0;
|
||||||
|
const stripped = tail.replace(ANSI_ESCAPE_PATTERN, '');
|
||||||
|
|
||||||
// Must find numbered option pattern
|
// Must find numbered option pattern
|
||||||
if (!PLAN_MODE_OPTION_PATTERN.test(stripped)) return false;
|
if (!PLAN_MODE_OPTION_PATTERN.test(stripped)) return false;
|
||||||
|
|||||||
+42
-20
@@ -34,6 +34,28 @@ const CLAUDE_SEARCH_DIRS = [
|
|||||||
`${homedir()}/bin`,
|
`${homedir()}/bin`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Timing Constants
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** Timeout for exec commands (5 seconds) */
|
||||||
|
const EXEC_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
/** Delay after screen creation (500ms) */
|
||||||
|
const SCREEN_CREATION_WAIT_MS = 500;
|
||||||
|
|
||||||
|
/** Delay after screen kill command (200ms) */
|
||||||
|
const SCREEN_KILL_WAIT_MS = 200;
|
||||||
|
|
||||||
|
/** Delay for graceful shutdown (100ms) */
|
||||||
|
const GRACEFUL_SHUTDOWN_WAIT_MS = 100;
|
||||||
|
|
||||||
|
/** Default stats collection interval (2 seconds) */
|
||||||
|
const DEFAULT_STATS_INTERVAL_MS = 2000;
|
||||||
|
|
||||||
|
/** Maximum retry attempts for carriage return (3) */
|
||||||
|
const CR_MAX_ATTEMPTS = 3;
|
||||||
|
|
||||||
/** Cached directory containing the claude binary */
|
/** Cached directory containing the claude binary */
|
||||||
let _claudeDir: string | null = null;
|
let _claudeDir: string | null = null;
|
||||||
|
|
||||||
@@ -45,7 +67,7 @@ function findClaudeDir(): string | null {
|
|||||||
if (_claudeDir !== null) return _claudeDir;
|
if (_claudeDir !== null) return _claudeDir;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim();
|
const result = execSync('which claude', { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }).trim();
|
||||||
if (result && existsSync(result)) {
|
if (result && existsSync(result)) {
|
||||||
_claudeDir = dirname(result);
|
_claudeDir = dirname(result);
|
||||||
return _claudeDir;
|
return _claudeDir;
|
||||||
@@ -234,7 +256,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
screenProcess.unref();
|
screenProcess.unref();
|
||||||
|
|
||||||
// Wait a moment for screen to start
|
// Wait a moment for screen to start
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, SCREEN_CREATION_WAIT_MS));
|
||||||
|
|
||||||
// Get the PID of the screen session
|
// Get the PID of the screen session
|
||||||
const pid = this.getScreenPid(screenName);
|
const pid = this.getScreenPid(screenName);
|
||||||
@@ -276,7 +298,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
const escapedName = shellEscape(screenName);
|
const escapedName = shellEscape(screenName);
|
||||||
const output = execSync(`screen -ls | grep "${escapedName}"`, {
|
const output = execSync(`screen -ls | grep "${escapedName}"`, {
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
timeout: 5000
|
timeout: EXEC_TIMEOUT_MS
|
||||||
});
|
});
|
||||||
// Output format: "12345.claudeman-abc12345 (Detached)"
|
// Output format: "12345.claudeman-abc12345 (Detached)"
|
||||||
const match = output.match(/(\d+)\./);
|
const match = output.match(/(\d+)\./);
|
||||||
@@ -292,7 +314,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
const output = execSync(`pgrep -P ${pid}`, {
|
const output = execSync(`pgrep -P ${pid}`, {
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
timeout: 5000
|
timeout: EXEC_TIMEOUT_MS
|
||||||
}).trim();
|
}).trim();
|
||||||
if (output) {
|
if (output) {
|
||||||
for (const childPid of output.split('\n').map(p => parseInt(p, 10)).filter(p => !isNaN(p))) {
|
for (const childPid of output.split('\n').map(p => parseInt(p, 10)).filter(p => !isNaN(p))) {
|
||||||
@@ -334,7 +356,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Give processes a moment to terminate gracefully
|
// Give processes a moment to terminate gracefully
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
await new Promise(resolve => setTimeout(resolve, SCREEN_KILL_WAIT_MS));
|
||||||
|
|
||||||
// Force kill any remaining children
|
// Force kill any remaining children
|
||||||
for (const childPid of childPids) {
|
for (const childPid of childPids) {
|
||||||
@@ -349,7 +371,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
// Strategy 2: Kill the entire process group (catches any orphans we missed)
|
// Strategy 2: Kill the entire process group (catches any orphans we missed)
|
||||||
try {
|
try {
|
||||||
process.kill(-currentPid, 'SIGTERM');
|
process.kill(-currentPid, 'SIGTERM');
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, GRACEFUL_SHUTDOWN_WAIT_MS));
|
||||||
process.kill(-currentPid, 'SIGKILL');
|
process.kill(-currentPid, 'SIGKILL');
|
||||||
} catch {
|
} catch {
|
||||||
// Process group may not exist or already terminated
|
// Process group may not exist or already terminated
|
||||||
@@ -358,7 +380,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
// Strategy 3: Kill screen session by name
|
// Strategy 3: Kill screen session by name
|
||||||
try {
|
try {
|
||||||
execSync(`screen -S ${screen.screenName} -X quit`, {
|
execSync(`screen -S ${screen.screenName} -X quit`, {
|
||||||
timeout: 5000
|
timeout: EXEC_TIMEOUT_MS
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// Screen may already be dead
|
// Screen may already be dead
|
||||||
@@ -425,7 +447,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
const output = execSync('screen -ls 2>/dev/null || true', {
|
const output = execSync('screen -ls 2>/dev/null || true', {
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
timeout: 5000
|
timeout: EXEC_TIMEOUT_MS
|
||||||
});
|
});
|
||||||
// Match: "12345.claudeman-abc12345 (Detached)" or similar
|
// Match: "12345.claudeman-abc12345 (Detached)" or similar
|
||||||
// Reset lastIndex since we're reusing the global regex
|
// Reset lastIndex since we're reusing the global regex
|
||||||
@@ -485,7 +507,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
// Get memory and CPU usage using ps
|
// Get memory and CPU usage using ps
|
||||||
const psOutput = execSync(
|
const psOutput = execSync(
|
||||||
`ps -o rss=,pcpu= -p ${screen.pid} 2>/dev/null || echo "0 0"`,
|
`ps -o rss=,pcpu= -p ${screen.pid} 2>/dev/null || echo "0 0"`,
|
||||||
{ encoding: 'utf-8', timeout: 5000 }
|
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
const [rss, cpu] = psOutput.split(/\s+/).map(x => parseFloat(x) || 0);
|
const [rss, cpu] = psOutput.split(/\s+/).map(x => parseFloat(x) || 0);
|
||||||
@@ -495,7 +517,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
const childOutput = execSync(
|
const childOutput = execSync(
|
||||||
`pgrep -P ${screen.pid} | wc -l`,
|
`pgrep -P ${screen.pid} | wc -l`,
|
||||||
{ encoding: 'utf-8', timeout: 5000 }
|
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||||
).trim();
|
).trim();
|
||||||
childCount = parseInt(childOutput, 10) || 0;
|
childCount = parseInt(childOutput, 10) || 0;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -528,7 +550,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
// Single ps call for all PIDs
|
// Single ps call for all PIDs
|
||||||
const psOutput = execSync(
|
const psOutput = execSync(
|
||||||
`ps -o pid=,rss=,pcpu= -p ${pids.join(',')} 2>/dev/null || true`,
|
`ps -o pid=,rss=,pcpu= -p ${pids.join(',')} 2>/dev/null || true`,
|
||||||
{ encoding: 'utf-8', timeout: 5000 }
|
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
// Parse output - each line: "PID RSS CPU"
|
// Parse output - each line: "PID RSS CPU"
|
||||||
@@ -552,7 +574,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
// Batch child count query - single pgrep call
|
// Batch child count query - single pgrep call
|
||||||
const pgrepOutput = execSync(
|
const pgrepOutput = execSync(
|
||||||
`for p in ${pids.join(' ')}; do echo "$p $(pgrep -P $p 2>/dev/null | wc -l)"; done`,
|
`for p in ${pids.join(' ')}; do echo "$p $(pgrep -P $p 2>/dev/null | wc -l)"; done`,
|
||||||
{ encoding: 'utf-8', timeout: 5000 }
|
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
for (const line of pgrepOutput.split('\n')) {
|
for (const line of pgrepOutput.split('\n')) {
|
||||||
@@ -582,7 +604,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start periodic stats collection
|
// Start periodic stats collection
|
||||||
startStatsCollection(intervalMs: number = 2000): void {
|
startStatsCollection(intervalMs: number = DEFAULT_STATS_INTERVAL_MS): void {
|
||||||
if (this.statsInterval) {
|
if (this.statsInterval) {
|
||||||
clearInterval(this.statsInterval);
|
clearInterval(this.statsInterval);
|
||||||
}
|
}
|
||||||
@@ -646,7 +668,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
// Check if screen is available on the system
|
// Check if screen is available on the system
|
||||||
static isScreenAvailable(): boolean {
|
static isScreenAvailable(): boolean {
|
||||||
try {
|
try {
|
||||||
execSync('which screen', { encoding: 'utf-8', timeout: 5000 });
|
execSync('which screen', { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS });
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
@@ -681,7 +703,7 @@ export class ScreenManager extends EventEmitter {
|
|||||||
// Send text first (if any)
|
// Send text first (if any)
|
||||||
if (escapedText) {
|
if (escapedText) {
|
||||||
const textCmd = `screen -S ${screen.screenName} -p 0 -X stuff "${escapedText}"`;
|
const textCmd = `screen -S ${screen.screenName} -p 0 -X stuff "${escapedText}"`;
|
||||||
execSync(textCmd, { encoding: 'utf-8', timeout: 5000 });
|
execSync(textCmd, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send carriage return separately (Enter key for Ink)
|
// Send carriage return separately (Enter key for Ink)
|
||||||
@@ -696,15 +718,15 @@ export class ScreenManager extends EventEmitter {
|
|||||||
|
|
||||||
const crCmd = `screen -S ${screen.screenName} -p 0 -X stuff "$(printf '\\015')"`;
|
const crCmd = `screen -S ${screen.screenName} -p 0 -X stuff "$(printf '\\015')"`;
|
||||||
|
|
||||||
// Try up to 3 times with increasing delays
|
// Try up to CR_MAX_ATTEMPTS times with increasing delays
|
||||||
let success = false;
|
let success = false;
|
||||||
for (let attempt = 1; attempt <= 3 && !success; attempt++) {
|
for (let attempt = 1; attempt <= CR_MAX_ATTEMPTS && !success; attempt++) {
|
||||||
try {
|
try {
|
||||||
execSync(crCmd, { encoding: 'utf-8', timeout: 5000 });
|
execSync(crCmd, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS });
|
||||||
success = true;
|
success = true;
|
||||||
} catch (crErr) {
|
} catch (crErr) {
|
||||||
console.warn(`[ScreenManager] Carriage return attempt ${attempt}/3 failed`);
|
console.warn(`[ScreenManager] Carriage return attempt ${attempt}/${CR_MAX_ATTEMPTS} failed`);
|
||||||
if (attempt < 3) {
|
if (attempt < CR_MAX_ATTEMPTS) {
|
||||||
execSync(`sleep 0.${attempt}`, { timeout: 1000 }); // 0.1s, 0.2s delays
|
execSync(`sleep 0.${attempt}`, { timeout: 1000 }); // 0.1s, 0.2s delays
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-11
@@ -56,6 +56,27 @@ const MAX_LINE_BUFFER_SIZE = 64 * 1024;
|
|||||||
/** Line buffer flush interval (100ms) - forces processing of partial lines */
|
/** Line buffer flush interval (100ms) - forces processing of partial lines */
|
||||||
const LINE_BUFFER_FLUSH_INTERVAL = 100;
|
const LINE_BUFFER_FLUSH_INTERVAL = 100;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Timing Constants
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** Timeout for exec commands like 'which claude' (5 seconds) */
|
||||||
|
const EXEC_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
/** Delay after screen creation before sending commands (300ms) */
|
||||||
|
const SCREEN_STARTUP_DELAY_MS = 300;
|
||||||
|
|
||||||
|
/** Delay before declaring session idle after last output (2 seconds) */
|
||||||
|
const IDLE_DETECTION_DELAY_MS = 2000;
|
||||||
|
|
||||||
|
/** Delay for auto-compact/clear retry attempts (2 seconds) */
|
||||||
|
const AUTO_RETRY_DELAY_MS = 2000;
|
||||||
|
|
||||||
|
/** Delay for auto-compact/clear initial check (1 second) */
|
||||||
|
const AUTO_INITIAL_DELAY_MS = 1000;
|
||||||
|
|
||||||
|
/** Graceful shutdown delay when stopping session (100ms) */
|
||||||
|
const GRACEFUL_SHUTDOWN_DELAY_MS = 100;
|
||||||
|
|
||||||
// Filter out terminal focus escape sequences (focus in/out reports)
|
// Filter out terminal focus escape sequences (focus in/out reports)
|
||||||
// ^[[I (focus in), ^[[O (focus out), and the enable/disable sequences
|
// ^[[I (focus in), ^[[O (focus out), and the enable/disable sequences
|
||||||
@@ -106,7 +127,7 @@ export function getAugmentedPath(): string {
|
|||||||
|
|
||||||
// Try `which` first (respects current PATH)
|
// Try `which` first (respects current PATH)
|
||||||
try {
|
try {
|
||||||
const result = execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim();
|
const result = execSync('which claude', { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }).trim();
|
||||||
if (result && existsSync(result)) {
|
if (result && existsSync(result)) {
|
||||||
claudeDir = dirname(result);
|
claudeDir = dirname(result);
|
||||||
}
|
}
|
||||||
@@ -813,7 +834,7 @@ export class Session extends EventEmitter {
|
|||||||
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
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
await new Promise(resolve => setTimeout(resolve, SCREEN_STARTUP_DELAY_MS));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach to the screen session via PTY
|
// Attach to the screen session via PTY
|
||||||
@@ -930,7 +951,7 @@ export class Session extends EventEmitter {
|
|||||||
this._lastPromptTime = Date.now();
|
this._lastPromptTime = Date.now();
|
||||||
this.emit('idle');
|
this.emit('idle');
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, IDLE_DETECTION_DELAY_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect when Claude starts working (thinking, writing, etc)
|
// Detect when Claude starts working (thinking, writing, etc)
|
||||||
@@ -1019,7 +1040,7 @@ export class Session extends EventEmitter {
|
|||||||
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
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
await new Promise(resolve => setTimeout(resolve, SCREEN_STARTUP_DELAY_MS));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach to the screen session via PTY
|
// Attach to the screen session via PTY
|
||||||
@@ -1559,16 +1580,16 @@ export class Session extends EventEmitter {
|
|||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check again in 2 seconds
|
// Check again after delay
|
||||||
if (!this._isStopped) {
|
if (!this._isStopped) {
|
||||||
this._autoCompactTimer = setTimeout(checkAndCompact, 2000);
|
this._autoCompactTimer = setTimeout(checkAndCompact, AUTO_RETRY_DELAY_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start checking after a short delay
|
// Start checking after a short delay
|
||||||
if (!this._isStopped) {
|
if (!this._isStopped) {
|
||||||
this._autoCompactTimer = setTimeout(checkAndCompact, 1000);
|
this._autoCompactTimer = setTimeout(checkAndCompact, AUTO_INITIAL_DELAY_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1603,16 +1624,16 @@ export class Session extends EventEmitter {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check again in 2 seconds
|
// Check again after delay
|
||||||
if (!this._isStopped) {
|
if (!this._isStopped) {
|
||||||
this._autoClearTimer = setTimeout(checkAndClear, 2000);
|
this._autoClearTimer = setTimeout(checkAndClear, AUTO_RETRY_DELAY_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start checking after a short delay
|
// Start checking after a short delay
|
||||||
if (!this._isStopped) {
|
if (!this._isStopped) {
|
||||||
this._autoClearTimer = setTimeout(checkAndClear, 1000);
|
this._autoClearTimer = setTimeout(checkAndClear, AUTO_INITIAL_DELAY_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1811,7 +1832,7 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Give it a moment to terminate gracefully
|
// Give it a moment to terminate gracefully
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, GRACEFUL_SHUTDOWN_DELAY_MS));
|
||||||
|
|
||||||
// Force kill with SIGKILL if still alive
|
// Force kill with SIGKILL if still alive
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -44,6 +44,23 @@ import {
|
|||||||
import { generateAgentClaudeMd, buildInitialPrompt } from './spawn-claude-md.js';
|
import { generateAgentClaudeMd, buildInitialPrompt } from './spawn-claude-md.js';
|
||||||
import { getErrorMessage } from './types.js';
|
import { getErrorMessage } from './types.js';
|
||||||
|
|
||||||
|
// ========== Local Constants ==========
|
||||||
|
|
||||||
|
/** UUID truncation length for fallback agent IDs */
|
||||||
|
const UUID_TRUNCATE_LENGTH = 8;
|
||||||
|
|
||||||
|
/** Message sequence number padding length */
|
||||||
|
const MESSAGE_SEQUENCE_PAD_LENGTH = 3;
|
||||||
|
|
||||||
|
/** Timeout warning threshold (90% of timeout) */
|
||||||
|
const TIMEOUT_WARNING_RATIO = 0.9;
|
||||||
|
|
||||||
|
/** Budget hard limit ratio (110% - force stop) */
|
||||||
|
const BUDGET_HARD_LIMIT_RATIO = 1.1;
|
||||||
|
|
||||||
|
/** Budget soft limit ratio (100% - warning) */
|
||||||
|
const BUDGET_SOFT_LIMIT_RATIO = 1.0;
|
||||||
|
|
||||||
// ========== Types for integration ==========
|
// ========== Types for integration ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,7 +187,7 @@ export class SpawnOrchestrator extends EventEmitter {
|
|||||||
|
|
||||||
// Parse task file
|
// Parse task file
|
||||||
const content = readFileSync(resolvedPath, 'utf-8');
|
const content = readFileSync(resolvedPath, 'utf-8');
|
||||||
const fallbackId = `agent-${uuidv4().slice(0, 8)}`;
|
const fallbackId = `agent-${uuidv4().slice(0, UUID_TRUNCATE_LENGTH)}`;
|
||||||
const parsed = parseTaskSpecFile(content, fallbackId);
|
const parsed = parseTaskSpecFile(content, fallbackId);
|
||||||
|
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
@@ -293,7 +310,7 @@ export class SpawnOrchestrator extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seq = existingMessages.length + 1;
|
const seq = existingMessages.length + 1;
|
||||||
const seqStr = String(seq).padStart(3, '0');
|
const seqStr = String(seq).padStart(MESSAGE_SEQUENCE_PAD_LENGTH, '0');
|
||||||
const fileName = `${seqStr}-parent.md`;
|
const fileName = `${seqStr}-parent.md`;
|
||||||
|
|
||||||
const message: SpawnMessage = {
|
const message: SpawnMessage = {
|
||||||
@@ -466,7 +483,7 @@ export class SpawnOrchestrator extends EventEmitter {
|
|||||||
parentWorkingDir: string,
|
parentWorkingDir: string,
|
||||||
parentDepth: number = 0
|
parentDepth: number = 0
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const fallbackId = `agent-${uuidv4().slice(0, 8)}`;
|
const fallbackId = `agent-${uuidv4().slice(0, UUID_TRUNCATE_LENGTH)}`;
|
||||||
const parsed = parseTaskSpecFile(taskContent, fallbackId);
|
const parsed = parseTaskSpecFile(taskContent, fallbackId);
|
||||||
if (!parsed) return null;
|
if (!parsed) return null;
|
||||||
|
|
||||||
@@ -702,7 +719,7 @@ export class SpawnOrchestrator extends EventEmitter {
|
|||||||
const timeoutMs = agent.task.spec.timeoutMinutes * 60 * 1000;
|
const timeoutMs = agent.task.spec.timeoutMinutes * 60 * 1000;
|
||||||
|
|
||||||
// Warning at 90% - store timer for cleanup
|
// Warning at 90% - store timer for cleanup
|
||||||
const warningMs = timeoutMs * 0.9;
|
const warningMs = timeoutMs * TIMEOUT_WARNING_RATIO;
|
||||||
agent.warningTimer = setTimeout(() => {
|
agent.warningTimer = setTimeout(() => {
|
||||||
if (agent.status === 'running' && this._sessionCreator && agent.sessionId) {
|
if (agent.status === 'running' && this._sessionCreator && agent.sessionId) {
|
||||||
this._sessionCreator.writeToSession(
|
this._sessionCreator.writeToSession(
|
||||||
@@ -728,11 +745,11 @@ export class SpawnOrchestrator extends EventEmitter {
|
|||||||
const tokensUsed = this._sessionCreator.getSessionTokens(agent.sessionId);
|
const tokensUsed = this._sessionCreator.getSessionTokens(agent.sessionId);
|
||||||
const ratio = tokensUsed / agent.tokenBudget;
|
const ratio = tokensUsed / agent.tokenBudget;
|
||||||
|
|
||||||
if (ratio >= 1.1) {
|
if (ratio >= BUDGET_HARD_LIMIT_RATIO) {
|
||||||
// Force kill at 110%
|
// Force kill at 110%
|
||||||
this.handleAgentTimeout(agent);
|
this.handleAgentTimeout(agent);
|
||||||
return;
|
return;
|
||||||
} else if (ratio >= 1.0) {
|
} else if (ratio >= BUDGET_SOFT_LIMIT_RATIO) {
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
this._sessionCreator.writeToSession(
|
this._sessionCreator.writeToSession(
|
||||||
agent.sessionId,
|
agent.sessionId,
|
||||||
@@ -753,10 +770,10 @@ export class SpawnOrchestrator extends EventEmitter {
|
|||||||
const costUsed = this._sessionCreator.getSessionCost(agent.sessionId);
|
const costUsed = this._sessionCreator.getSessionCost(agent.sessionId);
|
||||||
const ratio = costUsed / agent.costBudget;
|
const ratio = costUsed / agent.costBudget;
|
||||||
|
|
||||||
if (ratio >= 1.1) {
|
if (ratio >= BUDGET_HARD_LIMIT_RATIO) {
|
||||||
this.handleAgentTimeout(agent);
|
this.handleAgentTimeout(agent);
|
||||||
return;
|
return;
|
||||||
} else if (ratio >= 1.0) {
|
} else if (ratio >= BUDGET_SOFT_LIMIT_RATIO) {
|
||||||
this._sessionCreator.writeToSession(
|
this._sessionCreator.writeToSession(
|
||||||
agent.sessionId,
|
agent.sessionId,
|
||||||
'You have exceeded your cost budget. Write your result.md NOW and output your completion phrase.\r'
|
'You have exceeded your cost budget. Write your result.md NOW and output your completion phrase.\r'
|
||||||
|
|||||||
+1
-3
@@ -14,7 +14,7 @@
|
|||||||
* @module state-store
|
* @module state-store
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from 'node:fs';
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { AppState, createInitialState, RalphSessionState, createInitialRalphSessionState, GlobalStats, createInitialGlobalStats, TokenStats, TokenUsageEntry } from './types.js';
|
import { AppState, createInitialState, RalphSessionState, createInitialRalphSessionState, GlobalStats, createInitialGlobalStats, TokenStats, TokenUsageEntry } from './types.js';
|
||||||
@@ -138,7 +138,6 @@ export class StateStore {
|
|||||||
// Try to clean up temp file on error
|
// Try to clean up temp file on error
|
||||||
try {
|
try {
|
||||||
if (existsSync(tempPath)) {
|
if (existsSync(tempPath)) {
|
||||||
const { unlinkSync } = require('node:fs');
|
|
||||||
unlinkSync(tempPath);
|
unlinkSync(tempPath);
|
||||||
}
|
}
|
||||||
} catch { /* ignore cleanup errors */ }
|
} catch { /* ignore cleanup errors */ }
|
||||||
@@ -479,7 +478,6 @@ export class StateStore {
|
|||||||
// Try to clean up temp file on error
|
// Try to clean up temp file on error
|
||||||
try {
|
try {
|
||||||
if (existsSync(tempPath)) {
|
if (existsSync(tempPath)) {
|
||||||
const { unlinkSync } = require('node:fs');
|
|
||||||
unlinkSync(tempPath);
|
unlinkSync(tempPath);
|
||||||
}
|
}
|
||||||
} catch { /* ignore cleanup errors */ }
|
} catch { /* ignore cleanup errors */ }
|
||||||
|
|||||||
+22
-13
@@ -125,6 +125,15 @@ const POLL_INTERVAL_MS = 1000; // Check for new files every second
|
|||||||
const LIVENESS_CHECK_MS = 10000; // Check if subagent processes are still alive every 10s
|
const LIVENESS_CHECK_MS = 10000; // Check if subagent processes are still alive every 10s
|
||||||
const STALE_AGENT_MAX_AGE_MS = 24 * 60 * 60 * 1000; // Remove completed agents older than 24 hours
|
const STALE_AGENT_MAX_AGE_MS = 24 * 60 * 60 * 1000; // Remove completed agents older than 24 hours
|
||||||
|
|
||||||
|
// Display/preview length constants
|
||||||
|
const TEXT_PREVIEW_LENGTH = 200; // Length for text previews in tool results
|
||||||
|
const USER_TEXT_PREVIEW_LENGTH = 80; // Length for user message previews
|
||||||
|
const SMART_TITLE_MAX_LENGTH = 45; // Max length for smart title extraction
|
||||||
|
const MESSAGE_TEXT_LIMIT = 500; // Max length for message text content
|
||||||
|
const COMMAND_DISPLAY_LENGTH = 60; // Max length for command display
|
||||||
|
const INPUT_TRUNCATE_LENGTH = 100; // Max length for input value truncation
|
||||||
|
const FILE_CONTENT_DEBOUNCE_MS = 100; // Debounce delay for file content updates
|
||||||
|
|
||||||
// ========== SubagentWatcher Class ==========
|
// ========== SubagentWatcher Class ==========
|
||||||
|
|
||||||
export class SubagentWatcher extends EventEmitter {
|
export class SubagentWatcher extends EventEmitter {
|
||||||
@@ -492,7 +501,7 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
if (typeof entry.message.content === 'string') {
|
if (typeof entry.message.content === 'string') {
|
||||||
const text = entry.message.content.trim();
|
const text = entry.message.content.trim();
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
const preview = text.length > 200 ? text.substring(0, 200) + '...' : text;
|
const preview = text.length > TEXT_PREVIEW_LENGTH ? text.substring(0, TEXT_PREVIEW_LENGTH) + '...' : text;
|
||||||
lines.push(`${this.formatTime(entry.timestamp)} 💬 ${preview.replace(/\n/g, ' ')}`);
|
lines.push(`${this.formatTime(entry.timestamp)} 💬 ${preview.replace(/\n/g, ' ')}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -502,7 +511,7 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
} else if (content.type === 'text' && content.text) {
|
} else if (content.type === 'text' && content.text) {
|
||||||
const text = content.text.trim();
|
const text = content.text.trim();
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
const preview = text.length > 200 ? text.substring(0, 200) + '...' : text;
|
const preview = text.length > TEXT_PREVIEW_LENGTH ? text.substring(0, TEXT_PREVIEW_LENGTH) + '...' : text;
|
||||||
lines.push(`${this.formatTime(entry.timestamp)} 💬 ${preview.replace(/\n/g, ' ')}`);
|
lines.push(`${this.formatTime(entry.timestamp)} 💬 ${preview.replace(/\n/g, ' ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,14 +522,14 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
if (typeof entry.message.content === 'string') {
|
if (typeof entry.message.content === 'string') {
|
||||||
const text = entry.message.content.trim();
|
const text = entry.message.content.trim();
|
||||||
if (text.length < 100 && !text.includes('{')) {
|
if (text.length < 100 && !text.includes('{')) {
|
||||||
lines.push(`${this.formatTime(entry.timestamp)} 📥 User: ${text.substring(0, 80)}`);
|
lines.push(`${this.formatTime(entry.timestamp)} 📥 User: ${text.substring(0, USER_TEXT_PREVIEW_LENGTH)}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const firstContent = entry.message.content[0];
|
const firstContent = entry.message.content[0];
|
||||||
if (firstContent?.type === 'text' && firstContent.text) {
|
if (firstContent?.type === 'text' && firstContent.text) {
|
||||||
const text = firstContent.text.trim();
|
const text = firstContent.text.trim();
|
||||||
if (text.length < 100 && !text.includes('{')) {
|
if (text.length < 100 && !text.includes('{')) {
|
||||||
lines.push(`${this.formatTime(entry.timestamp)} 📥 User: ${text.substring(0, 80)}`);
|
lines.push(`${this.formatTime(entry.timestamp)} 📥 User: ${text.substring(0, USER_TEXT_PREVIEW_LENGTH)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -544,7 +553,7 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
* Aims for ~40-50 chars that convey what the agent is doing
|
* Aims for ~40-50 chars that convey what the agent is doing
|
||||||
*/
|
*/
|
||||||
private extractSmartTitle(text: string): string {
|
private extractSmartTitle(text: string): string {
|
||||||
const MAX_LEN = 45;
|
const MAX_LEN = SMART_TITLE_MAX_LENGTH;
|
||||||
|
|
||||||
// Get first line/sentence
|
// Get first line/sentence
|
||||||
let title = text.split('\n')[0].trim();
|
let title = text.split('\n')[0].trim();
|
||||||
@@ -773,7 +782,7 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
if (existsSync(filePath)) {
|
if (existsSync(filePath)) {
|
||||||
this.watchAgentFile(filePath, projectHash, sessionId);
|
this.watchAgentFile(filePath, projectHash, sessionId);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, FILE_CONTENT_DEBOUNCE_MS);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1015,7 +1024,7 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
sessionId,
|
sessionId,
|
||||||
timestamp: entry.timestamp,
|
timestamp: entry.timestamp,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
text: text.substring(0, 500),
|
text: text.substring(0, MESSAGE_TEXT_LIMIT),
|
||||||
};
|
};
|
||||||
this.emit('subagent:message', message);
|
this.emit('subagent:message', message);
|
||||||
}
|
}
|
||||||
@@ -1060,7 +1069,7 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
timestamp: entry.timestamp,
|
timestamp: entry.timestamp,
|
||||||
toolUseId: content.tool_use_id,
|
toolUseId: content.tool_use_id,
|
||||||
tool: toolName,
|
tool: toolName,
|
||||||
preview: resultContent.substring(0, 500),
|
preview: resultContent.substring(0, MESSAGE_TEXT_LIMIT),
|
||||||
contentLength: resultContent.length,
|
contentLength: resultContent.length,
|
||||||
isError: content.is_error || false,
|
isError: content.is_error || false,
|
||||||
};
|
};
|
||||||
@@ -1073,7 +1082,7 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
sessionId,
|
sessionId,
|
||||||
timestamp: entry.timestamp,
|
timestamp: entry.timestamp,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
text: text.substring(0, 500), // Limit text length
|
text: text.substring(0, MESSAGE_TEXT_LIMIT), // Limit text length
|
||||||
};
|
};
|
||||||
this.emit('subagent:message', message);
|
this.emit('subagent:message', message);
|
||||||
}
|
}
|
||||||
@@ -1110,7 +1119,7 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
timestamp: entry.timestamp,
|
timestamp: entry.timestamp,
|
||||||
toolUseId: content.tool_use_id,
|
toolUseId: content.tool_use_id,
|
||||||
tool: toolName,
|
tool: toolName,
|
||||||
preview: resultContent.substring(0, 500),
|
preview: resultContent.substring(0, MESSAGE_TEXT_LIMIT),
|
||||||
contentLength: resultContent.length,
|
contentLength: resultContent.length,
|
||||||
isError: content.is_error || false,
|
isError: content.is_error || false,
|
||||||
};
|
};
|
||||||
@@ -1154,9 +1163,9 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
private getTruncatedInput(_tool: string, input: Record<string, unknown>): Record<string, unknown> {
|
private getTruncatedInput(_tool: string, input: Record<string, unknown>): Record<string, unknown> {
|
||||||
const truncated: Record<string, unknown> = {};
|
const truncated: Record<string, unknown> = {};
|
||||||
for (const [key, value] of Object.entries(input)) {
|
for (const [key, value] of Object.entries(input)) {
|
||||||
if (typeof value === 'string' && value.length > 100) {
|
if (typeof value === 'string' && value.length > INPUT_TRUNCATE_LENGTH) {
|
||||||
// Keep short preview of long strings
|
// Keep short preview of long strings
|
||||||
truncated[key] = value.substring(0, 100) + '...';
|
truncated[key] = value.substring(0, INPUT_TRUNCATE_LENGTH) + '...';
|
||||||
} else {
|
} else {
|
||||||
truncated[key] = value;
|
truncated[key] = value;
|
||||||
}
|
}
|
||||||
@@ -1212,7 +1221,7 @@ export class SubagentWatcher extends EventEmitter {
|
|||||||
details = input.file_path as string;
|
details = input.file_path as string;
|
||||||
} else if (name === 'Bash' && input.command) {
|
} else if (name === 'Bash' && input.command) {
|
||||||
const cmd = input.command as string;
|
const cmd = input.command as string;
|
||||||
details = cmd.length > 60 ? cmd.substring(0, 60) + '...' : cmd;
|
details = cmd.length > COMMAND_DISPLAY_LENGTH ? cmd.substring(0, COMMAND_DISPLAY_LENGTH) + '...' : cmd;
|
||||||
} else if (name === 'Glob' && input.pattern) {
|
} else if (name === 'Glob' && input.pattern) {
|
||||||
details = input.pattern as string;
|
details = input.pattern as string;
|
||||||
} else if (name === 'Grep' && input.pattern) {
|
} else if (name === 'Grep' && input.pattern) {
|
||||||
|
|||||||
+22
-4
@@ -1,7 +1,25 @@
|
|||||||
// Claudeman App - Tab-based Terminal UI
|
// Claudeman App - Tab-based Terminal UI
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Constants
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
// Default terminal scrollback (can be changed via settings)
|
// Default terminal scrollback (can be changed via settings)
|
||||||
const DEFAULT_SCROLLBACK = 5000;
|
const DEFAULT_SCROLLBACK = 5000;
|
||||||
|
|
||||||
|
// Timing constants
|
||||||
|
const STUCK_THRESHOLD_DEFAULT_MS = 600000; // 10 minutes - default for stuck detection
|
||||||
|
const GROUPING_TIMEOUT_MS = 5000; // 5 seconds - notification grouping window
|
||||||
|
const NOTIFICATION_LIST_CAP = 100; // Max notifications in list
|
||||||
|
const TITLE_FLASH_INTERVAL_MS = 1500; // Title flash rate
|
||||||
|
const BROWSER_NOTIF_RATE_LIMIT_MS = 3000; // Rate limit for browser notifications
|
||||||
|
const AUTO_CLOSE_NOTIFICATION_MS = 8000; // Auto-close browser notifications
|
||||||
|
const THROTTLE_DELAY_MS = 100; // General UI throttle delay
|
||||||
|
const TERMINAL_CHUNK_SIZE = 64 * 1024; // 64KB chunks for terminal data
|
||||||
|
const TERMINAL_TAIL_SIZE = 256 * 1024; // 256KB tail for initial load
|
||||||
|
const SYNC_WAIT_TIMEOUT_MS = 50; // Wait timeout for terminal sync
|
||||||
|
const STATS_POLLING_INTERVAL_MS = 2000; // System stats polling
|
||||||
|
|
||||||
// DEC mode 2026 - Synchronized Output
|
// DEC mode 2026 - Synchronized Output
|
||||||
// Wrap terminal writes with these markers to prevent partial-frame flicker.
|
// Wrap terminal writes with these markers to prevent partial-frame flicker.
|
||||||
// Terminal buffers all output between markers and renders atomically.
|
// Terminal buffers all output between markers and renders atomically.
|
||||||
@@ -98,7 +116,7 @@ class NotificationManager {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
browserNotifications: true,
|
browserNotifications: true,
|
||||||
audioAlerts: false,
|
audioAlerts: false,
|
||||||
stuckThresholdMs: 600000,
|
stuckThresholdMs: STUCK_THRESHOLD_DEFAULT_MS,
|
||||||
muteCritical: false,
|
muteCritical: false,
|
||||||
muteWarning: false,
|
muteWarning: false,
|
||||||
muteInfo: false,
|
muteInfo: false,
|
||||||
@@ -140,7 +158,7 @@ class NotificationManager {
|
|||||||
existing.notification.message = message;
|
existing.notification.message = message;
|
||||||
existing.notification.timestamp = Date.now();
|
existing.notification.timestamp = Date.now();
|
||||||
clearTimeout(existing.timeout);
|
clearTimeout(existing.timeout);
|
||||||
existing.timeout = setTimeout(() => this.groupingMap.delete(groupKey), 5000);
|
existing.timeout = setTimeout(() => this.groupingMap.delete(groupKey), GROUPING_TIMEOUT_MS);
|
||||||
this.scheduleRender();
|
this.scheduleRender();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -163,7 +181,7 @@ class NotificationManager {
|
|||||||
if (this.notifications.length > 100) this.notifications.pop();
|
if (this.notifications.length > 100) this.notifications.pop();
|
||||||
|
|
||||||
// Track for grouping
|
// Track for grouping
|
||||||
const timeout = setTimeout(() => this.groupingMap.delete(groupKey), 5000);
|
const timeout = setTimeout(() => this.groupingMap.delete(groupKey), GROUPING_TIMEOUT_MS);
|
||||||
this.groupingMap.set(groupKey, { notification, timeout });
|
this.groupingMap.set(groupKey, { notification, timeout });
|
||||||
|
|
||||||
// Update unread
|
// Update unread
|
||||||
@@ -236,7 +254,7 @@ class NotificationManager {
|
|||||||
document.title = this.titleFlashState
|
document.title = this.titleFlashState
|
||||||
? `\u26A0\uFE0F (${this.unreadCount}) Claudeman`
|
? `\u26A0\uFE0F (${this.unreadCount}) Claudeman`
|
||||||
: this.originalTitle;
|
: this.originalTitle;
|
||||||
}, 1500);
|
}, TITLE_FLASH_INTERVAL_MS);
|
||||||
// Set immediately
|
// Set immediately
|
||||||
document.title = `\u26A0\uFE0F (${this.unreadCount}) Claudeman`;
|
document.title = `\u26A0\uFE0F (${this.unreadCount}) Claudeman`;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-7
@@ -105,6 +105,14 @@ const MAX_TERMINAL_ROWS = 200;
|
|||||||
const MAX_SESSION_NAME_LENGTH = 128;
|
const MAX_SESSION_NAME_LENGTH = 128;
|
||||||
// Maximum hook data size (prevents oversized SSE broadcasts)
|
// Maximum hook data size (prevents oversized SSE broadcasts)
|
||||||
const MAX_HOOK_DATA_SIZE = 8 * 1024;
|
const MAX_HOOK_DATA_SIZE = 8 * 1024;
|
||||||
|
// Stats collection interval (2 seconds)
|
||||||
|
const STATS_COLLECTION_INTERVAL_MS = 2000;
|
||||||
|
// Session limit wait time before retrying (5 seconds)
|
||||||
|
const SESSION_LIMIT_WAIT_MS = 5000;
|
||||||
|
// Pause between scheduled run iterations (2 seconds)
|
||||||
|
const ITERATION_PAUSE_MS = 2000;
|
||||||
|
// SSE batch flush threshold (number of items)
|
||||||
|
const BATCH_FLUSH_THRESHOLD = 1024;
|
||||||
// Pre-compiled regex for terminal buffer cleaning (avoids per-request compilation)
|
// Pre-compiled regex for terminal buffer cleaning (avoids per-request compilation)
|
||||||
const CLAUDE_BANNER_PATTERN = /\x1b\[1mClaud/;
|
const CLAUDE_BANNER_PATTERN = /\x1b\[1mClaud/;
|
||||||
const CTRL_L_PATTERN = /\x0c/g;
|
const CTRL_L_PATTERN = /\x0c/g;
|
||||||
@@ -1703,7 +1711,7 @@ export class WebServer extends EventEmitter {
|
|||||||
|
|
||||||
// Start stats collection
|
// Start stats collection
|
||||||
this.app.post('/api/screens/stats/start', async () => {
|
this.app.post('/api/screens/stats/start', async () => {
|
||||||
this.screenManager.startStatsCollection(2000);
|
this.screenManager.startStatsCollection(STATS_COLLECTION_INTERVAL_MS);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2700,7 +2708,7 @@ export class WebServer extends EventEmitter {
|
|||||||
// Check session limit before creating new session
|
// Check session limit before creating new session
|
||||||
if (this.sessions.size >= MAX_CONCURRENT_SESSIONS) {
|
if (this.sessions.size >= MAX_CONCURRENT_SESSIONS) {
|
||||||
addLog(`Waiting: maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached`);
|
addLog(`Waiting: maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached`);
|
||||||
await new Promise(r => setTimeout(r, 5000));
|
await new Promise(r => setTimeout(r, SESSION_LIMIT_WAIT_MS));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2733,7 +2741,7 @@ export class WebServer extends EventEmitter {
|
|||||||
run.sessionId = null;
|
run.sessionId = null;
|
||||||
|
|
||||||
// Small pause between iterations
|
// Small pause between iterations
|
||||||
await new Promise(r => setTimeout(r, 2000));
|
await new Promise(r => setTimeout(r, ITERATION_PAUSE_MS));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addLog(`Error: ${getErrorMessage(err)}`);
|
addLog(`Error: ${getErrorMessage(err)}`);
|
||||||
this.broadcast('scheduled:updated', run);
|
this.broadcast('scheduled:updated', run);
|
||||||
@@ -2749,7 +2757,7 @@ export class WebServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Continue despite errors
|
// Continue despite errors
|
||||||
await new Promise(r => setTimeout(r, 5000));
|
await new Promise(r => setTimeout(r, SESSION_LIMIT_WAIT_MS));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2884,8 +2892,8 @@ export class WebServer extends EventEmitter {
|
|||||||
const newBatch = existing + data;
|
const newBatch = existing + data;
|
||||||
this.terminalBatches.set(sessionId, newBatch);
|
this.terminalBatches.set(sessionId, newBatch);
|
||||||
|
|
||||||
// Flush immediately if batch is large (>1KB) for responsiveness
|
// Flush immediately if batch is large for responsiveness
|
||||||
if (newBatch.length > 1024) {
|
if (newBatch.length > BATCH_FLUSH_THRESHOLD) {
|
||||||
if (this.terminalBatchTimer) {
|
if (this.terminalBatchTimer) {
|
||||||
clearTimeout(this.terminalBatchTimer);
|
clearTimeout(this.terminalBatchTimer);
|
||||||
this.terminalBatchTimer = null;
|
this.terminalBatchTimer = null;
|
||||||
@@ -3222,7 +3230,7 @@ export class WebServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start stats collection to show screen info
|
// Start stats collection to show screen info
|
||||||
this.screenManager.startStatsCollection(2000);
|
this.screenManager.startStatsCollection(STATS_COLLECTION_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dead.length > 0) {
|
if (dead.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user