mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 11:20:37 +09:00
Merge pull request #1654 from paperclipai/pr/pap-795-agent-runtime
fix(runtime): improve agent recovery and heartbeat operations
This commit is contained in:
commit
f2637e6972
28 changed files with 1291 additions and 64 deletions
|
|
@ -573,9 +573,9 @@ export function AgentDetail() {
|
|||
});
|
||||
|
||||
const { data: allIssues } = useQuery({
|
||||
queryKey: queryKeys.issues.list(resolvedCompanyId!),
|
||||
queryFn: () => issuesApi.list(resolvedCompanyId!),
|
||||
enabled: !!resolvedCompanyId && needsDashboardData,
|
||||
queryKey: [...queryKeys.issues.list(resolvedCompanyId!), "participant-agent", resolvedAgentId ?? "__none__"],
|
||||
queryFn: () => issuesApi.list(resolvedCompanyId!, { participantAgentId: resolvedAgentId! }),
|
||||
enabled: !!resolvedCompanyId && !!resolvedAgentId && needsDashboardData,
|
||||
});
|
||||
|
||||
const { data: allAgents } = useQuery({
|
||||
|
|
@ -593,7 +593,6 @@ export function AgentDetail() {
|
|||
});
|
||||
|
||||
const assignedIssues = (allIssues ?? [])
|
||||
.filter((i) => i.assigneeAgentId === agent?.id)
|
||||
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||
const reportsToAgent = (allAgents ?? []).find((a) => a.id === agent?.reportsTo);
|
||||
const directReports = (allAgents ?? []).filter((a) => a.reportsTo === agent?.id && a.status !== "terminated");
|
||||
|
|
@ -1175,12 +1174,15 @@ function AgentOverview({
|
|||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">Recent Issues</h3>
|
||||
<Link to={`/issues?assignee=${agentId}`} className="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
||||
<Link
|
||||
to={`/issues?participantAgentId=${agentId}`}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
See All →
|
||||
</Link>
|
||||
</div>
|
||||
{assignedIssues.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No assigned issues.</p>
|
||||
<p className="text-sm text-muted-foreground">No recent issues.</p>
|
||||
) : (
|
||||
<div className="border border-border rounded-lg">
|
||||
{assignedIssues.slice(0, 10).map((issue) => (
|
||||
|
|
|
|||
|
|
@ -77,9 +77,64 @@ export function InstanceSettings() {
|
|||
},
|
||||
});
|
||||
|
||||
const disableAllMutation = useMutation({
|
||||
mutationFn: async (agentRows: InstanceSchedulerHeartbeatAgent[]) => {
|
||||
const enabled = agentRows.filter((a) => a.heartbeatEnabled);
|
||||
if (enabled.length === 0) return enabled;
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
enabled.map(async (agentRow) => {
|
||||
const agent = await agentsApi.get(agentRow.id, agentRow.companyId);
|
||||
const runtimeConfig = asRecord(agent.runtimeConfig) ?? {};
|
||||
const heartbeat = asRecord(runtimeConfig.heartbeat) ?? {};
|
||||
await agentsApi.update(
|
||||
agentRow.id,
|
||||
{
|
||||
runtimeConfig: {
|
||||
...runtimeConfig,
|
||||
heartbeat: { ...heartbeat, enabled: false },
|
||||
},
|
||||
},
|
||||
agentRow.companyId,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const failures = results.filter((result): result is PromiseRejectedResult => result.status === "rejected");
|
||||
if (failures.length > 0) {
|
||||
const firstError = failures[0]?.reason;
|
||||
const detail = firstError instanceof Error ? firstError.message : "Unknown error";
|
||||
throw new Error(
|
||||
failures.length === 1
|
||||
? `Failed to disable 1 timer heartbeat: ${detail}`
|
||||
: `Failed to disable ${failures.length} of ${enabled.length} timer heartbeats. First error: ${detail}`,
|
||||
);
|
||||
}
|
||||
return enabled;
|
||||
},
|
||||
onSuccess: async (updatedRows) => {
|
||||
setActionError(null);
|
||||
const companies = new Set(updatedRows.map((row) => row.companyId));
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.instance.schedulerHeartbeats }),
|
||||
...Array.from(companies, (companyId) =>
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(companyId) }),
|
||||
),
|
||||
...updatedRows.map((row) =>
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(row.id) }),
|
||||
),
|
||||
]);
|
||||
},
|
||||
onError: (error) => {
|
||||
setActionError(error instanceof Error ? error.message : "Failed to disable all heartbeats.");
|
||||
},
|
||||
});
|
||||
|
||||
const agents = heartbeatsQuery.data ?? [];
|
||||
const activeCount = agents.filter((agent) => agent.schedulerActive).length;
|
||||
const disabledCount = agents.length - activeCount;
|
||||
const enabledCount = agents.filter((agent) => agent.heartbeatEnabled).length;
|
||||
const anyEnabled = enabledCount > 0;
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
const map = new Map<string, { companyName: string; agents: InstanceSchedulerHeartbeatAgent[] }>();
|
||||
|
|
@ -120,10 +175,27 @@ export function InstanceSettings() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span><span className="font-semibold text-foreground">{activeCount}</span> active</span>
|
||||
<span><span className="font-semibold text-foreground">{disabledCount}</span> disabled</span>
|
||||
<span><span className="font-semibold text-foreground">{grouped.length}</span> {grouped.length === 1 ? "company" : "companies"}</span>
|
||||
{anyEnabled && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="ml-auto h-7 text-xs"
|
||||
disabled={disableAllMutation.isPending}
|
||||
onClick={() => {
|
||||
const noun = enabledCount === 1 ? "agent" : "agents";
|
||||
if (!window.confirm(`Disable timer heartbeats for all ${enabledCount} enabled ${noun}?`)) {
|
||||
return;
|
||||
}
|
||||
disableAllMutation.mutate(agents);
|
||||
}}
|
||||
>
|
||||
{disableAllMutation.isPending ? "Disabling..." : "Disable All"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{actionError && (
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export function Issues() {
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
const initialSearch = searchParams.get("q") ?? "";
|
||||
const participantAgentId = searchParams.get("participantAgentId") ?? undefined;
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
const handleSearchChange = useCallback((search: string) => {
|
||||
clearTimeout(debounceRef.current);
|
||||
|
|
@ -86,8 +87,8 @@ export function Issues() {
|
|||
}, [setBreadcrumbs]);
|
||||
|
||||
const { data: issues, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!),
|
||||
queryKey: [...queryKeys.issues.list(selectedCompanyId!), "participant-agent", participantAgentId ?? "__all__"],
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!, { participantAgentId }),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
|
|
@ -117,6 +118,7 @@ export function Issues() {
|
|||
initialSearch={initialSearch}
|
||||
onSearchChange={handleSearchChange}
|
||||
onUpdateIssue={(id, data) => updateIssue.mutate({ id, data })}
|
||||
searchFilters={participantAgentId ? { participantAgentId } : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue