mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
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:
co-authored by
Claude Sonnet 4.6
arkon
parent
2b2829ebde
commit
480584de63
+179
-99
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user