Merge pull request #138 from aakhter/cod-80-raise-terminal-defaults

Raise terminal history/scrollback/buffer defaults (50k→100k, 2MB→32MB)
This commit is contained in:
Ark0N
2026-07-12 13:22:37 +02:00
committed by GitHub
4 changed files with 55 additions and 20 deletions
+1 -2
View File
@@ -6,10 +6,9 @@
* it easy to tune memory usage. * it easy to tune memory usage.
* *
* Memory Budget Rationale (for 20 concurrent sessions): * Memory Budget Rationale (for 20 concurrent sessions):
* - Terminal buffer: 2MB max × 20 = 40MB worst case * - Terminal buffer: 32MB max × 20 = 640MB worst case
* - Text output: 1MB max × 20 = 20MB worst case * - Text output: 1MB max × 20 = 20MB worst case
* - Messages: ~1KB each × 1000 × 20 = 20MB worst case * - Messages: ~1KB each × 1000 × 20 = 20MB worst case
* - Total buffer overhead: ~80MB (acceptable for a long-running server)
* *
* @module config/buffer-limits * @module config/buffer-limits
*/ */
+21 -9
View File
@@ -1,19 +1,31 @@
/** /**
* Defaults, bounds, and resolution for terminal history retention. * Defaults, bounds, and resolution for terminal history retention.
* *
* Centralizes the terminal scrollback, tmux history-limit, and server PTY buffer * Raised defaults (the ones actually wired):
* byte caps that were previously scattered as hardcoded literals. Each value is * - tmux history-limit: 50,000 -> 100,000 lines (applied at session spawn)
* overridable (env var or the settings object) and clamped to a sane range via * - server PTY buffer cap: 2MB max / 1.5MB trim -> 32MB / 24MB (via buffer-limits.ts)
* resolveTerminalHistoryConfig(). Defaults intentionally match the prior * Browser xterm scrollback is a separate hardcoded DEFAULT_SCROLLBACK (50,000) in
* hardcoded values, so introducing this module is behavior-neutral. * src/web/public/constants.js and deliberately stays at 50k — 100k xterm lines per tab
* is a mobile-memory hazard — so DEFAULT_TERMINAL_SCROLLBACK_LINES stays 50,000 to match.
* The terminalScrollbackLines/terminalBufferMaxBytes/terminalBufferTrimBytes settings keys
* remain schema-validated but inert (a follow-up wires them); only tmuxHistoryLimit is live.
* All values remain env- and settings-overridable and bounds-clamped via
* resolveTerminalHistoryConfig().
*/ */
export const DEFAULT_TERMINAL_SCROLLBACK_LINES = 50_000; export const DEFAULT_TERMINAL_SCROLLBACK_LINES = 50_000;
export const DEFAULT_TMUX_HISTORY_LIMIT = 50_000; export const DEFAULT_TMUX_HISTORY_LIMIT = 100_000;
export const DEFAULT_TERMINAL_BUFFER_MAX_BYTES = export const DEFAULT_TERMINAL_BUFFER_MAX_BYTES =
parseInt(process.env.CODEMAN_MAX_TERMINAL_BUFFER || '', 10) || 2 * 1024 * 1024; parseInt(process.env.CODEMAN_MAX_TERMINAL_BUFFER || '', 10) || 32 * 1024 * 1024;
export const DEFAULT_TERMINAL_BUFFER_TRIM_BYTES = // Trim must stay below the max: BufferAccumulator.trim() keeps the last trimSize chars, so a
parseInt(process.env.CODEMAN_TRIM_TERMINAL_TO || '', 10) || 1.5 * 1024 * 1024; // trim >= max never shrinks the buffer — every append then re-joins the whole string (O(n²))
// and memory overshoots the operator's cap (e.g. CODEMAN_MAX_TERMINAL_BUFFER=2097152 with no
// trim env would leave the 24MB trim default in force). Clamp to 75% of the resolved max,
// preserving the 24MB/32MB default ratio as trim hysteresis.
export const DEFAULT_TERMINAL_BUFFER_TRIM_BYTES = Math.min(
parseInt(process.env.CODEMAN_TRIM_TERMINAL_TO || '', 10) || 24 * 1024 * 1024,
Math.floor(DEFAULT_TERMINAL_BUFFER_MAX_BYTES * 0.75)
);
export const MIN_TERMINAL_SCROLLBACK_LINES = 1_000; export const MIN_TERMINAL_SCROLLBACK_LINES = 1_000;
export const MAX_TERMINAL_SCROLLBACK_LINES = 1_000_000; export const MAX_TERMINAL_SCROLLBACK_LINES = 1_000_000;
+2 -1
View File
@@ -1202,7 +1202,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
/* Already set globally as fallback */ /* Already set globally as fallback */
}), }),
// Raise tmux scrollback from its 2000-line default so re-attach preserves // Raise tmux scrollback from its 2000-line default so re-attach preserves
// more context. Matches the xterm-side default in constants.js. // more context. Intentionally exceeds the xterm-side DEFAULT_SCROLLBACK (50k
// in constants.js), which stays lower to protect browser/mobile memory.
execAsync(`${this.tmux()} set-option -t "${muxName}" history-limit ${historyLimit}`, { execAsync(`${this.tmux()} set-option -t "${muxName}" history-limit ${historyLimit}`, {
timeout: EXEC_TIMEOUT_MS, timeout: EXEC_TIMEOUT_MS,
}) })
+31 -8
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { import {
resolveTerminalHistoryConfig, resolveTerminalHistoryConfig,
DEFAULT_TERMINAL_SCROLLBACK_LINES, DEFAULT_TERMINAL_SCROLLBACK_LINES,
@@ -22,11 +22,34 @@ describe('resolveTerminalHistoryConfig', () => {
}); });
}); });
it('defaults match the prior hardcoded values (behavior-neutral)', () => { it('defaults raise tmux history to 100k and buffer caps to 32MB/24MB; browser scrollback stays 50k', () => {
expect(DEFAULT_TMUX_HISTORY_LIMIT).toBe(50_000); expect(DEFAULT_TMUX_HISTORY_LIMIT).toBe(100_000);
// Matches the hardcoded browser-side DEFAULT_SCROLLBACK in src/web/public/constants.js —
// deliberately NOT raised (100k xterm lines per tab is a mobile-memory hazard).
expect(DEFAULT_TERMINAL_SCROLLBACK_LINES).toBe(50_000); expect(DEFAULT_TERMINAL_SCROLLBACK_LINES).toBe(50_000);
expect(DEFAULT_TERMINAL_BUFFER_MAX_BYTES).toBe(2 * 1024 * 1024); expect(DEFAULT_TERMINAL_BUFFER_MAX_BYTES).toBe(32 * 1024 * 1024);
expect(DEFAULT_TERMINAL_BUFFER_TRIM_BYTES).toBe(1.5 * 1024 * 1024); expect(DEFAULT_TERMINAL_BUFFER_TRIM_BYTES).toBe(24 * 1024 * 1024);
});
it('clamps the env-derived trim default below an env-lowered max (CODEMAN_MAX_TERMINAL_BUFFER only)', async () => {
const originalMax = process.env.CODEMAN_MAX_TERMINAL_BUFFER;
const originalTrim = process.env.CODEMAN_TRIM_TERMINAL_TO;
vi.resetModules();
process.env.CODEMAN_MAX_TERMINAL_BUFFER = String(2 * 1024 * 1024);
delete process.env.CODEMAN_TRIM_TERMINAL_TO;
try {
const mod = await import('../src/config/terminal-history.js');
expect(mod.DEFAULT_TERMINAL_BUFFER_MAX_BYTES).toBe(2 * 1024 * 1024);
// trim >= max would make BufferAccumulator.trim() a no-op (unbounded growth + O(n²) appends).
expect(mod.DEFAULT_TERMINAL_BUFFER_TRIM_BYTES).toBeLessThan(mod.DEFAULT_TERMINAL_BUFFER_MAX_BYTES);
expect(mod.DEFAULT_TERMINAL_BUFFER_TRIM_BYTES).toBe(Math.floor(2 * 1024 * 1024 * 0.75));
} finally {
if (originalMax === undefined) delete process.env.CODEMAN_MAX_TERMINAL_BUFFER;
else process.env.CODEMAN_MAX_TERMINAL_BUFFER = originalMax;
if (originalTrim === undefined) delete process.env.CODEMAN_TRIM_TERMINAL_TO;
else process.env.CODEMAN_TRIM_TERMINAL_TO = originalTrim;
vi.resetModules();
}
}); });
it('passes valid in-range values through unchanged', () => { it('passes valid in-range values through unchanged', () => {
@@ -80,9 +103,9 @@ describe('resolveTerminalHistoryConfig', () => {
}); });
it('caps the default trim to a lowered max', () => { it('caps the default trim to a lowered max', () => {
// Default trim (1.5MB) exceeds a 1MB max → trim must fall to the max. // Default trim (24MB) exceeds a 2MB max → trim must fall to the max.
const cfg = resolveTerminalHistoryConfig({ terminalBufferMaxBytes: 1 * 1024 * 1024 }); const cfg = resolveTerminalHistoryConfig({ terminalBufferMaxBytes: 2 * 1024 * 1024 });
expect(cfg.terminalBufferTrimBytes).toBe(1 * 1024 * 1024); expect(cfg.terminalBufferTrimBytes).toBe(2 * 1024 * 1024);
}); });
it('truncates fractional inputs to integers', () => { it('truncates fractional inputs to integers', () => {