refactor: Improve error handling and status preservation in auto-mode service

- Simplified the graceful shutdown process by removing redundant error handling for marking features as interrupted, as it is now managed internally.
- Updated orphan detection logging to streamline the process and enhance clarity.
- Added logic to preserve specific pipeline statuses when marking features as interrupted, ensuring correct resumption of features after a server restart.
- Enhanced unit tests to cover new behavior for preserving pipeline statuses and handling various feature states.
This commit is contained in:
Shirone
2026-01-25 14:57:23 +01:00
parent 011ac404bb
commit ef779daedf
4 changed files with 156 additions and 21 deletions

View File

@@ -787,11 +787,8 @@ const gracefulShutdown = async (signal: string) => {
// Mark all running features as interrupted before shutdown
// This ensures they can be resumed when the server restarts
try {
await autoModeService.markAllRunningFeaturesInterrupted(`${signal} signal received`);
} catch (error) {
logger.error('Failed to mark running features as interrupted:', error);
}
// Note: markAllRunningFeaturesInterrupted handles errors internally and never rejects
await autoModeService.markAllRunningFeaturesInterrupted(`${signal} signal received`);
terminalService.cleanup();
server.close(() => {

View File

@@ -28,25 +28,20 @@ export function createListHandler(featureLoader: FeatureLoader, autoModeService?
// Run orphan detection in background when project is loaded
// This detects features whose branches no longer exist (e.g., after merge/delete)
// We don't await this to keep the list response fast
// Note: detectOrphanedFeatures handles errors internally and always resolves
if (autoModeService) {
autoModeService
.detectOrphanedFeatures(projectPath)
.then((orphanedFeatures) => {
if (orphanedFeatures.length > 0) {
autoModeService.detectOrphanedFeatures(projectPath).then((orphanedFeatures) => {
if (orphanedFeatures.length > 0) {
logger.info(
`[ProjectLoad] Detected ${orphanedFeatures.length} orphaned feature(s) in ${projectPath}`
);
for (const { feature, missingBranch } of orphanedFeatures) {
logger.info(
`[ProjectLoad] Detected ${orphanedFeatures.length} orphaned feature(s) in ${projectPath}`
`[ProjectLoad] Orphaned: ${feature.title || feature.id} - branch "${missingBranch}" no longer exists`
);
for (const { feature, missingBranch } of orphanedFeatures) {
logger.info(
`[ProjectLoad] Orphaned: ${feature.title || feature.id} - branch "${missingBranch}" no longer exists`
);
}
}
})
.catch((error: unknown) => {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.warn(`[ProjectLoad] Failed to detect orphaned features: ${errorMessage}`);
});
}
});
}
res.json({ success: true, features });

View File

@@ -3095,6 +3095,10 @@ Format your response as a structured markdown document.`;
* restart, process crash, or manual stop). Features with this status can be
* resumed later using the resume functionality.
*
* Note: Features with pipeline_* statuses are preserved rather than overwritten
* to 'interrupted'. This ensures that resumePipelineFeature() can pick up from
* the correct pipeline step after a restart.
*
* @param projectPath - Path to the project
* @param featureId - ID of the feature to mark as interrupted
* @param reason - Optional reason for the interruption (logged for debugging)
@@ -3104,6 +3108,18 @@ Format your response as a structured markdown document.`;
featureId: string,
reason?: string
): Promise<void> {
// Load the feature to check its current status
const feature = await this.loadFeature(projectPath, featureId);
const currentStatus = feature?.status;
// Preserve pipeline_* statuses so resumePipelineFeature can resume from the correct step
if (currentStatus && currentStatus.startsWith('pipeline_')) {
logger.info(
`Feature ${featureId} was in ${currentStatus}; preserving pipeline status for resume`
);
return;
}
if (reason) {
logger.info(`Marking feature ${featureId} as interrupted: ${reason}`);
} else {