feat(FN-4307): complete Step 2 — suppress transient failure surfacing

Fusion-Task-Id: FN-4307
Fusion-Task-Lineage: 00140a5a-af93-4905-8570-32d7ecb8a874
This commit is contained in:
Fusion
2026-05-13 15:36:04 -07:00
committed by gsxdsm
parent 2e7e5e091c
commit 76a07fea21
2 changed files with 22 additions and 4 deletions

View File

@@ -61,6 +61,8 @@ export class NotificationService {
private readonly pendingFailureStartTimes = new Map<string, number>();
private readonly failedNotificationGraceMs: number;
private failureNotificationSuppressedCount = 0;
private failureNotificationDelayMs = 60_000;
private failureNotificationMode: "sticky-only" | "all" = "sticky-only";
constructor(
private readonly store: NotificationServiceStore,
@@ -68,6 +70,7 @@ export class NotificationService {
) {
this.chatStore = options.chatStore;
this.failedNotificationGraceMs = options.failedNotificationGraceMs ?? 60_000;
this.failureNotificationDelayMs = this.failedNotificationGraceMs;
}
attachChatStore(chatStore: NotificationChatStore): void {
@@ -91,6 +94,7 @@ export class NotificationService {
const settings = await this.store.getSettings();
this.setNotificationsEnabledFromSettings(settings);
this.refreshFailureNotificationSettings(settings);
await this.syncNtfyProvider(settings);
await this.syncWebhookProvider(settings);
@@ -153,7 +157,11 @@ export class NotificationService {
}
if (task.status === "failed") {
this.scheduleFailureNotification(task);
if (this.failureNotificationMode === "all" || this.failureNotificationDelayMs === 0) {
this.maybeNotify(task.id, "failed", this.createTaskPayload(task, "failed"));
} else {
this.scheduleFailureNotification(task);
}
}
if (task.status === "awaiting-approval") {
@@ -188,6 +196,7 @@ export class NotificationService {
private handleSettingsUpdated = async (data: { settings: Settings; previous: Settings }): Promise<void> => {
const { settings, previous } = data;
this.setNotificationsEnabledFromSettings(settings);
this.refreshFailureNotificationSettings(settings);
if (
settings.ntfyEnabled !== previous.ntfyEnabled ||
@@ -445,6 +454,7 @@ export class NotificationService {
this.refreshInFlight = (async () => {
const settings = await this.store.getSettings();
this.setNotificationsEnabledFromSettings(settings);
this.refreshFailureNotificationSettings(settings);
await this.syncNtfyProvider(settings);
await this.syncWebhookProvider(settings);
schedulerLog.log(`NotificationService refreshed notification state reason=${reason} enabled=${String(this.notificationsEnabled)}`);
@@ -457,6 +467,14 @@ export class NotificationService {
}
}
private refreshFailureNotificationSettings(settings: Settings): void {
this.failureNotificationDelayMs =
typeof settings.failureNotificationDelayMs === "number" && settings.failureNotificationDelayMs >= 0
? settings.failureNotificationDelayMs
: this.failedNotificationGraceMs;
this.failureNotificationMode = settings.failureNotificationMode ?? "sticky-only";
}
private scheduleFailureNotification(task: Task): void {
if (this.pendingFailureNotifications.has(task.id)) {
return;
@@ -466,7 +484,7 @@ export class NotificationService {
const payload = this.createTaskPayload(task, "failed");
const timer = setTimeout(() => {
void this.fireDeferredFailureNotification(task.id);
}, this.failedNotificationGraceMs);
}, this.failureNotificationDelayMs);
timer.unref?.();
this.pendingFailureNotifications.set(task.id, { timer, payload });
}

View File

@@ -1816,10 +1816,10 @@ export class ProjectEngine {
mergeRetries: ProjectEngine.MAX_AUTO_MERGE_RETRIES,
error: errorMsg,
});
await store.addTaskComment(
await store.logEntry(
taskId,
`Auto-merge failed with a non-conflict error and stopped retrying: ${errorMsg}`,
"agent",
"MergeNonConflictFailure",
);
} catch (recoveryErr) {
runtimeLog.error(