mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix: use TERMINAL_TAIL_SIZE constant and add client-drop recovery
Two hardcoded `256 * 1024` tail sizes in app.js bypassed the TERMINAL_TAIL_SIZE constant (128KB), causing stale cached browsers to fetch 256KB buffers even after the constant was reduced to prevent WebGL GPU stalls. Also adds a self-recovery timer that reloads the terminal buffer after client-side data drops, preventing permanent display corruption. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
|
|||||||
|
|
||||||
**CI**: `.github/workflows/ci.yml` runs `typecheck`, `lint`, `format:check` on push to master (Node 22). Tests excluded (they spawn tmux).
|
**CI**: `.github/workflows/ci.yml` runs `typecheck`, `lint`, `format:check` on push to master (Node 22). Tests excluded (they spawn tmux).
|
||||||
|
|
||||||
**Code style**: Prettier (`singleQuote: true`, `printWidth: 120`, `trailingComma: "es5"`). ESLint allows `no-console`, warns on `@typescript-eslint/no-explicit-any`. Does not lint `app.js` or `scripts/**/*.mjs`.
|
**Code style**: Prettier (`singleQuote: true`, `printWidth: 120`, `trailingComma: "es5"`). ESLint flat config (`eslint.config.js`) allows `no-console`, warns on `@typescript-eslint/no-explicit-any`. Ignores: `app.js`, `scripts/**/*.mjs`, `src/web/public/vendor/**`, `tools/**`, `remotion/**`.
|
||||||
|
|
||||||
## Common Gotchas
|
## Common Gotchas
|
||||||
|
|
||||||
@@ -108,9 +108,9 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
|
|||||||
| **State** | `src/state-store.ts`, `src/run-summary.ts`, `src/session-lifecycle-log.ts` | |
|
| **State** | `src/state-store.ts`, `src/run-summary.ts`, `src/session-lifecycle-log.ts` | |
|
||||||
| **Infra** | `src/hooks-config.ts`, `src/push-store.ts`, `src/tunnel-manager.ts`, `src/image-watcher.ts`, `src/file-stream-manager.ts` | |
|
| **Infra** | `src/hooks-config.ts`, `src/push-store.ts`, `src/tunnel-manager.ts`, `src/image-watcher.ts`, `src/file-stream-manager.ts` | |
|
||||||
| **Plan** | `src/plan-orchestrator.ts`, `src/prompts/*.ts`, `src/templates/claude-md.ts` | |
|
| **Plan** | `src/plan-orchestrator.ts`, `src/prompts/*.ts`, `src/templates/claude-md.ts` | |
|
||||||
| **Web** | `src/web/server.ts`, `src/web/sse-events.ts`, `src/web/routes/*.ts` (13 modules), `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts` | |
|
| **Web** | `src/web/server.ts`, `src/web/sse-events.ts`, `src/web/routes/*.ts` (12 route modules + barrel), `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts` | |
|
||||||
| **Frontend** | `src/web/public/app.js` ★ (~11.8K lines) + 10 JS modules (incl. `sw.js` service worker) | |
|
| **Frontend** | `src/web/public/app.js` ★ (~11.8K lines) + 10 JS modules (incl. `sw.js` service worker) | |
|
||||||
| **Types** | `src/types/index.ts` → 14 domain files | See `@fileoverview` in index.ts |
|
| **Types** | `src/types/index.ts` → 13 domain files | See `@fileoverview` in index.ts |
|
||||||
|
|
||||||
★ = Large file (>50KB). All files have `@fileoverview` JSDoc — read that before diving in.
|
★ = Large file (>50KB). All files have `@fileoverview` JSDoc — read that before diving in.
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
|
|||||||
|
|
||||||
### API Routes
|
### API Routes
|
||||||
|
|
||||||
~111 handlers across 13 route files in `src/web/routes/`: system (35), sessions (24), ralph (9), plan (8), respawn (7), cases (7), files (5), mux (5), scheduled (4), push (4), teams (2), hooks (1). Each file has `@fileoverview` with endpoint details.
|
~111 handlers across 12 route files in `src/web/routes/`: system (35), sessions (24), ralph (9), plan (8), respawn (7), cases (7), files (5), mux (5), scheduled (4), push (4), teams (2), hooks (1). Each file has `@fileoverview` with endpoint details.
|
||||||
|
|
||||||
## Adding Features
|
## Adding Features
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
|
|||||||
- **Session setting**: Add to `SessionState`, include in `session.toState()`, call `persistSessionState()`
|
- **Session setting**: Add to `SessionState`, include in `session.toState()`, call `persistSessionState()`
|
||||||
- **Hook event**: Add to `HookEventType`, add hook in `hooks-config.ts:generateHooksConfig()`, update `HookEventSchema`
|
- **Hook event**: Add to `HookEventType`, add hook in `hooks-config.ts:generateHooksConfig()`, update `HookEventSchema`
|
||||||
- **Mobile feature**: Add to relevant singleton, guard with `MobileDetection.isMobile()`
|
- **Mobile feature**: Add to relevant singleton, guard with `MobileDetection.isMobile()`
|
||||||
- **New test**: Pick unique port (search `const PORT =`). Integration: ports 3099-3211. Route tests: `app.inject()` — see `test/routes/_route-test-utils.ts`.
|
- **New test**: Pick unique port (search `const PORT =`). Route tests use `app.inject()` (no port needed) — see `test/routes/_route-test-utils.ts`.
|
||||||
|
|
||||||
**Validation**: Zod v4 (different API from v3). Define schemas in `schemas.ts`, use `.parse()`/`.safeParse()`.
|
**Validation**: Zod v4 (different API from v3). Define schemas in `schemas.ts`, use `.parse()`/`.safeParse()`.
|
||||||
|
|
||||||
@@ -238,7 +238,6 @@ Key: `scripts/tmux-manager.sh` (safe tmux mgmt), `scripts/tunnel.sh` (tunnel sta
|
|||||||
## Common Workflows
|
## Common Workflows
|
||||||
|
|
||||||
**Bug investigation**: Dev server → reproduce in browser → check terminal + `~/.codeman/state.json`.
|
**Bug investigation**: Dev server → reproduce in browser → check terminal + `~/.codeman/state.json`.
|
||||||
**API endpoint**: Types in `src/types/*.ts` → route in `src/web/routes/*-routes.ts` → SSE event if needed → handle in `app.js`.
|
|
||||||
**Respawn changes**: Read `docs/respawn-state-machine.md` first. Use `MockSession` from `test/respawn-test-utils.ts`.
|
**Respawn changes**: Read `docs/respawn-state-machine.md` first. Use `MockSession` from `test/respawn-test-utils.ts`.
|
||||||
|
|
||||||
## Tunnel
|
## Tunnel
|
||||||
|
|||||||
+19
-10
@@ -1817,14 +1817,24 @@ class CodemanApp {
|
|||||||
if (data.id === this.activeSessionId) {
|
if (data.id === this.activeSessionId) {
|
||||||
if (data.data.length > 32768) _crashDiag.log(`TERMINAL: ${(data.data.length/1024).toFixed(0)}KB`);
|
if (data.data.length > 32768) _crashDiag.log(`TERMINAL: ${(data.data.length/1024).toFixed(0)}KB`);
|
||||||
|
|
||||||
// Hard cap: track total bytes queued across ALL buffers (pendingWrites +
|
// Hard cap: track total bytes queued in render buffers (pendingWrites +
|
||||||
// flickerFilterBuffer + loadBufferQueue). When rAF is throttled (tab
|
// flickerFilterBuffer). When rAF is throttled (tab
|
||||||
// backgrounded, GPU busy), data accumulates with no flush, reaching
|
// backgrounded, GPU busy), data accumulates with no flush, reaching
|
||||||
// 889KB+ and freezing Chrome for minutes. Drop data beyond 96KB —
|
// 889KB+ and freezing Chrome for minutes. Drop data beyond 128KB and
|
||||||
// the server sends session:needsRefresh to recover.
|
// schedule a buffer reload to recover the display once the burst subsides.
|
||||||
const queued = (this.pendingWrites?.reduce((s, w) => s + w.length, 0) || 0)
|
const queued = (this.pendingWrites?.reduce((s, w) => s + w.length, 0) || 0)
|
||||||
+ (this.flickerFilterBuffer?.length || 0);
|
+ (this.flickerFilterBuffer?.length || 0);
|
||||||
if (queued > 131072) return; // 128KB — drop to prevent accumulation
|
if (queued > 131072) { // 128KB — drop to prevent accumulation
|
||||||
|
// Schedule a self-recovery: reload the full terminal buffer once the
|
||||||
|
// queue drains (debounced to avoid hammering the API during sustained bursts).
|
||||||
|
if (!this._clientDropRecoveryTimer) {
|
||||||
|
this._clientDropRecoveryTimer = setTimeout(() => {
|
||||||
|
this._clientDropRecoveryTimer = null;
|
||||||
|
this._onSessionNeedsRefresh();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.batchTerminalWrite(data.data);
|
this.batchTerminalWrite(data.data);
|
||||||
}
|
}
|
||||||
@@ -1835,8 +1845,7 @@ class CodemanApp {
|
|||||||
// so reload the buffer to recover from any display corruption.
|
// so reload the buffer to recover from any display corruption.
|
||||||
if (!this.activeSessionId || !this.terminal) return;
|
if (!this.activeSessionId || !this.terminal) return;
|
||||||
try {
|
try {
|
||||||
const tailSize = 256 * 1024;
|
const res = await fetch(`/api/sessions/${this.activeSessionId}/terminal?tail=${TERMINAL_TAIL_SIZE}`);
|
||||||
const res = await fetch(`/api/sessions/${this.activeSessionId}/terminal?tail=${tailSize}`);
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.terminalBuffer) {
|
if (data.terminalBuffer) {
|
||||||
this.terminal.clear();
|
this.terminal.clear();
|
||||||
@@ -3532,6 +3541,7 @@ class CodemanApp {
|
|||||||
this._tabCompletionRetries = 0;
|
this._tabCompletionRetries = 0;
|
||||||
this._tabCompletionBaseText = null;
|
this._tabCompletionBaseText = null;
|
||||||
if (this._tabCompletionFallback) { clearTimeout(this._tabCompletionFallback); this._tabCompletionFallback = null; }
|
if (this._tabCompletionFallback) { clearTimeout(this._tabCompletionFallback); this._tabCompletionFallback = null; }
|
||||||
|
if (this._clientDropRecoveryTimer) { clearTimeout(this._clientDropRecoveryTimer); this._clientDropRecoveryTimer = null; }
|
||||||
|
|
||||||
// Clean up pending terminal writes to prevent old session data from appearing in new session
|
// Clean up pending terminal writes to prevent old session data from appearing in new session
|
||||||
if (this.syncWaitTimeout) {
|
if (this.syncWaitTimeout) {
|
||||||
@@ -3634,7 +3644,7 @@ class CodemanApp {
|
|||||||
|
|
||||||
// Load terminal buffer for this session
|
// Load terminal buffer for this session
|
||||||
// Show cached content instantly while fetching fresh data in background.
|
// Show cached content instantly while fetching fresh data in background.
|
||||||
// Use tail mode for faster initial load (256KB is enough for recent visible content).
|
// Use tail mode for faster initial load (128KB is enough for recent visible content).
|
||||||
//
|
//
|
||||||
// Protect flushed state during buffer load: terminal.write() can trigger
|
// Protect flushed state during buffer load: terminal.write() can trigger
|
||||||
// xterm.js onData responses (DA, OSC, etc.) that would otherwise clear
|
// xterm.js onData responses (DA, OSC, etc.) that would otherwise clear
|
||||||
@@ -3664,8 +3674,7 @@ class CodemanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_crashDiag.log('FETCH_START');
|
_crashDiag.log('FETCH_START');
|
||||||
const tailSize = 256 * 1024;
|
const res = await fetch(`/api/sessions/${sessionId}/terminal?tail=${TERMINAL_TAIL_SIZE}`);
|
||||||
const res = await fetch(`/api/sessions/${sessionId}/terminal?tail=${tailSize}`);
|
|
||||||
if (selectGen !== this._selectGeneration) { if (this._isLoadingBuffer) this._finishBufferLoad(); this._restoringFlushedState = false; return; }
|
if (selectGen !== this._selectGeneration) { if (this._isLoadingBuffer) this._finishBufferLoad(); this._restoringFlushedState = false; return; }
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
_crashDiag.log(`FETCH_DONE: ${data.terminalBuffer ? (data.terminalBuffer.length/1024).toFixed(0) + 'KB' : 'empty'} truncated=${data.truncated}`);
|
_crashDiag.log(`FETCH_DONE: ${data.terminalBuffer ? (data.terminalBuffer.length/1024).toFixed(0) + 'KB' : 'empty'} truncated=${data.truncated}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user