mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
chore: bump version to 0.1390
This commit is contained in:
@@ -25,7 +25,20 @@ Always bump version on every COM, even for small changes.
|
||||
|
||||
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.1389 (must match `package.json`)
|
||||
### Mission: Rock-Solid Performance
|
||||
|
||||
**The app MUST remain fast, responsive, and never hang** — even with many tabs open and many subagent windows active. This is a core design principle, not a nice-to-have. Every feature must be built with this constraint in mind:
|
||||
|
||||
- **60fps terminal rendering** with batched writes and `requestAnimationFrame`
|
||||
- **Auto-trimming buffers** prevent memory bloat in long-running sessions
|
||||
- **Debounced state persistence** (500ms) avoids disk thrashing
|
||||
- **SSE batching** (16ms) reduces client-side event storms
|
||||
- **Lazy loading** of agent transcripts and historical data
|
||||
- **No blocking operations** in the main event loop
|
||||
|
||||
When adding new features, always ask: "Will this maintain responsiveness with 20 sessions and 50 agent windows?"
|
||||
|
||||
**Version**: 0.1390 (must match `package.json`)
|
||||
|
||||
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
|
||||
|
||||
@@ -528,7 +541,7 @@ TUI uses React JSX (`jsx: react-jsx`, `jsxImportSource: react`) for Ink componen
|
||||
- **Session event**: Add to `SessionEvents` interface in `session.ts`, emit via `this.emit()`, subscribe in server.ts, handle in frontend
|
||||
- **Session setting**: Add field to `SessionState` in `types.ts`, include in `session.toState()`, call `this.persistSessionState(session)` in server.ts after the change
|
||||
- **MCP tool**: Add tool definition in `mcp-server.ts` using `server.tool()`, use `apiRequest()` to call Claudeman REST API
|
||||
- **New test file**: Create `test/<name>.test.ts`, pick unique port (next available: 3155), add to port allocation table above
|
||||
- **New test file**: Create `test/<name>.test.ts`, pick unique port (next available: 3183), add to port allocation table above
|
||||
|
||||
### API Error Codes
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1389",
|
||||
"version": "0.1390",
|
||||
"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",
|
||||
|
||||
+93
-23
@@ -123,7 +123,10 @@ const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude/projects');
|
||||
const IDLE_TIMEOUT_MS = 30000; // Consider agent idle after 30s of no activity
|
||||
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 STALE_AGENT_MAX_AGE_MS = 24 * 60 * 60 * 1000; // Remove completed agents older than 24 hours
|
||||
const STALE_COMPLETED_MAX_AGE_MS = 60 * 60 * 1000; // Remove completed agents older than 1 hour
|
||||
const STALE_IDLE_MAX_AGE_MS = 4 * 60 * 60 * 1000; // Remove idle agents older than 4 hours
|
||||
const STARTUP_MAX_FILE_AGE_MS = 4 * 60 * 60 * 1000; // Only load files modified in last 4 hours on startup
|
||||
const MAX_TRACKED_AGENTS = 500; // Maximum agents to track (LRU eviction when exceeded)
|
||||
|
||||
// Display/preview length constants
|
||||
const TEXT_PREVIEW_LENGTH = 200; // Length for text previews in tool results
|
||||
@@ -282,36 +285,91 @@ export class SubagentWatcher extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up stale completed agents to prevent unbounded memory growth
|
||||
* Removes agents that have been completed for longer than STALE_AGENT_MAX_AGE_MS
|
||||
* Clean up stale agents to prevent unbounded memory growth.
|
||||
* - Completed agents: removed after STALE_COMPLETED_MAX_AGE_MS (1 hour)
|
||||
* - Idle agents: removed after STALE_IDLE_MAX_AGE_MS (4 hours)
|
||||
* Also enforces MAX_TRACKED_AGENTS limit with LRU eviction.
|
||||
*/
|
||||
private cleanupStaleAgents(): void {
|
||||
const now = Date.now();
|
||||
const agentsToDelete: string[] = [];
|
||||
|
||||
for (const [agentId, info] of this.agentInfo) {
|
||||
if (info.status === 'completed') {
|
||||
const lastActivity = new Date(info.lastActivityAt).getTime();
|
||||
if (now - lastActivity > STALE_AGENT_MAX_AGE_MS) {
|
||||
agentsToDelete.push(agentId);
|
||||
}
|
||||
const lastActivity = new Date(info.lastActivityAt).getTime();
|
||||
const age = now - lastActivity;
|
||||
|
||||
// Clean up based on status and age
|
||||
if (info.status === 'completed' && age > STALE_COMPLETED_MAX_AGE_MS) {
|
||||
agentsToDelete.push(agentId);
|
||||
} else if (info.status === 'idle' && age > STALE_IDLE_MAX_AGE_MS) {
|
||||
agentsToDelete.push(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const agentId of agentsToDelete) {
|
||||
const info = this.agentInfo.get(agentId);
|
||||
if (info) {
|
||||
// Clean up all associated resources
|
||||
this.agentInfo.delete(agentId);
|
||||
this.pendingToolCalls.delete(agentId);
|
||||
this.filePositions.delete(info.filePath);
|
||||
const watcher = this.fileWatchers.get(info.filePath);
|
||||
if (watcher) {
|
||||
watcher.close();
|
||||
this.fileWatchers.delete(info.filePath);
|
||||
}
|
||||
// Enforce max tracked agents limit (LRU eviction)
|
||||
const currentCount = this.agentInfo.size - agentsToDelete.length;
|
||||
if (currentCount > MAX_TRACKED_AGENTS) {
|
||||
// Sort by lastActivityAt (oldest first) and evict oldest completed/idle agents
|
||||
const sortedAgents = Array.from(this.agentInfo.entries())
|
||||
.filter(([id]) => !agentsToDelete.includes(id))
|
||||
.filter(([, info]) => info.status !== 'active') // Keep active agents
|
||||
.sort((a, b) => new Date(a[1].lastActivityAt).getTime() - new Date(b[1].lastActivityAt).getTime());
|
||||
|
||||
const toEvict = currentCount - MAX_TRACKED_AGENTS;
|
||||
for (let i = 0; i < toEvict && i < sortedAgents.length; i++) {
|
||||
agentsToDelete.push(sortedAgents[i][0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Perform cleanup
|
||||
for (const agentId of agentsToDelete) {
|
||||
this.removeAgent(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an agent and all its associated resources.
|
||||
*/
|
||||
private removeAgent(agentId: string): void {
|
||||
const info = this.agentInfo.get(agentId);
|
||||
if (info) {
|
||||
this.agentInfo.delete(agentId);
|
||||
this.pendingToolCalls.delete(agentId);
|
||||
this.filePositions.delete(info.filePath);
|
||||
const watcher = this.fileWatchers.get(info.filePath);
|
||||
if (watcher) {
|
||||
watcher.close();
|
||||
this.fileWatchers.delete(info.filePath);
|
||||
}
|
||||
const timer = this.idleTimers.get(agentId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
this.idleTimers.delete(agentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger cleanup of stale agents.
|
||||
* Returns number of agents removed.
|
||||
*/
|
||||
cleanupNow(): number {
|
||||
const beforeCount = this.agentInfo.size;
|
||||
this.cleanupStaleAgents();
|
||||
return beforeCount - this.agentInfo.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all tracked agents (for manual reset).
|
||||
* Returns number of agents cleared.
|
||||
*/
|
||||
clearAll(): number {
|
||||
const count = this.agentInfo.size;
|
||||
const agentIds = Array.from(this.agentInfo.keys());
|
||||
for (const agentId of agentIds) {
|
||||
this.removeAgent(agentId);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -757,12 +815,12 @@ export class SubagentWatcher extends EventEmitter {
|
||||
if (this.knownSubagentDirs.has(dir)) return;
|
||||
this.knownSubagentDirs.add(dir);
|
||||
|
||||
// Watch existing files
|
||||
// Watch existing files (initial scan - skip old files)
|
||||
try {
|
||||
const files = readdirSync(dir);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.jsonl')) {
|
||||
this.watchAgentFile(join(dir, file), projectHash, sessionId);
|
||||
this.watchAgentFile(join(dir, file), projectHash, sessionId, true);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -801,8 +859,12 @@ export class SubagentWatcher extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Watch a specific agent transcript file
|
||||
* @param filePath Path to the agent transcript file
|
||||
* @param projectHash Claude project hash
|
||||
* @param sessionId Claude session ID
|
||||
* @param isInitialScan If true, skip files older than STARTUP_MAX_FILE_AGE_MS
|
||||
*/
|
||||
private watchAgentFile(filePath: string, projectHash: string, sessionId: string): void {
|
||||
private watchAgentFile(filePath: string, projectHash: string, sessionId: string, isInitialScan: boolean = false): void {
|
||||
if (this.fileWatchers.has(filePath)) return;
|
||||
|
||||
const agentId = basename(filePath).replace('agent-', '').replace('.jsonl', '');
|
||||
@@ -816,6 +878,14 @@ export class SubagentWatcher extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// On initial scan, skip old files to avoid loading stale historical data
|
||||
if (isInitialScan) {
|
||||
const fileAge = Date.now() - stat.mtime.getTime();
|
||||
if (fileAge > STARTUP_MAX_FILE_AGE_MS) {
|
||||
return; // Skip old files on startup
|
||||
}
|
||||
}
|
||||
|
||||
// Extract description - prefer reading from parent transcript (most reliable)
|
||||
// The parent transcript has the exact Task tool call with description parameter
|
||||
let description = this.extractDescriptionFromParentTranscript(projectHash, sessionId, agentId);
|
||||
|
||||
@@ -1972,6 +1972,18 @@ export class WebServer extends EventEmitter {
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Subagent not found or already completed');
|
||||
});
|
||||
|
||||
// Trigger cleanup of stale subagents
|
||||
this.app.post('/api/subagents/cleanup', async () => {
|
||||
const removed = subagentWatcher.cleanupNow();
|
||||
return { success: true, data: { removed, remaining: subagentWatcher.getSubagents().length } };
|
||||
});
|
||||
|
||||
// Clear all tracked subagents (memory only - does not delete files)
|
||||
this.app.delete('/api/subagents', async () => {
|
||||
const cleared = subagentWatcher.clearAll();
|
||||
return { success: true, data: { cleared } };
|
||||
});
|
||||
|
||||
|
||||
// ========== Hook Events ==========
|
||||
|
||||
|
||||
Reference in New Issue
Block a user