Files
Codewalkers/packages/web/src/components/TaskDetailModal.tsx
Lukas May 43e2c8b0ba fix(agent): Eliminate race condition in completion handling
PROBLEM:
- Agents completing with questions were incorrectly marked as "crashed"
- Race condition: polling handler AND crash handler both called handleCompletion()
- Caused database corruption and lost pending questions

SOLUTION:
- Add completion mutex in OutputHandler to prevent concurrent processing
- Remove duplicate completion call from crash handler
- Only one handler executes completion logic per agent

TESTING:
- Added mutex-completion.test.ts with 4 test cases
- Verified mutex prevents concurrent access
- Verified lock cleanup on exceptions
- Verified different agents can process concurrently

FIXES: residential-cuckoo and 12+ other agents stuck in crashed state
2026-02-08 15:51:32 +01:00

159 lines
4.8 KiB
TypeScript

import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { StatusBadge } from "@/components/StatusBadge";
import { StatusDot } from "@/components/StatusDot";
import type { SerializedTask } from "@/components/TaskRow";
interface DependencyInfo {
name: string;
status: string;
}
interface TaskDetailModalProps {
task: SerializedTask | null;
phaseName: string;
agentName: string | null;
dependencies: DependencyInfo[];
dependents: DependencyInfo[];
onClose: () => void;
onQueueTask: (taskId: string) => void;
onStopTask: (taskId: string) => void;
}
export function TaskDetailModal({
task,
phaseName,
agentName,
dependencies,
dependents,
onClose,
onQueueTask,
onStopTask,
}: TaskDetailModalProps) {
const allDependenciesComplete =
dependencies.length === 0 ||
dependencies.every((d) => d.status === "completed");
const canQueue = task !== null && task.status === "pending" && allDependenciesComplete;
const canStop = task !== null && task.status === "in_progress";
return (
<Dialog open={task !== null} onOpenChange={(open) => { if (!open) onClose(); }}>
<DialogContent>
<DialogHeader>
<DialogTitle>{task?.name ?? "Task"}</DialogTitle>
<DialogDescription>Task details and dependencies</DialogDescription>
</DialogHeader>
{task && (
<div className="space-y-4">
{/* Metadata grid */}
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-muted-foreground">Status</span>
<div className="mt-1">
<StatusBadge status={task.status} />
</div>
</div>
<div>
<span className="text-muted-foreground">Priority</span>
<p className="mt-1 font-medium capitalize">{task.priority}</p>
</div>
<div>
<span className="text-muted-foreground">Phase</span>
<p className="mt-1 font-medium">{phaseName}</p>
</div>
<div>
<span className="text-muted-foreground">Type</span>
<p className="mt-1 font-medium">{task.type}</p>
</div>
<div className="col-span-2">
<span className="text-muted-foreground">Agent</span>
<p className="mt-1 font-medium">
{agentName ?? "Unassigned"}
</p>
</div>
</div>
{/* Description */}
<div>
<h4 className="mb-1 text-sm font-medium">Description</h4>
<p className="text-sm text-muted-foreground">
{task.description ?? "No description"}
</p>
</div>
{/* Dependencies */}
<div>
<h4 className="mb-1 text-sm font-medium">Dependencies</h4>
{dependencies.length === 0 ? (
<p className="text-sm text-muted-foreground">
No dependencies
</p>
) : (
<ul className="space-y-1">
{dependencies.map((dep) => (
<li
key={dep.name}
className="flex items-center gap-2 text-sm"
>
<span>{dep.name}</span>
<StatusDot status={dep.status} size="md" />
</li>
))}
</ul>
)}
</div>
{/* Dependents (Blocks) */}
<div>
<h4 className="mb-1 text-sm font-medium">Blocks</h4>
{dependents.length === 0 ? (
<p className="text-sm text-muted-foreground">None</p>
) : (
<ul className="space-y-1">
{dependents.map((dep) => (
<li
key={dep.name}
className="flex items-center gap-2 text-sm"
>
<span>{dep.name}</span>
<StatusDot status={dep.status} size="md" />
</li>
))}
</ul>
)}
</div>
</div>
)}
<DialogFooter>
<Button
variant="outline"
size="sm"
disabled={!canQueue}
onClick={() => task && onQueueTask(task.id)}
>
Queue Task
</Button>
<Button
variant="destructive"
size="sm"
disabled={!canStop}
onClick={() => task && onStopTask(task.id)}
>
Stop Task
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}