feat(FN-3977): complete Step 4-5 — verification and docs

Fusion-Task-Id: FN-3977
Fusion-Task-Lineage: c84f2ce0-3726-49b2-ad84-6703edbfecf1
This commit is contained in:
Fusion
2026-05-14 15:49:27 -07:00
committed by gsxdsm
parent e5b5d5c9e5
commit f9af9fc66e
5 changed files with 29 additions and 15 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Add explicit overlap/`blockedBy` bottleneck visibility across scheduler logs and dashboard fan-out surfaces, including de-duplicated scheduler warnings and overlap-specific footer/card/detail summaries.

View File

@@ -271,7 +271,7 @@ A persistent footer status bar at the bottom of the dashboard displays real-time
- **Stuck**: Count of tasks in "in-progress" with no activity for longer than the project's `taskStuckTimeoutMs` setting (shown only when > 0 and the setting is enabled). Uses the same `isTaskStuck()` predicate as task cards and list rows, so the footer count always matches the visible stuck indicators on the board
- **Queued**: Count of tasks in "todo" column
- **In Review**: Count of tasks in "in-review" column
- **Escalated blocker summary**: FN-3942 surfaces immediate high fan-out blockers (`activeTodoCount >= 5`); FN-3954 upgrades long-lived high fan-out blockers to an explicit **Escalated** summary in the footer, ranked by todo fan-out, active total, age, then task ID.
- **Overlap queue summary**: surfaces overlap bottlenecks from `blockedBy` fan-out (`overlapBlockedTodoCount >= 5`). The footer always shows the highest overlap blocker (temporary vs escalated) with blocker task ID + overlap-blocked todo count.
- **Executor State**: Current state badge (Idle/Running/Paused)
- **Last Activity**: Relative timestamp of most recent task event
@@ -282,7 +282,7 @@ A persistent footer status bar at the bottom of the dashboard displays real-time
**Features**:
- **Shared task list**: Task counts are derived from the same task list used by the board and list views, so the footer always matches the board state exactly. Stuck task detection uses a shared `isTaskStuck()` utility (see `utils/taskStuck.ts`) so the footer count and individual card/row indicators are always consistent.
- **Age-based escalation**: Task cards keep ordinary `Blocks N` visibility for non-critical chains, show immediate **High fan-out** at `activeTodoCount >= 5`, and only switch to **Escalated** when that high fan-out blocker remains in blocking columns longer than `staleHighFanoutBlockerAgeThresholdMs` (`columnMovedAt ?? updatedAt`). Done/archived downstream tasks never contribute to the threshold.
- **Age-based escalation**: overlap bottleneck warning/escalation is driven only by todo tasks waiting through `blockedBy` (`overlapBlockedTodoCount`), not dependency-only chains. Task cards keep ordinary `Blocks N` visibility for low overlap fan-out, switch to **Overlap bottleneck** at threshold, and only switch to **Escalated overlap** when long-lived (`staleHighFanoutBlockerAgeThresholdMs`).
- **Footer-safe layout**: Project-view content (board, list view, agents view) automatically reserves space for the fixed footer using a CSS custom property (`--executor-footer-height`). The `project-content--with-footer` wrapper class sets this token to 36px on desktop and 32px on mobile, ensuring all content remains fully visible and scrollable above the status bar
- Real-time updates via 5-second polling for executor state (globalPause, enginePaused, maxConcurrent)
- Responsive design: collapses labels on mobile screens (<768px); footer height reduces from 36px to 32px

View File

