fix(merge): clear mergeActive + abort wedged session on stale-merge recovery

The prior commit re-enqueued tasks after stale-merge recovery but didn't
account for the engine's in-memory `mergeActive` set, which still held the
wedged task. `internalEnqueueMerge` silently no-ops when the entry is
present, so the re-enqueue had no effect.

The recovery callback now also aborts the active merge's signal and
disposes its session if the wedged attempt was the currently-active one.
This is what unsticks tasks where an AI provider call is hung mid-await
and the surrounding `try/finally` never gets a chance to run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-28 13:07:22 -07:00
parent 84708e4fb1
commit c856ed66a4

View File

@@ -195,8 +195,28 @@ export class ProjectEngine {
// Let the runtime's SelfHealingManager re-enqueue tasks directly into our // Let the runtime's SelfHealingManager re-enqueue tasks directly into our
// auto-merge queue when it clears a stale `merging` status, instead of // auto-merge queue when it clears a stale `merging` status, instead of
// relying on the 15s polling sweep to eventually catch them. // relying on the 15s polling sweep to eventually catch them.
//
// Critically: clear the in-memory `mergeActive` entry before re-enqueueing.
// A stale-merge recovery means the prior merge attempt is dead — but its
// `try/finally` may never have fired (e.g. an AI provider call is wedged
// mid-await), so the entry is still in `mergeActive` and would otherwise
// cause `internalEnqueueMerge` to silently no-op.
//
// Tests substitute a minimal runtime mock that may not implement this hook. // Tests substitute a minimal runtime mock that may not implement this hook.
this.runtime.setMergeEnqueuer?.((taskId) => this.internalEnqueueMerge(taskId)); this.runtime.setMergeEnqueuer?.((taskId) => {
// If the wedged attempt was the active one, abort its in-flight signal
// and dispose its session so subsequent code paths can release file
// handles / child processes promptly.
if (this.activeMergeTaskId === taskId) {
this.mergeAbortController?.abort();
this.mergeAbortController = null;
this.activeMergeSession?.dispose();
this.activeMergeSession = null;
this.activeMergeTaskId = null;
}
this.mergeActive.delete(taskId);
this.internalEnqueueMerge(taskId);
});
} }
/** /**