feat(FN-3019): merge fusion/fn-3019

Commits merged:
- feat(FN-3019): complete Step 4 — document gridlock cooldown semantics
- fix(FN-3019): normalize legacy custom provider payloads for typecheck
- test(FN-3019): complete Step 2 — add gridlock cooldown regression coverage
- fix(FN-3019): repair notifier class structure after cooldown refactor
- feat(FN-3019): complete Step 1 — add gridlock notification cooldown

Files changed:
docs/architecture.md                               |  3 +-
 docs/settings-reference.md                         |  4 +-
 .../app/components/CustomProvidersSection.tsx      | 25 ++++++----
 .../engine/src/__tests__/gridlock-detector.test.ts |  5 +-
 packages/engine/src/__tests__/notifier.test.ts     | 47 ++++++++++++++++--
 packages/engine/src/gridlock-detector.ts           | 16 +++++--
 packages/engine/src/notifier.ts                    | 55 ++++++++++++----------
 packages/engine/src/project-engine.ts              |  1 +
 8 files changed, 109 insertions(+), 47 deletions(-)

Fusion-Task-Id: FN-3019
This commit is contained in:
Fusion
2026-04-30 00:18:41 -07:00
committed by gsxdsm
parent e100f2ab7f
commit 115ea77d12
8 changed files with 110 additions and 48 deletions

View File

@@ -15,6 +15,7 @@ export interface GridlockDetectorOptions {
pollIntervalMs?: number;
missionStore?: MissionStore;
onGridlock?: (event: GridlockEvent) => void;
onGridlockCleared?: () => void;
}
export class GridlockDetector {
@@ -22,6 +23,7 @@ export class GridlockDetector {
private readonly pollIntervalMs: number;
private readonly missionStore?: MissionStore;
private readonly onGridlock?: (event: GridlockEvent) => void;
private readonly onGridlockCleared?: () => void;
private lastGridlockKey: string | null = null;
constructor(
@@ -31,6 +33,7 @@ export class GridlockDetector {
this.pollIntervalMs = options.pollIntervalMs ?? 30_000;
this.missionStore = options.missionStore;
this.onGridlock = options.onGridlock;
this.onGridlockCleared = options.onGridlockCleared;
}
start(): void {
@@ -65,13 +68,13 @@ export class GridlockDetector {
});
if (schedulable.length === 0) {
this.lastGridlockKey = null;
this.clearGridlockState();
return null;
}
const active = tasks.filter((task) => task.column === "in-progress" || (task.column === "in-review" && Boolean(task.worktree)));
if (active.length === 0) {
this.lastGridlockKey = null;
this.clearGridlockState();
return null;
}
@@ -117,7 +120,7 @@ export class GridlockDetector {
const blockedTaskIds = Object.keys(reasons).sort();
if (blockedTaskIds.length !== schedulable.length) {
this.lastGridlockKey = null;
this.clearGridlockState();
return null;
}
@@ -138,6 +141,13 @@ export class GridlockDetector {
return event;
}
private clearGridlockState(): void {
if (this.lastGridlockKey !== null) {
this.lastGridlockKey = null;
this.onGridlockCleared?.();
}
}
private isMissionBlocked(task: Task): boolean {
if (!this.missionStore || !task.sliceId) return false;
try {