@@ -479,6 +479,7 @@ export function ListView({
column: "Column",
dependencies: "Dependencies",
progress: "Progress",
retries: "Retries",
};
const handleSort = useCallback((field: SortField) => {

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useRef, lazy, Suspense, type CSSProperties, type MouseEvent } from "react";
import { Globe, Folder, RefreshCw, Star, HelpCircle, Loader2, CheckCircle, AlertTriangle } from "lucide-react";
import {
AGENT_PERMISSION_POLICY_ACTION_CATEGORIES,
THINKING_LEVELS,
getErrorMessage,
isGlobalSettingsKey,
@@ -10,7 +11,7 @@ import {
resolveProjectDefaultModel,
resolveTitleSummarizerSettingsModel,
} from "@fusion/core";
import type { Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, AgentPromptsConfig, ThinkingLevel } from "@fusion/core";
import type { AgentPermissionPolicyRules, Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, AgentPromptsConfig, ThinkingLevel } from "@fusion/core";
import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, cancelProviderLogin, saveApiKey, clearApiKey, fetchModels, testNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, triggerMemoryDreams, fetchGitRemotesDetailed, fetchDashboardHealth, checkForUpdates, fetchRemoteSettings, updateRemoteSettings, fetchRemoteStatus, installCloudflared, startRemoteTunnel, stopRemoteTunnel, killExternalTunnel, regenerateRemotePersistentToken, generateShortLivedRemoteToken, fetchRemoteQr, fetchRemoteUrl, submitProviderManualCode } from "../api";
import type { AuthProvider, ManualOAuthCodeInfo, ModelInfo, BackupListResponse, SettingsExportData, MemoryFileInfo, MemoryRetrievalTestResult, GitRemoteDetailed, RemoteSettings, RemoteStatus, UpdateCheckResponse } from "../api";
import { useMemoryBackendStatus } from "../hooks/useMemoryBackendStatus";
@@ -57,6 +58,13 @@ const GITHUB_STAR_CACHE_KEY = "fusion_github_star_count";
const GITHUB_STAR_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
const GITHUB_STAR_CLICKED_KEY = "fusion:github-star-clicked";
function toCompleteAgentPermissionRules(rules?: Partial<AgentPermissionPolicyRules>): AgentPermissionPolicyRules {
return AGENT_PERMISSION_POLICY_ACTION_CATEGORIES.reduce((acc, category) => {
acc[category] = rules?.[category] ?? "allow";
return acc;
}, {} as AgentPermissionPolicyRules);
}
function getNodeStatusLabel(status: "online" | "offline" | "connecting" | "error"): string {
if (status === "online") return "Online";
if (status === "connecting") return "Connecting";
@@ -4198,11 +4206,11 @@ export function SettingsModal({
</div>
<AgentPermissionPolicyEditor
mode="project-default"
value={form.defaultAgentPermissionPolicy ? { presetId: "custom", rules: form.defaultAgentPermissionPolicy.rules ?? {} } : { presetId: "custom", rules: {} }}
value={form.defaultAgentPermissionPolicy ? { presetId: "custom", rules: toCompleteAgentPermissionRules(form.defaultAgentPermissionPolicy.rules) } : { presetId: "custom", rules: toCompleteAgentPermissionRules() }}
onChange={(next) =>
setForm((f) => ({
...f,
defaultAgentPermissionPolicy: { rules: next?.rules ?? {} },
defaultAgentPermissionPolicy: { rules: toCompleteAgentPermissionRules(next?.rules) },
}))
}
/>

View File

@@ -895,16 +895,16 @@ export function TaskDetailContent({
const showGithubTrackingSection = canEditGithubTracking || githubTrackingEnabled || Boolean(githubTrackedIssue);
const retrySummary = task.retrySummary;
const retryRows = [
{ key: "stuckKillCount", label: "Stuck kills", title: "Stuck-task detector forced agent kill retries", value: retrySummary?.stuckKillCount ?? 0 },
{ key: "recoveryRetryCount", label: "Recovery retries", title: "Transient executor recovery retries", value: retrySummary?.recoveryRetryCount ?? 0 },
{ key: "taskDoneRetryCount", label: "task_done retries", title: "Agent exited without task_done and task was retried", value: retrySummary?.taskDoneRetryCount ?? 0 },
{ key: "workflowStepRetries", label: "Workflow retries", title: "Workflow step failure retries", value: retrySummary?.workflowStepRetries ?? 0 },
{ key: "verificationFailureCount", label: "Verification bounces", title: "Verification failure bounce retries", value: retrySummary?.verificationFailureCount ?? 0 },
{ key: "postReviewFixCount", label: "Post-review fixes", title: "Post-review remediation retries", value: retrySummary?.postReviewFixCount ?? 0 },
{ key: "mergeConflictBounceCount", label: "Merge conflict bounces", title: "Merge conflict bounce retries", value: retrySummary?.mergeConflictBounceCount ?? 0 },
{ key: "branchConflictRecoveryCount", label: "Branch conflict recovery", title: "FN-4068 branch-conflict recovery retries", value: retrySummary?.branchConflictRecoveryCount ?? 0 },
{ key: "reviewerContextRetryCount", label: "Reviewer context retries", title: "FN-4082 compact reviewer retry", value: retrySummary?.reviewerContextRetryCount ?? 0 },
{ key: "reviewerFallbackRetryCount", label: "Reviewer fallback retries", title: "FN-4092 fallback-model retry", value: retrySummary?.reviewerFallbackRetryCount ?? 0 },
{ key: "stuckKill", label: "Stuck kills", title: "Stuck-task detector forced agent kill retries", value: retrySummary?.stuckKill ?? 0 },
{ key: "recovery", label: "Recovery retries", title: "Transient executor recovery retries", value: retrySummary?.recovery ?? 0 },
{ key: "taskDone", label: "task_done retries", title: "Agent exited without task_done and task was retried", value: retrySummary?.taskDone ?? 0 },
{ key: "workflowStep", label: "Workflow retries", title: "Workflow step failure retries", value: retrySummary?.workflowStep ?? 0 },
{ key: "verification", label: "Verification bounces", title: "Verification failure bounce retries", value: retrySummary?.verification ?? 0 },
{ key: "postReviewFix", label: "Post-review fixes", title: "Post-review remediation retries", value: retrySummary?.postReviewFix ?? 0 },
{ key: "mergeConflict", label: "Merge conflict bounces", title: "Merge conflict bounce retries", value: retrySummary?.mergeConflict ?? 0 },
{ key: "branchConflict", label: "Branch conflict recovery", title: "FN-4068 branch-conflict recovery retries", value: retrySummary?.branchConflict ?? 0 },
{ key: "reviewerContext", label: "Reviewer context retries", title: "FN-4082 compact reviewer retry", value: retrySummary?.reviewerContext ?? 0 },
{ key: "reviewerFallback", label: "Reviewer fallback retries", title: "FN-4092 fallback-model retry", value: retrySummary?.reviewerFallback ?? 0 },
].filter((row) => row.value > 0);
const githubTrackingStatus = githubTrackingDetailPending
? "Loading"