mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
fix: use temp file for plan checker prompt to avoid E2BIG + fix cancel race condition
The ai-plan-checker.ts was passing the prompt directly as a shell argument, which can cause E2BIG errors when the terminal buffer is large (8KB+). This fix applies the same temp file approach already used in ai-idle-checker.ts: - Write prompt to a temp file instead of passing as shell argument - Pipe the file to claude via stdin: `cat prompt.txt | claude -p ...` - Clean up prompt file after check completes Also fixes a race condition in the cancel() method of both AI checkers where the poll timer could fire between setting checkCancelled and clearing timers. Now timers are cleared before resolving the promise to prevent this race. Includes test utilities and analysis documents for the respawn controller created by other agents: - test/respawn-test-utils.ts - MockSession, MockAiIdleChecker utilities - test/respawn-analysis.md - Code analysis and issue identification - test/respawn-scenarios.md - Test scenario documentation - test/respawn-test-plan.md - Testing architecture documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -268,13 +268,16 @@ export class AiIdleChecker extends EventEmitter {
|
||||
this.log('Cancelling AI check');
|
||||
this.checkCancelled = true;
|
||||
|
||||
// Resolve the pending promise before cleanup
|
||||
// Clear poll/timeout timers first to prevent race condition where
|
||||
// the poll timer fires between setting checkCancelled and cleanup
|
||||
this.cleanupCheck();
|
||||
|
||||
// Resolve the pending promise after cleanup
|
||||
if (this.checkResolve) {
|
||||
this.checkResolve({ verdict: 'ERROR', reasoning: 'Cancelled', durationMs: Date.now() - this.checkStartTime });
|
||||
this.checkResolve = null;
|
||||
}
|
||||
|
||||
this.cleanupCheck();
|
||||
this._status = 'ready';
|
||||
}
|
||||
|
||||
|
||||
+28
-9
@@ -152,6 +152,7 @@ export class AiPlanChecker extends EventEmitter {
|
||||
// Active check state
|
||||
private checkScreenName: string | null = null;
|
||||
private checkTempFile: string | null = null;
|
||||
private checkPromptFile: string | null = null;
|
||||
private checkPollTimer: NodeJS.Timeout | null = null;
|
||||
private checkTimeoutTimer: NodeJS.Timeout | null = null;
|
||||
private checkStartTime: number = 0;
|
||||
@@ -276,13 +277,16 @@ export class AiPlanChecker extends EventEmitter {
|
||||
this.log('Cancelling AI plan check');
|
||||
this.checkCancelled = true;
|
||||
|
||||
// Resolve the pending promise before cleanup
|
||||
// Clear poll/timeout timers first to prevent race condition where
|
||||
// the poll timer fires between setting checkCancelled and cleanup
|
||||
this.cleanupCheck();
|
||||
|
||||
// Resolve the pending promise after cleanup
|
||||
if (this.checkResolve) {
|
||||
this.checkResolve({ verdict: 'ERROR', reasoning: 'Cancelled', durationMs: Date.now() - this.checkStartTime });
|
||||
this.checkResolve = null;
|
||||
}
|
||||
|
||||
this.cleanupCheck();
|
||||
this._status = 'ready';
|
||||
}
|
||||
|
||||
@@ -329,21 +333,25 @@ export class AiPlanChecker extends EventEmitter {
|
||||
// Build the prompt
|
||||
const prompt = AI_PLAN_CHECK_PROMPT.replace('{TERMINAL_BUFFER}', trimmed);
|
||||
|
||||
// Generate temp file and screen name
|
||||
// Generate temp files and screen name
|
||||
const shortId = this.sessionId.slice(0, 8);
|
||||
const timestamp = Date.now();
|
||||
this.checkTempFile = join(tmpdir(), `claudeman-plancheck-${shortId}-${timestamp}.txt`);
|
||||
this.checkPromptFile = join(tmpdir(), `claudeman-plancheck-prompt-${shortId}-${timestamp}.txt`);
|
||||
this.checkScreenName = `claudeman-plancheck-${shortId}`;
|
||||
|
||||
// Ensure temp file exists (empty) so we can poll it
|
||||
// Ensure output temp file exists (empty) so we can poll it
|
||||
writeFileSync(this.checkTempFile, '');
|
||||
|
||||
// Build the command - escape the prompt for shell
|
||||
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
||||
// Write prompt to file to avoid E2BIG error (argument list too long)
|
||||
// The prompt can be 8KB+ which exceeds shell argument limits
|
||||
writeFileSync(this.checkPromptFile, prompt);
|
||||
|
||||
// Build the command - read prompt from file via stdin to avoid argument size limits
|
||||
const modelArg = `--model ${this.config.model}`;
|
||||
const augmentedPath = getAugmentedPath();
|
||||
const claudeCmd = `claude -p ${modelArg} --output-format text '${escapedPrompt}'`;
|
||||
const fullCmd = `export PATH="${augmentedPath}"; ${claudeCmd} > "${this.checkTempFile}" 2>&1; echo "${DONE_MARKER}" >> "${this.checkTempFile}"`;
|
||||
const claudeCmd = `cat "${this.checkPromptFile}" | claude -p ${modelArg} --output-format text`;
|
||||
const fullCmd = `export PATH="${augmentedPath}"; ${claudeCmd} > "${this.checkTempFile}" 2>&1; echo "${DONE_MARKER}" >> "${this.checkTempFile}"; rm -f "${this.checkPromptFile}"`;
|
||||
|
||||
// Spawn screen
|
||||
try {
|
||||
@@ -447,7 +455,7 @@ export class AiPlanChecker extends EventEmitter {
|
||||
this.checkScreenName = null;
|
||||
}
|
||||
|
||||
// Delete temp file
|
||||
// Delete temp files
|
||||
if (this.checkTempFile) {
|
||||
try {
|
||||
if (existsSync(this.checkTempFile)) {
|
||||
@@ -458,6 +466,17 @@ export class AiPlanChecker extends EventEmitter {
|
||||
}
|
||||
this.checkTempFile = null;
|
||||
}
|
||||
|
||||
if (this.checkPromptFile) {
|
||||
try {
|
||||
if (existsSync(this.checkPromptFile)) {
|
||||
unlinkSync(this.checkPromptFile);
|
||||
}
|
||||
} catch {
|
||||
// Best effort cleanup
|
||||
}
|
||||
this.checkPromptFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(errorMsg: string): void {
|
||||
|
||||
Reference in New Issue
Block a user