COD-80 centralize terminal history/scrollback/buffer limits into config

Introduce src/config/terminal-history.ts: one place for terminal scrollback,
tmux history-limit, and PTY buffer byte caps, each overridable via env var or
the settings object and bounds-clamped via resolveTerminalHistoryConfig().
Defaults match the prior hardcoded values, so this is behavior-neutral. Wires
the resolver through buffer-limits, tmux-manager (incl. a setHistoryLimit so a
settings change applies live), session, server, system-routes, session-routes,
schemas, and the config port. Adds 4 optional settings keys (terminalScrollback
Lines, tmuxHistoryLimit, terminalBufferMaxBytes, terminalBufferTrimBytes) with
bounds + a trim<=max cross-check.
This commit is contained in:
Aamer Akhter
2026-06-30 19:52:49 -04:00
parent abb6447f66
commit 8c0c94540c
13 changed files with 355 additions and 6 deletions
+2
View File
@@ -5,6 +5,7 @@
import type { ClaudeMode, NiceConfig } from '../../types.js';
import type { StateStore } from '../../state-store.js';
import type { TerminalHistoryConfig } from '../../config/terminal-history.js';
export interface ConfigPort {
readonly store: StateStore;
@@ -15,6 +16,7 @@ export interface ConfigPort {
getGlobalNiceConfig(): Promise<NiceConfig | undefined>;
getModelConfig(): Promise<{ defaultModel?: string; agentTypeOverrides?: Record<string, string> } | null>;
getClaudeModeConfig(): Promise<{ claudeMode?: ClaudeMode; allowedTools?: string }>;
getTerminalHistoryConfig(): Promise<TerminalHistoryConfig>;
getDefaultClaudeMdPath(): Promise<string | undefined>;
getLightState(): unknown;
getLightSessionsState(): unknown[];
+4
View File
@@ -406,6 +406,7 @@ export function registerSessionRoutes(
? modelConfig?.defaultModel || undefined
: undefined;
const claudeModeConfig = await ctx.getClaudeModeConfig();
const terminalHistoryConfig = await ctx.getTerminalHistoryConfig();
const session = new Session({
workingDir,
mode,
@@ -422,6 +423,7 @@ export function registerSessionRoutes(
resumeSessionId: validatedResumeId,
envOverrides: body.envOverrides,
effort: body.effort,
tmuxHistoryLimit: terminalHistoryConfig.tmuxHistoryLimit,
});
ctx.addSession(session);
@@ -1362,6 +1364,7 @@ export function registerSessionRoutes(
? qsModelConfig?.defaultModel || undefined
: undefined;
const qsClaudeModeConfig = await ctx.getClaudeModeConfig();
const qsTerminalHistoryConfig = await ctx.getTerminalHistoryConfig();
const session = new Session({
workingDir: casePath,
mux: ctx.mux,
@@ -1376,6 +1379,7 @@ export function registerSessionRoutes(
geminiConfig: mode === 'gemini' ? geminiConfig : undefined,
envOverrides,
effort,
tmuxHistoryLimit: qsTerminalHistoryConfig.tmuxHistoryLimit,
});
// Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
+6
View File
@@ -49,6 +49,7 @@ import type { SessionPort, EventPort, ConfigPort, InfraPort, AuthPort } from '..
import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
import { QR_AUTH_FAILURE_MAX } from '../../config/tunnel-config.js';
import { AUTH_SESSION_TTL_MS } from '../../config/auth-config.js';
import { resolveTerminalHistoryConfig } from '../../config/terminal-history.js';
// Maximum screenshot upload size (10MB)
const MAX_SCREENSHOT_SIZE = 10 * 1024 * 1024;
@@ -624,6 +625,11 @@ export function registerSystemRoutes(
const merged = { ...existing, ...settingsToStore };
await fs.writeFile(SETTINGS_PATH, JSON.stringify(merged, null, 2));
// Apply a changed tmux history-limit to all live sessions immediately.
if (settings.tmuxHistoryLimit !== undefined) {
await ctx.mux.setHistoryLimit(resolveTerminalHistoryConfig(merged).tmuxHistoryLimit);
}
// Handle subagent tracking toggle dynamically
toggleService((settings.subagentTrackingEnabled as boolean) ?? true, subagentWatcher, 'Subagent watcher');
+30 -1
View File
@@ -9,6 +9,12 @@
import { z } from 'zod';
import { SAFE_PATH_PATTERN, isSafePushEndpoint } from '../utils/index.js';
import {
MAX_TERMINAL_BUFFER_BYTES,
MAX_TERMINAL_SCROLLBACK_LINES,
MIN_TERMINAL_BUFFER_BYTES,
MIN_TERMINAL_SCROLLBACK_LINES,
} from '../config/terminal-history.js';
// ========== Path Validation ==========
@@ -412,6 +418,16 @@ export const SettingsUpdateSchema = z
allowedTools: z.string().max(2000).optional(),
// Codex CLI settings
codexDangerouslyBypassApprovals: z.boolean().optional(),
// Terminal history and retention
terminalScrollbackLines: z
.number()
.int()
.min(MIN_TERMINAL_SCROLLBACK_LINES)
.max(MAX_TERMINAL_SCROLLBACK_LINES)
.optional(),
tmuxHistoryLimit: z.number().int().min(MIN_TERMINAL_SCROLLBACK_LINES).max(MAX_TERMINAL_SCROLLBACK_LINES).optional(),
terminalBufferMaxBytes: z.number().int().min(MIN_TERMINAL_BUFFER_BYTES).max(MAX_TERMINAL_BUFFER_BYTES).optional(),
terminalBufferTrimBytes: z.number().int().min(MIN_TERMINAL_BUFFER_BYTES).max(MAX_TERMINAL_BUFFER_BYTES).optional(),
// CPU priority
nice: z
.object({
@@ -480,7 +496,20 @@ export const SettingsUpdateSchema = z
.max(20)
.optional(),
})
.strict();
.strict()
.superRefine((settings, ctx) => {
if (
settings.terminalBufferMaxBytes !== undefined &&
settings.terminalBufferTrimBytes !== undefined &&
settings.terminalBufferTrimBytes > settings.terminalBufferMaxBytes
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['terminalBufferTrimBytes'],
message: 'terminalBufferTrimBytes must be less than or equal to terminalBufferMaxBytes',
});
}
});
/**
* Schema for POST /api/sessions/:id/input with length limit
+8
View File
@@ -129,6 +129,7 @@ import {
import type { EventLoopMonitorHandle } from '../utils/index.js';
import { MAX_CONCURRENT_SESSIONS, MAX_SSE_CLIENTS } from '../config/map-limits.js';
import { MAX_PASTE_IMAGE_BYTES } from '../config/buffer-limits.js';
import { resolveTerminalHistoryConfig } from '../config/terminal-history.js';
import { SseEvent } from './sse-events.js';
import { getLatestPlanUsage } from './plan-usage-latest.js';
import type { ScheduledRun } from './ports/index.js';
@@ -587,6 +588,7 @@ export class WebServer extends EventEmitter {
getGlobalNiceConfig: this.getGlobalNiceConfig.bind(this),
getModelConfig: this.getModelConfig.bind(this),
getClaudeModeConfig: this.getClaudeModeConfig.bind(this),
getTerminalHistoryConfig: this.getTerminalHistoryConfig.bind(this),
getDefaultClaudeMdPath: this.getDefaultClaudeMdPath.bind(this),
getLightState: this.getLightState.bind(this),
getLightSessionsState: this.getLightSessionsState.bind(this),
@@ -1463,6 +1465,12 @@ export class WebServer extends EventEmitter {
return {};
}
// Resolve the bounds-clamped terminal-history config from settings.json.
private async getTerminalHistoryConfig() {
const settings = await this.readSettings();
return resolveTerminalHistoryConfig(settings);
}
// Helper to get model configuration from settings
private async getModelConfig(): Promise<{
defaultModel?: string;