From a0ffec7eb6e180fdbf6ce66e03a3914e097fd710 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 26 Jan 2026 21:27:08 +0100 Subject: [PATCH] fix: exclude full terminal buffers from SSE init to prevent browser freezes - Add getLightState() and getLightSessionsState() methods - SSE init payload reduced from ~1.9MB to ~5KB (~400x smaller) - Terminal buffers are fetched on-demand when switching tabs - Fixes UI becoming unresponsive on SSE reconnect chore: bump version to 0.1388 --- CLAUDE.md | 15 ++++++++++--- package.json | 2 +- src/web/server.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 484827af..d0c407db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,13 +15,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## ⚡ COM Shorthand (Deployment) -When user says "COM": 1) Increment version in BOTH `package.json` AND `CLAUDE.md` (keep them in sync), 2) `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`. Always bump version on every COM, even for small changes. +When user says "COM": +1. Increment version in BOTH `package.json` AND `CLAUDE.md` (keep them in sync) +2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web` + +Always bump version on every COM, even for small changes. ## Project Overview 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.1387 (must match `package.json`) +**Version**: 0.1388 (must match `package.json`) **Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty @@ -126,7 +130,7 @@ Unit tests (no port needed): respawn-controller, ralph-tracker, pty-interactive, **Test Cleanup Patterns**: Integration tests track resources in `createdSessions` and `createdCases` arrays, cleaned up by `afterAll`/`afterEach` hooks. However, some tests perform cleanup in the test body itself (e.g., `edge-cases.test.ts:273-302` creates 5 sessions and cleans them in a loop). If assertions fail before cleanup code runs, resources leak. -**Known Cleanup Issues**: +**Known Cleanup Issues** (technical debt): - `pty-interactive.test.ts`: Uses `await session.stop()` at end of each test, not in `afterEach`. Test failures leave sessions running. - `edge-cases.test.ts`: Multiple sessions created in test body with cleanup at end; failures leak sessions. - Test cases (`~/claudeman-cases/`): Cases named `flow-test-*`, `ralph-track-loop-*`, `session-detail-*` may persist after test failures. @@ -604,11 +608,16 @@ Placeholders replaced: ## Documentation +**Reference docs** (read these for deep dives): - `docs/respawn-state-machine.md` - Respawn controller states, idle detection, auto-accept - `docs/spawn-protocol.md` - Spawn1337 agent protocol, MCP tools, resource governance - `docs/ralph-wiggum-guide.md` - Ralph Wiggum loop guide (plugin reference, prompt templates) - `docs/claude-code-hooks-reference.md` - Claude Code hooks documentation +**Internal planning** (historical context, may be outdated): +- `docs/respawn-improvement-plan.md` - Planned respawn improvements +- `docs/run-summary-plan.md` - Run summary feature design + ### Ralph Wiggum Loops **Core Pattern**: `PHRASE` - The completion signal that tells the loop to stop. diff --git a/package.json b/package.json index 113f5edf..41a0d20c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1387", + "version": "0.1388", "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/web/server.ts b/src/web/server.ts index bbf7ad01..c98409b7 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -384,7 +384,9 @@ export class WebServer extends EventEmitter { this.sseClients.add(reply); // Send initial state - this.sendSSE(reply, 'init', this.getFullState()); + // Use light state for SSE init to avoid sending 2MB+ terminal buffers + // Buffers are fetched on-demand when switching tabs + this.sendSSE(reply, 'init', this.getLightState()); req.raw.on('close', () => { this.sseClients.delete(reply); @@ -2797,6 +2799,27 @@ export class WebServer extends EventEmitter { }); } + /** + * Get lightweight session state for SSE init - excludes full terminal buffers + * to prevent browser freezes on SSE reconnect. Full buffers are fetched + * on-demand when switching tabs via /api/sessions/:id/buffer + */ + private getLightSessionsState() { + return Array.from(this.sessions.values()).map(s => { + const controller = this.respawnControllers.get(s.id); + const detailed = s.toDetailedState(); + return { + ...detailed, + // Exclude full buffers - they're fetched on-demand + terminalBuffer: '', + textOutput: '', + respawnEnabled: controller?.getConfig()?.enabled ?? false, + respawnConfig: controller?.getConfig() ?? null, + respawn: controller?.getStatus() ?? null, + }; + }); + } + // Clean up old completed scheduled runs private cleanupScheduledRuns(): void { const now = Date.now(); @@ -2850,6 +2873,36 @@ export class WebServer extends EventEmitter { }; } + /** + * Get lightweight state for SSE init - excludes full terminal buffers + * to prevent browser freezes. Terminal buffers are fetched on-demand. + */ + private getLightState() { + const respawnStatus: Record> = {}; + for (const [sessionId, controller] of this.respawnControllers) { + respawnStatus[sessionId] = controller.getStatus(); + } + + const activeSessionTokens: Record = {}; + for (const [sessionId, session] of this.sessions) { + activeSessionTokens[sessionId] = { + inputTokens: session.inputTokens, + outputTokens: session.outputTokens, + totalCost: session.totalCost, + }; + } + + return { + version: APP_VERSION, + sessions: this.getLightSessionsState(), + scheduledRuns: Array.from(this.scheduledRuns.values()), + respawnStatus, + globalStats: this.store.getAggregateStats(activeSessionTokens), + subagents: subagentWatcher.getRecentSubagents(60), + timestamp: Date.now(), + }; + } + private sendSSE(reply: FastifyReply, event: string, data: unknown): void { try { reply.raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);