From 3a77fe48e30b7a08fcb5f59021209b4c6a4fa303 Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 13 Feb 2026 07:42:50 +0100 Subject: [PATCH] chore: bump version to 0.1496 --- CLAUDE.md | 2 +- package.json | 2 +- src/ralph-tracker.ts | 6 ++++++ src/respawn-controller.ts | 4 ++++ src/state-store.ts | 2 +- src/subagent-watcher.ts | 13 +++---------- src/team-watcher.ts | 2 +- src/web/public/app.js | 10 ++++------ src/web/server.ts | 25 +++++++++++++++++++++---- 9 files changed, 42 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c0035e2c..a1efd589 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,7 @@ When user says "COM": 1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`) 2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web` -**Version**: 0.1495 (must match `package.json` for npm publish) +**Version**: 0.1496 (must match `package.json` for npm publish) ## Project Overview diff --git a/package.json b/package.json index 9c2d16ea..11fc02a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1495", + "version": "0.1496", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/src/ralph-tracker.ts b/src/ralph-tracker.ts index 602b34a4..785272ae 100644 --- a/src/ralph-tracker.ts +++ b/src/ralph-tracker.ts @@ -981,6 +981,8 @@ export class RalphTracker extends EventEmitter { this._todos.clear(); this._completionPhraseCount.clear(); this._taskNumberToContent.clear(); + this._todoStartTimes.clear(); + this._alternateCompletionPhrases.clear(); this._lineBuffer = ''; // Reset all RALPH_STATUS block and circuit breaker state this._statusBlockBuffer = []; @@ -2888,6 +2890,8 @@ export class RalphTracker extends EventEmitter { this._loopState = createInitialRalphTrackerState(); // This sets enabled: false this._todos.clear(); this._taskNumberToContent.clear(); + this._todoStartTimes.clear(); + this._alternateCompletionPhrases.clear(); this._lineBuffer = ''; this._partialPromiseBuffer = ''; this._completionPhraseCount.clear(); @@ -3838,6 +3842,8 @@ export class RalphTracker extends EventEmitter { this.stopIterationStallDetection(); this._todos.clear(); this._taskNumberToContent.clear(); + this._todoStartTimes.clear(); + this._alternateCompletionPhrases.clear(); this._completionPhraseCount.clear(); this._planTasks.clear(); this.removeAllListeners(); diff --git a/src/respawn-controller.ts b/src/respawn-controller.ts index fef248cd..24d3f727 100644 --- a/src/respawn-controller.ts +++ b/src/respawn-controller.ts @@ -1765,6 +1765,10 @@ export class RespawnController extends EventEmitter { clearInterval(this.stuckStateTimer); this.stuckStateTimer = null; } + if (this.detectionUpdateTimer) { + clearInterval(this.detectionUpdateTimer); + this.detectionUpdateTimer = null; + } // Clear all tracked timers this.activeTimers.clear(); } diff --git a/src/state-store.ts b/src/state-store.ts index ae053e4b..be786691 100644 --- a/src/state-store.ts +++ b/src/state-store.ts @@ -657,7 +657,7 @@ export class StateStore { const tempPath = this.ralphStatePath + '.tmp'; let json: string; try { - json = JSON.stringify(data, null, 2); + json = JSON.stringify(data); } catch (err) { console.error('[StateStore] Failed to serialize Ralph state (circular reference or invalid data):', err); // Keep dirty flag true for retry - don't throw, let caller continue diff --git a/src/subagent-watcher.ts b/src/subagent-watcher.ts index 0172dbc0..3c8a962d 100644 --- a/src/subagent-watcher.ts +++ b/src/subagent-watcher.ts @@ -1232,16 +1232,9 @@ export class SubagentWatcher extends EventEmitter { const agentCalls = this.pendingToolCalls.get(agentId)!; // Enforce size limit to prevent memory leak from rapid tool calls if (agentCalls.size >= MAX_PENDING_TOOL_CALLS) { - // Evict oldest entry by timestamp - let oldestId: string | null = null; - let oldestTime = Infinity; - for (const [id, call] of agentCalls) { - if (call.timestamp < oldestTime) { - oldestTime = call.timestamp; - oldestId = id; - } - } - if (oldestId) agentCalls.delete(oldestId); + // FIFO eviction: delete first (oldest) entry using Map insertion order + const firstKey = agentCalls.keys().next().value; + if (firstKey !== undefined) agentCalls.delete(firstKey); } agentCalls.set(content.id, { toolName: content.name, diff --git a/src/team-watcher.ts b/src/team-watcher.ts index 0205ad15..0c48d831 100644 --- a/src/team-watcher.ts +++ b/src/team-watcher.ts @@ -18,7 +18,7 @@ import { LRUMap } from './utils/lru-map.js'; // ========== Constants ========== -const POLL_INTERVAL_MS = 3000; +const POLL_INTERVAL_MS = 5000; const MAX_CACHED_TEAMS = 50; const MAX_CACHED_TASKS = 200; diff --git a/src/web/public/app.js b/src/web/public/app.js index 4d39afa0..1abf3599 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -40,6 +40,8 @@ const WINDOW_DEFAULT_WIDTH_PX = 300; // xterm.js doesn't support DEC 2026 natively, so we implement buffering ourselves. const DEC_SYNC_START = '\x1b[?2026h'; const DEC_SYNC_END = '\x1b[?2026l'; +// Pre-compiled regex for stripping DEC 2026 markers (single pass instead of two replaceAll calls) +const DEC_SYNC_STRIP_RE = /\x1b\[\?2026[hl]/g; // Built-in respawn configuration presets const BUILTIN_RESPAWN_PRESETS = [ @@ -1867,9 +1869,7 @@ class ClaudemanApp { // Strip any DEC 2026 markers that might be in the buffer // (from historical SSE data that was stored with markers) - const cleanBuffer = buffer - .replaceAll(DEC_SYNC_START, '') - .replaceAll(DEC_SYNC_END, ''); + const cleanBuffer = buffer.replace(DEC_SYNC_STRIP_RE, ''); // For small buffers, write directly if (cleanBuffer.length <= chunkSize) { @@ -2157,9 +2157,7 @@ class ClaudemanApp { if (termData.terminalBuffer) { // Strip any DEC 2026 markers and write raw content // (markers don't help here - this is a static buffer reload, not live Ink redraws) - const cleanBuffer = termData.terminalBuffer - .replaceAll(DEC_SYNC_START, '') - .replaceAll(DEC_SYNC_END, ''); + const cleanBuffer = termData.terminalBuffer.replace(DEC_SYNC_STRIP_RE, ''); // Use chunked write to avoid UI freeze with large buffers (can be 1-2MB) await this.chunkedTerminalWrite(cleanBuffer); } diff --git a/src/web/server.ts b/src/web/server.ts index 6a8fbc09..805ec1f1 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -352,6 +352,10 @@ export class WebServer extends EventEmitter { private sseHealthCheckTimer: NodeJS.Timeout | null = null; // Flag to prevent new timers during shutdown private _isStopping: boolean = false; + // Cached light state for SSE init (avoids rebuilding on every reconnect) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private cachedLightState: { data: Record; timestamp: number } | null = null; + private static readonly LIGHT_STATE_CACHE_TTL_MS = 1000; // Token recording for daily stats (track what's been recorded to avoid double-counting) private lastRecordedTokens: Map = new Map(); private tokenRecordingTimer: NodeJS.Timeout | null = null; @@ -684,8 +688,9 @@ export class WebServer extends EventEmitter { this.persistSessionState(session); await this.setupSessionListeners(session); - this.broadcast('session:created', session.toDetailedState()); - return { success: true, session: session.toDetailedState() }; + const detailedState = session.toDetailedState(); + this.broadcast('session:created', detailedState); + return { success: true, session: detailedState }; }); // Rename a session @@ -4371,6 +4376,11 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; * to prevent browser freezes. Terminal buffers are fetched on-demand. */ private getLightState() { + const now = Date.now(); + if (this.cachedLightState && now - this.cachedLightState.timestamp < WebServer.LIGHT_STATE_CACHE_TTL_MS) { + return this.cachedLightState.data; + } + const respawnStatus: Record> = {}; for (const [sessionId, controller] of this.respawnControllers) { respawnStatus[sessionId] = controller.getStatus(); @@ -4385,15 +4395,18 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; }; } - return { + const result = { version: APP_VERSION, sessions: this.getLightSessionsState(), scheduledRuns: Array.from(this.scheduledRuns.values()), respawnStatus, globalStats: this.store.getAggregateStats(activeSessionTokens), subagents: subagentWatcher.getRecentSubagents(15), // 15 min to avoid stale agents - timestamp: Date.now(), + timestamp: now, }; + + this.cachedLightState = { data: result, timestamp: now }; + return result; } private sendSSE(reply: FastifyReply, event: string, data: unknown): void { @@ -4426,6 +4439,10 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; } private broadcast(event: string, data: unknown): void { + // Invalidate light state cache on any state-changing broadcast + if (event.startsWith('session:') || event === 'respawn:') { + this.cachedLightState = null; + } // Performance optimization: serialize JSON once for all clients let message: string; try {