fix: address PR review issues for auto-mode refactor

- agent-executor: move executeQuery into try block for proper heartbeat cleanup,
  re-parse tasks when edited plan is approved
- auto-loop-coordinator: handle feature execution failures with proper logging
  and failure tracking, support backward-compatible method signatures
- facade: delegate getActiveAutoLoopProjects/Worktrees to coordinator,
  always create own AutoLoopCoordinator (not shared), pass projectPath
  to approval methods and branchName to failure tracking
- global-service: document shared autoLoopCoordinator is for monitoring only
- execution-types: fix ExecuteFeatureFn type to match implementation
- feature-state-manager: use readJsonWithRecovery for loadFeature
- pipeline-orchestrator: add defensive null check and try/catch for
  merge response parsing
- plan-approval-service: use project-scoped keys to prevent cross-project
  collisions, maintain backward compatibility for featureId-only lookups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Shirone
2026-01-31 13:59:24 +01:00
committed by gsxdsm
parent 63b1a353d9
commit 7c89923a6e
9 changed files with 214 additions and 91 deletions

View File

@@ -183,7 +183,13 @@ export class AutoLoopCoordinator {
nextFeature.id,
projectState.config.useWorktrees,
true
).catch(() => {});
).catch((error) => {
const errorInfo = classifyError(error);
logger.error(`Auto-loop feature ${nextFeature.id} failed:`, errorInfo.message);
if (this.trackFailureAndCheckPauseForProject(projectPath, branchName, errorInfo)) {
this.signalShouldPauseForProject(projectPath, branchName, errorInfo);
}
});
}
await this.sleep(2000, projectState.abortController.signal);
} catch {
@@ -268,27 +274,64 @@ export class AutoLoopCoordinator {
trackFailureAndCheckPauseForProject(
projectPath: string,
errorInfo: { type: string; message: string }
branchNameOrError: string | null | { type: string; message: string },
errorInfo?: { type: string; message: string }
): boolean {
const projectState = this.autoLoopsByProject.get(getWorktreeAutoLoopKey(projectPath, null));
// Support both old (projectPath, errorInfo) and new (projectPath, branchName, errorInfo) signatures
let branchName: string | null;
let actualErrorInfo: { type: string; message: string };
if (
typeof branchNameOrError === 'object' &&
branchNameOrError !== null &&
'type' in branchNameOrError
) {
// Old signature: (projectPath, errorInfo)
branchName = null;
actualErrorInfo = branchNameOrError;
} else {
// New signature: (projectPath, branchName, errorInfo)
branchName = branchNameOrError;
actualErrorInfo = errorInfo!;
}
const projectState = this.autoLoopsByProject.get(
getWorktreeAutoLoopKey(projectPath, branchName)
);
if (!projectState) return false;
const now = Date.now();
projectState.consecutiveFailures.push({ timestamp: now, error: errorInfo.message });
projectState.consecutiveFailures.push({ timestamp: now, error: actualErrorInfo.message });
projectState.consecutiveFailures = projectState.consecutiveFailures.filter(
(f) => now - f.timestamp < FAILURE_WINDOW_MS
);
return (
projectState.consecutiveFailures.length >= CONSECUTIVE_FAILURE_THRESHOLD ||
errorInfo.type === 'quota_exhausted' ||
errorInfo.type === 'rate_limit'
actualErrorInfo.type === 'quota_exhausted' ||
actualErrorInfo.type === 'rate_limit'
);
}
signalShouldPauseForProject(
projectPath: string,
errorInfo: { type: string; message: string }
branchNameOrError: string | null | { type: string; message: string },
errorInfo?: { type: string; message: string }
): void {
const projectState = this.autoLoopsByProject.get(getWorktreeAutoLoopKey(projectPath, null));
// Support both old (projectPath, errorInfo) and new (projectPath, branchName, errorInfo) signatures
let branchName: string | null;
let actualErrorInfo: { type: string; message: string };
if (
typeof branchNameOrError === 'object' &&
branchNameOrError !== null &&
'type' in branchNameOrError
) {
branchName = null;
actualErrorInfo = branchNameOrError;
} else {
branchName = branchNameOrError;
actualErrorInfo = errorInfo!;
}
const projectState = this.autoLoopsByProject.get(
getWorktreeAutoLoopKey(projectPath, branchName)
);
if (!projectState || projectState.pausedDueToFailures) return;
projectState.pausedDueToFailures = true;
const failureCount = projectState.consecutiveFailures.length;
@@ -297,24 +340,29 @@ export class AutoLoopCoordinator {
failureCount >= CONSECUTIVE_FAILURE_THRESHOLD
? `Auto Mode paused: ${failureCount} consecutive failures detected.`
: 'Auto Mode paused: Usage limit or API error detected.',
errorType: errorInfo.type,
originalError: errorInfo.message,
errorType: actualErrorInfo.type,
originalError: actualErrorInfo.message,
failureCount,
projectPath,
branchName,
});
this.stopAutoLoopForProject(projectPath);
this.stopAutoLoopForProject(projectPath, branchName);
}
resetFailureTrackingForProject(projectPath: string): void {
const projectState = this.autoLoopsByProject.get(getWorktreeAutoLoopKey(projectPath, null));
resetFailureTrackingForProject(projectPath: string, branchName: string | null = null): void {
const projectState = this.autoLoopsByProject.get(
getWorktreeAutoLoopKey(projectPath, branchName)
);
if (projectState) {
projectState.consecutiveFailures = [];
projectState.pausedDueToFailures = false;
}
}
recordSuccessForProject(projectPath: string): void {
const projectState = this.autoLoopsByProject.get(getWorktreeAutoLoopKey(projectPath, null));
recordSuccessForProject(projectPath: string, branchName: string | null = null): void {
const projectState = this.autoLoopsByProject.get(
getWorktreeAutoLoopKey(projectPath, branchName)
);
if (projectState) projectState.consecutiveFailures = [];
}