chore: project setup and tooling cleanup (#26)

* add project setup

* add tools to eslint

* remove contributing.md

* update claude md

* chore: simplify CI to single Node.js version (22)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* use node 20

* adjust formatting

* fix linting

* format

* fix layout

* fix: align CI node version to .nvmrc (22), remove redundant gotcha

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: arkon <arkon.85@hotmail.com>
This commit is contained in:
Noah Waldner
2026-02-27 13:16:23 +01:00
committed by GitHub
co-authored by Claude Sonnet 4.6 arkon
parent 2b2829ebde
commit 480584de63
61 changed files with 4306 additions and 4013 deletions
+179 -99
View File
@@ -18,19 +18,26 @@
import { EventEmitter } from 'node:events';
import { v4 as uuidv4 } from 'uuid';
import * as pty from 'node-pty';
import { SessionState, SessionStatus, SessionConfig, RalphTrackerState, RalphTodoItem, ActiveBashTool, NiceConfig, DEFAULT_NICE_CONFIG, type ClaudeMode, type SessionMode, type OpenCodeConfig } from './types.js';
import {
SessionState,
SessionStatus,
SessionConfig,
RalphTrackerState,
RalphTodoItem,
ActiveBashTool,
NiceConfig,
DEFAULT_NICE_CONFIG,
type ClaudeMode,
type SessionMode,
type OpenCodeConfig,
} from './types.js';
import type { TerminalMultiplexer, MuxSession } from './mux-interface.js';
import { TaskTracker, type BackgroundTask } from './task-tracker.js';
import { RalphTracker } from './ralph-tracker.js';
import { BashToolParser } from './bash-tool-parser.js';
import { BufferAccumulator } from './utils/buffer-accumulator.js';
import { LRUMap } from './utils/lru-map.js';
import {
ANSI_ESCAPE_PATTERN_FULL,
TOKEN_PATTERN,
SPINNER_PATTERN,
MAX_SESSION_TOKENS,
} from './utils/index.js';
import { ANSI_ESCAPE_PATTERN_FULL, TOKEN_PATTERN, SPINNER_PATTERN, MAX_SESSION_TOKENS } from './utils/index.js';
import {
MAX_TERMINAL_BUFFER_SIZE,
TRIM_TERMINAL_TO as TERMINAL_BUFFER_TRIM_SIZE,
@@ -68,6 +75,7 @@ const GRACEFUL_SHUTDOWN_DELAY_MS = 100;
// Filter out terminal focus escape sequences (focus in/out reports)
// ^[[I (focus in), ^[[O (focus out), and the enable/disable sequences
// eslint-disable-next-line no-control-regex
const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
// Pattern to match Task tool invocations in terminal output
@@ -78,8 +86,10 @@ const TASK_TOOL_PATTERN = /\b(Explore|Task|Bash|Plan|general-purpose)\(([^)]+)\)
// Pre-compiled patterns for hot paths (avoid regex compilation per call)
/** Pattern to strip leading ANSI escapes and whitespace from terminal buffer */
// eslint-disable-next-line no-control-regex
const LEADING_ANSI_WHITESPACE_PATTERN = /^(\x1b\[\??[\d;]*[A-Za-z]|[\s\r\n])+/;
/** Pattern to match Ctrl+L (form feed) characters */
// eslint-disable-next-line no-control-regex
const CTRL_L_PATTERN = /\x0c/g;
/** Pattern to split by newlines (CR or LF) */
const NEWLINE_SPLIT_PATTERN = /\r?\n/;
@@ -96,11 +106,7 @@ import { getAugmentedPath } from './utils/claude-cli-resolver.js';
* @param operation - Description of the operation for error messages
* @returns Promise that resolves/rejects with the original result or timeout error
*/
function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
operation: string
): Promise<T> {
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, operation: string): Promise<T> {
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<never>((_, reject) => {
@@ -193,7 +199,12 @@ export interface SessionEvents {
/** Active Bash tools list updated */
bashToolsUpdate: (tools: ActiveBashTool[]) => void;
/** CLI info (version, model, account) updated */
cliInfoUpdated: (info: { version: string | null; model: string | null; accountType: string | null; latestVersion: string | null }) => void;
cliInfoUpdated: (info: {
version: string | null;
model: string | null;
accountType: string | null;
latestVersion: string | null;
}) => void;
}
// SessionMode is imported from types.ts (single source of truth)
@@ -258,7 +269,7 @@ export class Session extends EventEmitter {
private _lineBufferFlushTimer: NodeJS.Timeout | null = null;
private resolvePromise: ((value: { result: string; cost: number }) => void) | null = null;
private rejectPromise: ((reason: Error) => void) | null = null;
private _promptResolved: boolean = false; // Guard against race conditions in runPrompt
private _promptResolved: boolean = false; // Guard against race conditions in runPrompt
private _isWorking: boolean = false;
private _lastPromptTime: number = 0;
private activityTimeout: NodeJS.Timeout | null = null;
@@ -356,7 +367,9 @@ export class Session extends EventEmitter {
// Task descriptions parsed from terminal output (e.g., "Explore(Description)")
// Used to correlate with SubagentWatcher discoveries for better window titles
// Uses LRUMap for automatic eviction at MAX_TASK_DESCRIPTIONS limit
private _recentTaskDescriptions: LRUMap<number, string> = new LRUMap({ maxSize: Session.MAX_TASK_DESCRIPTIONS });
private _recentTaskDescriptions: LRUMap<number, string> = new LRUMap({
maxSize: Session.MAX_TASK_DESCRIPTIONS,
});
// Throttle expensive PTY processing (Ralph, bash parser, task descriptions)
// Accumulates clean data between processing windows to avoid running regex on every chunk
@@ -365,26 +378,28 @@ export class Session extends EventEmitter {
private _expensiveProcessTimer: NodeJS.Timeout | null = null;
private static readonly EXPENSIVE_PROCESS_INTERVAL_MS = 150; // Process at most every 150ms
constructor(config: Partial<SessionConfig> & {
workingDir: string;
mode?: SessionMode;
name?: string;
/** Terminal multiplexer instance (tmux) */
mux?: TerminalMultiplexer;
/** Whether to use multiplexer wrapping */
useMux?: boolean;
/** Existing mux session for restored sessions */
muxSession?: MuxSession;
niceConfig?: NiceConfig; // Nice prioritying configuration
/** Claude model override (e.g., 'opus', 'sonnet', 'haiku') */
model?: string;
/** Claude CLI startup permission mode */
claudeMode?: ClaudeMode;
/** Comma-separated allowed tools (for 'allowedTools' mode) */
allowedTools?: string;
/** OpenCode configuration (only for mode === 'opencode') */
openCodeConfig?: OpenCodeConfig;
}) {
constructor(
config: Partial<SessionConfig> & {
workingDir: string;
mode?: SessionMode;
name?: string;
/** Terminal multiplexer instance (tmux) */
mux?: TerminalMultiplexer;
/** Whether to use multiplexer wrapping */
useMux?: boolean;
/** Existing mux session for restored sessions */
muxSession?: MuxSession;
niceConfig?: NiceConfig; // Nice prioritying configuration
/** Claude model override (e.g., 'opus', 'sonnet', 'haiku') */
model?: string;
/** Claude CLI startup permission mode */
claudeMode?: ClaudeMode;
/** Comma-separated allowed tools (for 'allowedTools' mode) */
allowedTools?: string;
/** OpenCode configuration (only for mode === 'opencode') */
openCodeConfig?: OpenCodeConfig;
}
) {
super();
this.setMaxListeners(25);
@@ -472,7 +487,6 @@ export class Session extends EventEmitter {
this._bashToolParser.on('toolStart', this._bashToolHandlers.toolStart);
this._bashToolParser.on('toolEnd', this._bashToolHandlers.toolEnd);
this._bashToolParser.on('toolsUpdate', this._bashToolHandlers.toolsUpdate);
}
get status(): SessionStatus {
@@ -673,17 +687,23 @@ export class Session extends EventEmitter {
restoreTokens(inputTokens: number, outputTokens: number, totalCost: number): void {
// Sanity check: reject absurdly large individual values
if (inputTokens > MAX_SESSION_TOKENS || outputTokens > MAX_SESSION_TOKENS) {
console.warn(`[Session ${this.id}] Rejected absurd restored tokens: input=${inputTokens}, output=${outputTokens}`);
console.warn(
`[Session ${this.id}] Rejected absurd restored tokens: input=${inputTokens}, output=${outputTokens}`
);
return;
}
// Check token sum doesn't overflow MAX_SESSION_TOKENS
if (inputTokens + outputTokens > MAX_SESSION_TOKENS) {
console.warn(`[Session ${this.id}] Rejected token sum overflow: input=${inputTokens} + output=${outputTokens} = ${inputTokens + outputTokens} > ${MAX_SESSION_TOKENS}`);
console.warn(
`[Session ${this.id}] Rejected token sum overflow: input=${inputTokens} + output=${outputTokens} = ${inputTokens + outputTokens} > ${MAX_SESSION_TOKENS}`
);
return;
}
// Reject negative values
if (inputTokens < 0 || outputTokens < 0 || totalCost < 0) {
console.warn(`[Session ${this.id}] Rejected negative restored tokens: input=${inputTokens}, output=${outputTokens}, cost=${totalCost}`);
console.warn(
`[Session ${this.id}] Rejected negative restored tokens: input=${inputTokens}, output=${outputTokens}, cost=${totalCost}`
);
return;
}
@@ -722,7 +742,9 @@ export class Session extends EventEmitter {
if (threshold !== undefined) {
// Validate threshold bounds
if (threshold < Session.MIN_AUTO_THRESHOLD || threshold > Session.MAX_AUTO_THRESHOLD) {
console.warn(`[Session ${this.id}] Invalid autoClear threshold ${threshold}, must be between ${Session.MIN_AUTO_THRESHOLD} and ${Session.MAX_AUTO_THRESHOLD}. Using default ${Session.DEFAULT_AUTO_CLEAR_THRESHOLD}.`);
console.warn(
`[Session ${this.id}] Invalid autoClear threshold ${threshold}, must be between ${Session.MIN_AUTO_THRESHOLD} and ${Session.MAX_AUTO_THRESHOLD}. Using default ${Session.DEFAULT_AUTO_CLEAR_THRESHOLD}.`
);
this._autoClearThreshold = Session.DEFAULT_AUTO_CLEAR_THRESHOLD;
} else {
this._autoClearThreshold = threshold;
@@ -747,7 +769,9 @@ export class Session extends EventEmitter {
if (threshold !== undefined) {
// Validate threshold bounds
if (threshold < Session.MIN_AUTO_THRESHOLD || threshold > Session.MAX_AUTO_THRESHOLD) {
console.warn(`[Session ${this.id}] Invalid autoCompact threshold ${threshold}, must be between ${Session.MIN_AUTO_THRESHOLD} and ${Session.MAX_AUTO_THRESHOLD}. Using default ${Session.DEFAULT_AUTO_COMPACT_THRESHOLD}.`);
console.warn(
`[Session ${this.id}] Invalid autoCompact threshold ${threshold}, must be between ${Session.MIN_AUTO_THRESHOLD} and ${Session.MAX_AUTO_THRESHOLD}. Using default ${Session.DEFAULT_AUTO_COMPACT_THRESHOLD}.`
);
this._autoCompactThreshold = Session.DEFAULT_AUTO_COMPACT_THRESHOLD;
} else {
this._autoCompactThreshold = threshold;
@@ -911,7 +935,9 @@ export class Session extends EventEmitter {
this._lastActivityAt = Date.now();
const modeLabel = this.mode === 'opencode' ? 'OpenCode' : 'Claude';
console.log(`[Session] Starting interactive ${modeLabel} session` + (this._useMux ? ` (with ${this._mux!.backend})` : ''));
console.log(
`[Session] Starting interactive ${modeLabel} session` + (this._useMux ? ` (with ${this._mux!.backend})` : '')
);
// If mux wrapping is enabled, create or attach to a mux session
if (this._useMux && this._mux) {
@@ -928,16 +954,21 @@ export class Session extends EventEmitter {
if (this._muxSession && this._mux.isPaneDead(this._muxSession.muxName)) {
console.log('[Session] Dead pane detected, respawning:', this._muxSession.muxName);
const newPid = await this._mux.respawnPane({
sessionId: this.id, workingDir: this.workingDir, mode: this.mode,
niceConfig: this._niceConfig, model: this._model, claudeMode: this._claudeMode,
allowedTools: this._allowedTools, openCodeConfig: this._openCodeConfig,
sessionId: this.id,
workingDir: this.workingDir,
mode: this.mode,
niceConfig: this._niceConfig,
model: this._model,
claudeMode: this._claudeMode,
allowedTools: this._allowedTools,
openCodeConfig: this._openCodeConfig,
});
if (!newPid) {
console.error('[Session] Failed to respawn pane, will create new session');
needsNewSession = true;
} else {
// Wait a moment for the respawned process to fully start
await new Promise(resolve => setTimeout(resolve, MUX_STARTUP_DELAY_MS));
await new Promise((resolve) => setTimeout(resolve, MUX_STARTUP_DELAY_MS));
}
}
@@ -948,9 +979,14 @@ export class Session extends EventEmitter {
} else {
// Create a new mux session
this._muxSession = await this._mux.createSession({
sessionId: this.id, workingDir: this.workingDir, mode: this.mode,
name: this._name, niceConfig: this._niceConfig, model: this._model,
claudeMode: this._claudeMode, allowedTools: this._allowedTools,
sessionId: this.id,
workingDir: this.workingDir,
mode: this.mode,
name: this._name,
niceConfig: this._niceConfig,
model: this._model,
claudeMode: this._claudeMode,
allowedTools: this._allowedTools,
openCodeConfig: this._openCodeConfig,
});
console.log('[Session] Created mux session:', this._muxSession.muxName);
@@ -962,13 +998,21 @@ export class Session extends EventEmitter {
this.ptyProcess = pty.spawn(
this._mux.getAttachCommand(),
this._mux.getAttachArgs(this._muxSession!.muxName),
{
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: { ...process.env, LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8', TERM: 'xterm-256color', COLORTERM: undefined, CLAUDECODE: undefined },
});
{
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: {
...process.env,
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US.UTF-8',
TERM: 'xterm-256color',
COLORTERM: undefined,
CLAUDECODE: undefined,
},
}
);
// Set claudeSessionId immediately since we passed --session-id to Claude
// The mux manager passes --session-id ${sessionId} to Claude
@@ -1010,9 +1054,7 @@ export class Session extends EventEmitter {
// Clean the buffer - remove mux init junk before actual content
// Strip: cursor movement (\x1b[nA/B/C/D), positioning (\x1b[n;nH),
// clear screen (\x1b[2J), scroll region (\x1b[n;nr), and whitespace
this._terminalBuffer.set(
bufferValue.replace(LEADING_ANSI_WHITESPACE_PATTERN, '')
);
this._terminalBuffer.set(bufferValue.replace(LEADING_ANSI_WHITESPACE_PATTERN, ''));
// Signal client to refresh
this.emit('clearTerminal');
}
@@ -1081,9 +1123,7 @@ export class Session extends EventEmitter {
this.ptyProcess.onData((rawData: string) => {
// Filter out focus escape sequences and Ctrl+L (form feed)
const data = rawData
.replace(FOCUS_ESCAPE_FILTER, '')
.replace(CTRL_L_PATTERN, ''); // Remove Ctrl+L
const data = rawData.replace(FOCUS_ESCAPE_FILTER, '').replace(CTRL_L_PATTERN, ''); // Remove Ctrl+L
if (!data) return; // Skip if only filtered sequences
// BufferAccumulator handles auto-trimming when max size exceeded
@@ -1252,8 +1292,12 @@ export class Session extends EventEmitter {
// Only check if spinner didn't already trigger working state
if (!this._isWorking) {
const cleanData = getCleanData();
if (cleanData.includes('Thinking') || cleanData.includes('Writing') ||
cleanData.includes('Reading') || cleanData.includes('Running')) {
if (
cleanData.includes('Thinking') ||
cleanData.includes('Writing') ||
cleanData.includes('Reading') ||
cleanData.includes('Running')
) {
this._isWorking = true;
this._status = 'busy';
this.emit('working');
@@ -1293,7 +1337,10 @@ export class Session extends EventEmitter {
// Use user's default shell or bash
const shell = process.env.SHELL || '/bin/bash';
console.log('[Session] Starting shell session with:', shell + (this._useMux ? ` (with ${this._mux!.backend})` : ''));
console.log(
'[Session] Starting shell session with:',
shell + (this._useMux ? ` (with ${this._mux!.backend})` : '')
);
// If mux wrapping is enabled, create or attach to a mux session
if (this._useMux && this._mux) {
@@ -1308,12 +1355,17 @@ export class Session extends EventEmitter {
let needsNewSession = false;
if (this._muxSession && this._mux.isPaneDead(this._muxSession.muxName)) {
console.log('[Session] Dead pane detected, respawning:', this._muxSession.muxName);
const newPid = await this._mux.respawnPane({ sessionId: this.id, workingDir: this.workingDir, mode: 'shell', niceConfig: this._niceConfig });
const newPid = await this._mux.respawnPane({
sessionId: this.id,
workingDir: this.workingDir,
mode: 'shell',
niceConfig: this._niceConfig,
});
if (!newPid) {
console.error('[Session] Failed to respawn pane, will create new session');
needsNewSession = true;
} else {
await new Promise(resolve => setTimeout(resolve, MUX_STARTUP_DELAY_MS));
await new Promise((resolve) => setTimeout(resolve, MUX_STARTUP_DELAY_MS));
}
}
@@ -1323,7 +1375,13 @@ export class Session extends EventEmitter {
console.log('[Session] Attaching to existing mux session:', this._muxSession!.muxName);
} else {
// Create a new mux session
this._muxSession = await this._mux.createSession({ sessionId: this.id, workingDir: this.workingDir, mode: 'shell', name: this._name, niceConfig: this._niceConfig });
this._muxSession = await this._mux.createSession({
sessionId: this.id,
workingDir: this.workingDir,
mode: 'shell',
name: this._name,
niceConfig: this._niceConfig,
});
console.log('[Session] Created mux session:', this._muxSession.muxName);
// No extra sleep — createSession() already waits for tmux readiness
}
@@ -1333,13 +1391,21 @@ export class Session extends EventEmitter {
this.ptyProcess = pty.spawn(
this._mux.getAttachCommand(),
this._mux.getAttachArgs(this._muxSession!.muxName),
{
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: { ...process.env, LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8', TERM: 'xterm-256color', COLORTERM: undefined, CLAUDECODE: undefined },
});
{
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: {
...process.env,
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US.UTF-8',
TERM: 'xterm-256color',
COLORTERM: undefined,
CLAUDECODE: undefined,
},
}
);
} catch (spawnErr) {
console.error('[Session] Failed to spawn PTY for shell mux attachment:', spawnErr);
this.emit('error', `Failed to attach to mux session: ${spawnErr}`);
@@ -1474,7 +1540,7 @@ export class Session extends EventEmitter {
this._messages = [];
this._lineBuffer = '';
this._lastActivityAt = Date.now();
this._promptResolved = false; // Reset race condition guard
this._promptResolved = false; // Reset race condition guard
this.resolvePromise = resolve;
this.rejectPromise = reject;
@@ -1482,14 +1548,13 @@ export class Session extends EventEmitter {
try {
// Spawn claude in a real PTY
const model = options?.model;
console.log('[Session] Spawning PTY for claude with prompt:', prompt.substring(0, 50), model ? `(model: ${model})` : '');
console.log(
'[Session] Spawning PTY for claude with prompt:',
prompt.substring(0, 50),
model ? `(model: ${model})` : ''
);
const args = [
'-p',
'--verbose',
'--dangerously-skip-permissions',
'--output-format', 'stream-json',
];
const args = ['-p', '--verbose', '--dangerously-skip-permissions', '--output-format', 'stream-json'];
if (model) {
args.push('--model', model);
}
@@ -1517,7 +1582,10 @@ export class Session extends EventEmitter {
});
} catch (spawnErr) {
console.error('[Session] Failed to spawn Claude PTY for runPrompt:', spawnErr);
this.emit('error', `Failed to spawn Claude: ${spawnErr instanceof Error ? spawnErr.message : String(spawnErr)}`);
this.emit(
'error',
`Failed to spawn Claude: ${spawnErr instanceof Error ? spawnErr.message : String(spawnErr)}`
);
throw spawnErr;
}
@@ -1561,7 +1629,7 @@ export class Session extends EventEmitter {
this.rejectPromise = null;
// Find result from parsed messages or use text output
const resultMsg = this._messages.find(m => m.type === 'result');
const resultMsg = this._messages.find((m) => m.type === 'result');
if (resultMsg && !resultMsg.is_error) {
this._status = 'idle';
@@ -1579,13 +1647,15 @@ export class Session extends EventEmitter {
} else {
this._status = 'idle';
if (resolve) {
resolve({ result: this._textOutput.value || this._terminalBuffer.value, cost: this._totalCost });
resolve({
result: this._textOutput.value || this._terminalBuffer.value,
cost: this._totalCost,
});
}
}
this.emit('exit', exitCode);
});
} catch (err) {
this._status = 'error';
reject(err);
@@ -1649,8 +1719,8 @@ export class Session extends EventEmitter {
// Extract Claude session ID from messages (can be in any message type)
// Support both sessionId (camelCase) and session_id (snake_case)
const msgSessionId = (msg as unknown as Record<string, unknown>).sessionId as string | undefined
?? msg.session_id;
const msgSessionId =
((msg as unknown as Record<string, unknown>).sessionId as string | undefined) ?? msg.session_id;
if (msgSessionId && !this._claudeSessionId) {
this._claudeSessionId = msgSessionId;
}
@@ -1689,7 +1759,10 @@ export class Session extends EventEmitter {
}
} catch (parseErr) {
// Not JSON, just regular output - this is expected for non-JSON lines
console.debug('[Session] Line not JSON (expected for text output):', parseErr instanceof Error ? parseErr.message : parseErr);
console.debug(
'[Session] Line not JSON (expected for text output):',
parseErr instanceof Error ? parseErr.message : parseErr
);
this._textOutput.append(line + '\n');
}
} else if (trimmed) {
@@ -1829,7 +1902,9 @@ export class Session extends EventEmitter {
// Safety: Reject M values that would result in > 500k tokens
// Claude's context window is ~200k, so anything claiming millions is likely a false match
if (tokenCount > 0.5) {
console.warn(`[Session ${this.id}] Rejected suspicious M token value: ${tokenMatch[0]} (would be ${tokenCount * 1000000} tokens)`);
console.warn(
`[Session ${this.id}] Rejected suspicious M token value: ${tokenMatch[0]} (would be ${tokenCount * 1000000} tokens)`
);
return;
}
tokenCount *= 1000000;
@@ -1850,7 +1925,9 @@ export class Session extends EventEmitter {
// Safety: Reject suspiciously large jumps (max 100k per update)
const MAX_DELTA_PER_UPDATE = 100_000;
if (delta > MAX_DELTA_PER_UPDATE) {
console.warn(`[Session ${this.id}] Rejected suspicious token jump: ${currentTotal} -> ${tokenCount} (delta: ${delta})`);
console.warn(
`[Session ${this.id}] Rejected suspicious token jump: ${currentTotal} -> ${tokenCount} (delta: ${delta})`
);
return;
}
@@ -1874,7 +1951,12 @@ export class Session extends EventEmitter {
if (this._cliInfoParsed) return;
// Quick pre-checks
if (!cleanData.includes('Claude') && !cleanData.includes('current:') && !cleanData.includes('Opus') && !cleanData.includes('Sonnet')) {
if (
!cleanData.includes('Claude') &&
!cleanData.includes('current:') &&
!cleanData.includes('Opus') &&
!cleanData.includes('Sonnet')
) {
return;
}
let changed = false;
@@ -1960,14 +2042,12 @@ export class Session extends EventEmitter {
if (this._isStopped) return;
// Send /compact command with optional prompt
const compactCmd = this._autoCompactPrompt
? `/compact ${this._autoCompactPrompt}\r`
: '/compact\r';
const compactCmd = this._autoCompactPrompt ? `/compact ${this._autoCompactPrompt}\r` : '/compact\r';
await this.writeViaMux(compactCmd);
this.emit('autoCompact', {
tokens: totalTokens,
threshold: this._autoCompactThreshold,
prompt: this._autoCompactPrompt || undefined
prompt: this._autoCompactPrompt || undefined,
});
// Wait a moment then re-enable (longer than clear since compact takes time)
@@ -2121,7 +2201,7 @@ export class Session extends EventEmitter {
async sendInput(input: string): Promise<void> {
this._status = 'busy';
this._lastActivityAt = Date.now();
this.runPrompt(input).catch(err => {
this.runPrompt(input).catch((err) => {
const errorMsg = err instanceof Error ? err.message : String(err);
this.emit('error', errorMsg);
});
@@ -2260,7 +2340,7 @@ export class Session extends EventEmitter {
}
// Give it a moment to terminate gracefully
await new Promise(resolve => setTimeout(resolve, GRACEFUL_SHUTDOWN_DELAY_MS));
await new Promise((resolve) => setTimeout(resolve, GRACEFUL_SHUTDOWN_DELAY_MS));
// Force kill with SIGKILL if still alive
try {