mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-02 16:58:52 +01:00
Failed lifecycle verification could allocate another test environment every minute indefinitely. Count failed host verification tests separately from claims, persist a repair wait for confirmed configuration failures or three generic failed tests, and clear it through authorized retry or execution configuration changes. Preserve typed temporary execution-target failures through adapter checks so timeouts, transport failures and interrupted probes keep automatic recovery. Retain existing readiness, authorization, termination and cleanup gates. Document the policy and add real PostgreSQL persistence and actual OpenCode adapter regressions. Verified 398 focused tests (one Windows-only test skipped on macOS), repository-wide typecheck and build, structural checks, fully green current-head CI and Greptile 5/5 with no unresolved comments. Co-Authored-By: Paperclip <noreply@paperclip.ing>
72 lines
3.7 KiB
TypeScript
72 lines
3.7 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { agentsApi } from "../api/agents";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import type { Agent, AgentLifecycleState } from "@paperclipai/shared";
|
|
import { Loader2 } from "lucide-react";
|
|
import { Badge } from "./ui/badge";
|
|
import { Button } from "./ui/button";
|
|
|
|
const phases = {
|
|
preparing: ["Preparing agent", "Required resources are being prepared. Work stays queued."],
|
|
verifying: ["Verifying agent", "The saved execution configuration is being checked. Work stays queued."],
|
|
pausing: ["Pausing agent", "Waiting for execution and required resources to stop."],
|
|
resuming: ["Resuming agent", "Required resources are being started. Work stays queued."],
|
|
terminating: ["Terminating agent", "Stopping execution before resource cleanup."],
|
|
cleaning_up: ["Cleaning up agent", "Required resources are being removed. Termination is not complete."],
|
|
} satisfies Partial<Record<AgentLifecycleState, [string, string]>>;
|
|
|
|
const failedPhases = {
|
|
preparing: "Agent preparation failed", verifying: "Agent verification failed",
|
|
pausing: "Agent pause failed", resuming: "Agent resume failed",
|
|
terminating: "Agent termination failed", cleaning_up: "Agent cleanup failed",
|
|
};
|
|
|
|
function pendingPhase(state?: AgentLifecycleState) {
|
|
return state && state in phases ? phases[state as keyof typeof phases] : undefined;
|
|
}
|
|
|
|
export function agentLifecycleRefetchInterval(state?: AgentLifecycleState) {
|
|
return pendingPhase(state) ? 2_000 : 30_000;
|
|
}
|
|
|
|
/** Poll lifecycle fields without refreshing the record used by editable forms. */
|
|
export function useAgentLifecycleStatus(agent?: Agent) {
|
|
return useQuery({
|
|
queryKey: [...queryKeys.agents.detail(agent?.id ?? ""), "lifecycle", agent?.companyId],
|
|
queryFn: () => agentsApi.get(agent!.id, agent!.companyId),
|
|
enabled: Boolean(agent),
|
|
select: current => ({ status: current.status, pauseReason: current.pauseReason, pausedAt: current.pausedAt, lifecycleState: current.lifecycleState,
|
|
lifecycleVersion: current.lifecycleVersion, lifecycleError: current.lifecycleError }),
|
|
refetchInterval: query => agentLifecycleRefetchInterval(query.state.data?.lifecycleState ?? agent?.lifecycleState),
|
|
});
|
|
}
|
|
|
|
export function AgentLifecycleStatus({ agent, onRetry, retryPending, refreshError = false }: {
|
|
agent: Pick<Agent, "lifecycleState" | "lifecycleError">;
|
|
onRetry: () => void;
|
|
retryPending: boolean;
|
|
refreshError?: boolean;
|
|
}) {
|
|
const phase = pendingPhase(agent.lifecycleState);
|
|
if (!phase && !refreshError) return null;
|
|
return <div role="status" aria-live="polite" className="space-y-2 text-sm">
|
|
{refreshError && <p className="text-destructive">Could not refresh agent lifecycle status.</p>}
|
|
{phase && <div className="flex flex-wrap items-center gap-2">
|
|
<Badge variant={agent.lifecycleError ? "destructive" : "secondary"}>
|
|
{!agent.lifecycleError && <Loader2 aria-hidden="true" className="mr-1 size-3 animate-spin motion-reduce:animate-none" />}
|
|
{agent.lifecycleError ? failedPhases[agent.lifecycleState as keyof typeof failedPhases] : phase[0]}
|
|
</Badge>
|
|
{!agent.lifecycleError && <span className="text-muted-foreground">{phase[1]}</span>}
|
|
</div>}
|
|
{phase && agent.lifecycleError && <div className="flex flex-wrap items-center gap-2 text-destructive">
|
|
<span>{agent.lifecycleError}</span>
|
|
<Button variant="outline" size="sm" disabled={retryPending} onClick={onRetry}>
|
|
{retryPending ? "Retrying…" : "Retry now"}
|
|
</Button>
|
|
</div>}
|
|
{phase && agent.lifecycleError && <p className="text-muted-foreground">{agent.lifecycleState === "verifying"
|
|
? "Retry now checks the saved configuration again immediately."
|
|
: "Paperclip retries automatically; Retry now checks again immediately."}</p>}
|
|
</div>;
|
|
}
|