Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,226 changes: 1,226 additions & 0 deletions PRPs/fix-error-operation-visibility.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Retry Knowledge Source Hook
* Allows retrying failed crawl operations with original parameters
*/

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { progressKeys } from "../../progress/hooks/useProgressQueries";
import type { FailedOperation } from "../../progress/types";
import { useToast } from "../../shared/hooks/useToast";
import { knowledgeService } from "../services";

export function useRetryKnowledgeSource() {
const queryClient = useQueryClient();
const { showToast } = useToast();

return useMutation({
mutationFn: async (failedOp: FailedOperation) => {
if (!failedOp.original_request?.url) {
throw new Error("Cannot retry: Original request data missing");
}

// Retry by calling crawl endpoint with original parameters
return knowledgeService.crawlUrl({
url: failedOp.original_request.url,
max_depth: failedOp.original_request.max_depth,
tags: failedOp.original_request.tags,
});
},

onSuccess: (_data, failedOp) => {
showToast(
`Crawl restarted for ${failedOp.original_request?.url}`,
"success",
);

// Remove the failed operation from failed list
queryClient.removeQueries({
queryKey: progressKeys.detail(failedOp.progressId),
exact: true,
});

// Refresh both failed and active operations
queryClient.invalidateQueries({ queryKey: progressKeys.failed() });
queryClient.invalidateQueries({ queryKey: progressKeys.active() });
},

onError: (error) => {
showToast(
error instanceof Error ? error.message : "Could not restart crawl",
"error",
);
},
});
}
6 changes: 6 additions & 0 deletions archon-ui-main/src/features/knowledge/views/KnowledgeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useToast } from "@/features/shared/hooks/useToast";
import { CrawlingProgress } from "../../progress/components/CrawlingProgress";
import { FailedOperationsSection } from "../../progress/components/FailedOperationsSection";
import type { ActiveOperation } from "../../progress/types";
import { AddKnowledgeDialog } from "../components/AddKnowledgeDialog";
import { KnowledgeHeader } from "../components/KnowledgeHeader";
Expand Down Expand Up @@ -149,6 +150,11 @@ export const KnowledgeView = () => {
</div>
)}

{/* Failed Operations - Show persistent error states */}
<div className="mb-6">
<FailedOperationsSection />
</div>

{/* Knowledge Items List */}
<KnowledgeList
items={knowledgeItems}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* Failed Operations Section Component
* Displays persistent error states with retry/remove actions
*/

import { AlertCircle, ChevronDown, ChevronUp, RotateCcw, X } from "lucide-react";
import { useState } from "react";
import { useRetryKnowledgeSource } from "../../knowledge/hooks/useRetryKnowledgeSource";
import { DeleteConfirmModal } from "../../ui/components/DeleteConfirmModal";
import { Button } from "../../ui/primitives/button";
import {
useFailedOperations,
useRemoveFailedOperation,
} from "../hooks/useProgressQueries";
import type { FailedOperation } from "../types";

export function FailedOperationsSection() {
const { data: failedOps, isLoading } = useFailedOperations();
const retryMutation = useRetryKnowledgeSource();
const removeMutation = useRemoveFailedOperation();
const [expandedOps, setExpandedOps] = useState<Set<string>>(new Set());
const [confirmRemove, setConfirmRemove] = useState<FailedOperation | null>(null);

const toggleExpanded = (progressId: string) => {
setExpandedOps((prev) => {
const next = new Set(prev);
if (next.has(progressId)) {
next.delete(progressId);
} else {
next.add(progressId);
}
return next;
});
};

if (isLoading) {
return <div className="text-gray-400">Loading failed operations...</div>;
}

if (!failedOps?.operations.length) {
return null;
}

return (
<div className="space-y-4">
{/* Section Header */}
<div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-red-400" />
<h3 className="text-lg font-semibold text-white">Failed Operations</h3>
<span className="ml-2 px-2 py-0.5 bg-red-500/20 text-red-400 text-xs font-semibold rounded-full">
{failedOps.count}
</span>
</div>

{/* Failed Operations List */}
<div className="space-y-3">
{failedOps.operations.map((op) => (
<div key={op.progressId} className="rounded-lg border border-red-500/20 bg-red-500/10 p-4 backdrop-blur-sm">
{/* Operation Header */}
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-400 flex-shrink-0" />
<p className="text-sm font-medium text-red-400 truncate">
{op.url || op.currentUrl || "Unknown URL"}
</p>
</div>
<p className="text-xs text-gray-400 mt-1">
Failed {op.error_time ? new Date(op.error_time).toLocaleString() : "recently"}
</p>
{/* Error Message Preview */}
<p className="text-sm text-gray-300 mt-2 line-clamp-2">{op.error || "Unknown error"}</p>
</div>

{/* Action Buttons */}
<div className="flex items-center gap-2 flex-shrink-0">
<Button
size="sm"
variant="ghost"
onClick={() => toggleExpanded(op.progressId)}
className="text-gray-400 hover:text-white"
>
{expandedOps.has(op.progressId) ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => retryMutation.mutate(op)}
disabled={retryMutation.isPending}
className="border-blue-500/20 text-blue-400 hover:bg-blue-500/10"
>
<RotateCcw className="h-4 w-4 mr-2" />
Retry
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => setConfirmRemove(op)}
className="text-red-400 hover:bg-red-500/10"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>

{/* Expanded Error Details */}
{expandedOps.has(op.progressId) && (
<div className="mt-4 border-t border-red-500/20 pt-4">
<h4 className="text-sm font-medium text-gray-300 mb-2">Full Error Message:</h4>
<pre className="text-xs text-gray-400 bg-black/20 p-3 rounded overflow-x-auto whitespace-pre-wrap">
{op.error}
</pre>

{op.logs && op.logs.length > 0 && (
<>
<h4 className="text-sm font-medium text-gray-300 mb-2 mt-4">Crawl Logs:</h4>
<div className="space-y-1 max-h-48 overflow-y-auto">
{op.logs
.slice(-10)
.filter((log): log is { timestamp: string; message: string } => typeof log !== "string")
.map((log) => (
<div key={log.timestamp} className="text-xs text-gray-400">
<span className="text-gray-500">{new Date(log.timestamp).toLocaleTimeString()}</span>
{" - "}
{log.message}
</div>
))}
</div>
</>
)}
</div>
)}
</div>
))}
</div>

{/* Remove Confirmation Modal */}
{confirmRemove && (
<DeleteConfirmModal
type="knowledge"
itemName={confirmRemove.url || "this operation"}
onConfirm={() => {
removeMutation.mutate(confirmRemove.progressId);
setConfirmRemove(null);
}}
onCancel={() => setConfirmRemove(null)}
/>
)}
</div>
);
}
56 changes: 54 additions & 2 deletions archon-ui-main/src/features/progress/hooks/useProgressQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@
* Handles polling for operation progress with TanStack Query
*/

