mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 14:09:42 +02:00
fix: improve memory safety and regex pattern handling
- Add withTimeout utility for async operation protection - Move Promise callback cleanup earlier in stop() to prevent orphaned refs - Fix regex lastIndex resets in inner-loop-tracker (reset BEFORE test) - Add DEFAULT_ASYNC_TIMEOUT_MS and INTERACTIVE_START_TIMEOUT_MS constants Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -461,21 +461,21 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
}
|
||||
|
||||
// Todo checkboxes: "- [ ] Task" or "- [x] Task"
|
||||
// Reset lastIndex BEFORE test to ensure consistent matching with /g flag patterns
|
||||
TODO_CHECKBOX_PATTERN.lastIndex = 0;
|
||||
if (TODO_CHECKBOX_PATTERN.test(data)) {
|
||||
// Reset lastIndex since we're reusing the global regex
|
||||
TODO_CHECKBOX_PATTERN.lastIndex = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Todo indicator icons: "Todo: ☐", "Todo: ◐", etc.
|
||||
TODO_INDICATOR_PATTERN.lastIndex = 0;
|
||||
if (TODO_INDICATOR_PATTERN.test(data)) {
|
||||
TODO_INDICATOR_PATTERN.lastIndex = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Claude Code native todo format: "☐ Task", "☒ Task"
|
||||
TODO_NATIVE_PATTERN.lastIndex = 0;
|
||||
if (TODO_NATIVE_PATTERN.test(data)) {
|
||||
TODO_NATIVE_PATTERN.lastIndex = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+42
-6
@@ -26,6 +26,7 @@ import { ScreenManager } from './screen-manager.js';
|
||||
|
||||
export type { BackgroundTask } from './task-tracker.js';
|
||||
export type { InnerLoopState, InnerTodoItem } from './types.js';
|
||||
export { withTimeout };
|
||||
|
||||
// ============================================================================
|
||||
// Buffer Size Constants
|
||||
@@ -52,6 +53,12 @@ const MAX_LINE_BUFFER_SIZE = 64 * 1024;
|
||||
/** Line buffer flush interval (100ms) - forces processing of partial lines */
|
||||
const LINE_BUFFER_FLUSH_INTERVAL = 100;
|
||||
|
||||
/** Default timeout for async operations in milliseconds (5 minutes) */
|
||||
const DEFAULT_ASYNC_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
|
||||
/** Timeout for interactive session startup in milliseconds (30 seconds) */
|
||||
const INTERACTIVE_START_TIMEOUT_MS = 30 * 1000;
|
||||
|
||||
// Filter out terminal focus escape sequences (focus in/out reports)
|
||||
// ^[[I (focus in), ^[[O (focus out), and the enable/disable sequences
|
||||
const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
|
||||
@@ -131,6 +138,33 @@ class BufferAccumulator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a promise with a timeout to prevent indefinite hangs.
|
||||
* If the promise doesn't resolve within the timeout, rejects with TimeoutError.
|
||||
*
|
||||
* @param promise - The promise to wrap
|
||||
* @param timeoutMs - Timeout in milliseconds
|
||||
* @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> {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(`${operation} timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
return Promise.race([promise, timeoutPromise]).finally(() => {
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a JSON message from Claude CLI's stream-json output format.
|
||||
* Messages are newline-delimited JSON objects parsed from PTY output.
|
||||
@@ -1253,6 +1287,14 @@ export class Session extends EventEmitter {
|
||||
this._lineBufferFlushTimer = null;
|
||||
}
|
||||
|
||||
// Immediately cleanup Promise callbacks to prevent orphaned references
|
||||
// during the rest of stop() processing (e.g., if screen kill times out)
|
||||
if (this.rejectPromise) {
|
||||
this.rejectPromise(new Error('Session stopped'));
|
||||
}
|
||||
this.resolvePromise = null;
|
||||
this.rejectPromise = null;
|
||||
|
||||
if (this.ptyProcess) {
|
||||
const pid = this.ptyProcess.pid;
|
||||
|
||||
@@ -1306,12 +1348,6 @@ export class Session extends EventEmitter {
|
||||
console.log('[Session] Keeping screen session alive:', this._screenSession.screenName);
|
||||
this._screenSession = null; // Detach but don't kill
|
||||
}
|
||||
|
||||
if (this.rejectPromise) {
|
||||
this.rejectPromise(new Error('Session stopped'));
|
||||
this.resolvePromise = null;
|
||||
this.rejectPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
assignTask(taskId: string): void {
|
||||
|
||||
Reference in New Issue
Block a user