Even with FN-5627's merger TOCTOU fix + transient-failure self-healing sweep + safety-fallback auto-prerebase landed, the merger can still hit transient failure classes (lease handoff races, brief same-SHA non-FF advances) for tasks whose branches are particularly out-of-sync. The self-healing sweep auto-recovers them within bounded budget \u2014 but each individual failure cycle was firing a ntfy alarm before the recovery cleared the failed state, producing user-facing alarm spam for tasks that were never actually stuck. Two layers of fix: 1. NotificationService.handleTaskUpdated now classifies task.error via the new shared classifyTransientMergeError helper before scheduling the deferred failure notification. Transient classes (lease-handoff-target-not-queued, spurious-concurrent-advance-same-sha) get logged as suppressed and never schedule a ntfy timer. 2. Defense-in-depth: fireDeferredFailureNotification re-classifies the error at dispatch time, so a failure scheduled before the suppression landed on a newer cycle still suppresses if the error matches a transient class. The classifier itself moved from self-healing.ts to a new logger-free transient-merge-error-classifier.ts module so consumers in NotificationService don't pull createLogger through the import chain and break test mocks of ../logger.js (per project-memory rule about new modules using createLogger). self-healing.ts re-exports the symbol for backward compatibility. Log prefix for the recovery actions also changed from '[FN-5627] Auto-recovering...' to 'Auto-recovered:' so that NotificationService.maybeSuppressTransientFailedNotification's existing /^Auto-recovered:/ log-prefix check cancels any already-scheduled failure notification when the sweep runs mid-grace-window. Tests (3 new): - transient lease-handoff-target-not-queued failure NOT notified - transient spurious-concurrent-advance-same-sha failure NOT notified - genuine different-SHAs concurrent-advance still notifies (control) Engine suite: 6166 tests pass. Fusion-Task-Id: FN-5627
48 lines
2.5 KiB
TypeScript
48 lines
2.5 KiB
TypeScript
/**
|
|
* FN-5627: Shared classifier for transient merge failure error messages.
|
|
*
|
|
* Extracted from `self-healing.ts` to break the import chain that would
|
|
* otherwise pull in `createLogger` and break `vi.mock("../logger.js")` setups
|
|
* in tests that don't currently mock the full logger surface (notification-
|
|
* service.test.ts in particular).
|
|
*
|
|
* Used by both `SelfHealingManager.recoverTransientMergeFailures` (the
|
|
* recovery sweep) and `NotificationService.handleTaskUpdated` (the
|
|
* notification-suppression gate). Both consumers must agree on what counts
|
|
* as transient so the user doesn't get ntfy alarms for failures that the
|
|
* engine will auto-recover within bounded budget.
|
|
*
|
|
* Recognized classes:
|
|
*
|
|
* - `lease-handoff-target-not-queued`: the merge queue lease acquisition saw
|
|
* the task drop out of the queue between enqueue and handoff. Race with
|
|
* self-healing sweeps that clean stale `mergeQueue` rows (FN-5353/FN-5363).
|
|
*
|
|
* - `spurious-concurrent-advance-same-sha`: the merger reported
|
|
* `Integration branch X advanced concurrently (expected SHA, observed SHA)`
|
|
* with identical SHA on both sides. This signature shows up in two cases:
|
|
* (1) Pre-FN-5627 misclassification in `merger-ref-update-advance.ts`
|
|
* routed real ref-update-refusal failures (lock contention, hook
|
|
* rejection) through `IntegrationBranchConcurrentAdvanceError`.
|
|
* (2) Post-FN-5627: the merger's `advanceIntegrationBranchRef` correctly
|
|
* detects `non-fast-forward-advance` when the freshly built squash
|
|
* commit does not descend from the current integration ref. The error
|
|
* carries the same SHA in both the "expected" and "observed" slots
|
|
* because the pre-advance rev-parse captured the ref state and
|
|
* update-ref refused without moving it. On the next merge attempt,
|
|
* the safety-fallback auto-prerebase (`merger-auto-prerebase.ts`,
|
|
* FN-5627) rebases the task branch onto current main, so the retry
|
|
* succeeds.
|
|
*/
|
|
export function classifyTransientMergeError(error: string | null | undefined): string | null {
|
|
if (!error) return null;
|
|
if (/lease-handoff-failed[^a-z]+target-not-queued/i.test(error)) {
|
|
return "lease-handoff-target-not-queued";
|
|
}
|
|
const sameSha = error.match(/advanced concurrently \(expected ([0-9a-f]{7,40}),\s+observed ([0-9a-f]{7,40})\)/i);
|
|
if (sameSha && sameSha[1].toLowerCase() === sameSha[2].toLowerCase()) {
|
|
return "spurious-concurrent-advance-same-sha";
|
|
}
|
|
return null;
|
|
}
|