import { type UseQueryResult, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
import {
type UseMutationResult,
type UseQueryResult,
useMutation,
useQueries,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/config/queryPatterns";
import { useSmartPolling } from "../../shared/hooks";
import { APIServiceError } from "../../shared/types/errors";
import { progressService } from "../services";
import type { ActiveOperationsResponse, ProgressResponse, ProgressStatus } from "../types";
import type { ActiveOperationsResponse, FailedOperationsResponse, ProgressResponse, ProgressStatus } from "../types";

// Query keys factory
export const progressKeys = {
all: ["progress"] as const,
lists: () => [...progressKeys.all, "list"] as const,
detail: (id: string) => [...progressKeys.all, "detail", id] as const,
active: () => [...progressKeys.all, "active"] as const,
failed: () => [...progressKeys.all, "failed"] as const,
};

// Terminal states that should stop polling
Expand Down Expand Up @@ -381,3 +389,47 @@ export function useMultipleOperations(
};
});
}

/**
* Get all failed operations
* These are operations with error/failed status that persist for 5 minutes
* IMPORTANT: These are NOT auto-removed - user must explicitly dismiss
*/
export function useFailedOperations() {
return useQuery<FailedOperationsResponse>({
queryKey: progressKeys.failed(),
queryFn: () => progressService.listFailedOperations(),
enabled: true,
refetchInterval: 30000, // Poll every 30s - failed ops are mostly static
staleTime: STALE_TIMES.normal,
// CRITICAL: No auto-removal for failed operations
// User must explicitly click "Remove" button
});
}

/**
* Remove a failed operation from the list
* This is explicit user action, not automatic cleanup
*/
export function useRemoveFailedOperation(): UseMutationResult<{ progressId: string }, Error, string> {
const queryClient = useQueryClient();

return useMutation({
mutationFn: async (progressId: string) => {
// Just remove from cache - backend will auto-cleanup after 5 minutes
// No API call needed - this is client-side dismissal
return { progressId };
},

onSuccess: (_data, progressId) => {
// Remove specific operation
queryClient.removeQueries({
queryKey: progressKeys.detail(progressId),
exact: true,
});

// Refresh failed operations list
queryClient.invalidateQueries({ queryKey: progressKeys.failed() });
},
});
}
24 changes: 23 additions & 1 deletion archon-ui-main/src/features/progress/services/progressService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { callAPIWithETag } from "../../shared/api/apiClient";
import type { ActiveOperationsResponse, ProgressResponse } from "../types";
import type { ActiveOperationsResponse, FailedOperationsResponse, ProgressResponse } from "../types";

export const progressService = {
/**
Expand All @@ -21,4 +21,26 @@ export const progressService = {
// IMPORTANT: Use trailing slash to avoid FastAPI redirect that breaks in Docker
return callAPIWithETag<ActiveOperationsResponse>("/api/progress/");
},

/**
* List all failed operations
* These are operations with error/failed status that persist for 5 minutes
*/
async listFailedOperations(): Promise<FailedOperationsResponse> {
// Request all operations including failed ones
const response = await callAPIWithETag<FailedOperationsResponse>(
"/api/progress/?include_failed=true"
);

// Filter to only return operations with error or failed status
const failedOperations = response.operations.filter(
(op) => op.status === "error" || op.status === "failed"
);

return {
operations: failedOperations,
count: failedOperations.length,
timestamp: response.timestamp,
};
},
};
29 changes: 28 additions & 1 deletion archon-ui-main/src/features/progress/types/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,14 @@ export interface ProgressResponse {
fileSize?: number;
chunksProcessed?: number;
totalChunks?: number;
logs?: string[];
logs?:
| string[]
| Array<{
timestamp: string;
message: string;
status?: string;
progress?: number;
}>;
timestamp?: string;
startedAt?: string; // ISO date string of when operation started
stats?: {
Expand All @@ -172,3 +179,23 @@ export interface ProgressResponse {
current_operation?: string;
};
}

// Failed operation types for error visibility feature
export interface FailedOperation extends ProgressResponse {
status: "error" | "failed";
error: string; // Error message (required for failed operations)
error_time?: string; // ISO timestamp of when error occurred
original_request?: {
// For retry functionality - original parameters
url: string;
max_depth?: number;
max_concurrent?: number;
tags?: string[];
};
}

export interface FailedOperationsResponse {
operations: FailedOperation[];
count: number;
timestamp: string;
}
Loading