diff --git a/web-ui/src/components/cases/agent-status-widget.tsx b/web-ui/src/components/cases/agent-status-widget.tsx
index 86f4ee8..0171521 100644
--- a/web-ui/src/components/cases/agent-status-widget.tsx
+++ b/web-ui/src/components/cases/agent-status-widget.tsx
@@ -1,8 +1,20 @@
"use client";
-import { useAgentActivity } from "@/lib/api/agents";
+import { useState } from "react";
+import { useAgentActivity, useResetCaseAgents } from "@/lib/api/agents";
import type { PaperclipAgentStatus } from "@/lib/api/agents";
-import { Bot } from "lucide-react";
+import { Bot, RotateCcw, Loader2 } from "lucide-react";
+import { toast } from "sonner";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
/* ── Status dot colors ───────────────────────────────────────── */
@@ -40,6 +52,75 @@ function AgentRow({ agent }: { agent: PaperclipAgentStatus }) {
);
}
+/* ── Reset button ─────────────────────────────────────────────── */
+
+function ResetAgentsButton({ caseNumber }: { caseNumber: string }) {
+ const [open, setOpen] = useState(false);
+ const reset = useResetCaseAgents(caseNumber);
+
+ function handleConfirm() {
+ reset.mutate(undefined, {
+ onSuccess: (result) => {
+ setOpen(false);
+ const agentNames = result.reset_agents
+ .filter((a) => a.ok)
+ .map((a) => a.name)
+ .join(", ");
+ const msg = agentNames
+ ? `סוכנים אופסו: ${agentNames}`
+ : "אופסו בהצלחה";
+ toast.success(msg);
+ },
+ onError: () => {
+ setOpen(false);
+ toast.error("שגיאה בביצוע האיפוס");
+ },
+ });
+ }
+
+ return (
+
+ );
+}
+
/* ── Widget ───────────────────────────────────────────────────── */
export function AgentStatusWidget({
@@ -54,6 +135,7 @@ export function AgentStatusWidget({
const agents = data.agents ?? [];
const activeCount = agents.filter((a) => a.status === "active" || a.status === "running").length;
+ const hasErrors = agents.some((a) => a.status === "error");
return (
@@ -62,11 +144,14 @@ export function AgentStatusWidget({
סוכנים
- {agents.length > 0 && (
-
- {activeCount} פעילים מתוך {agents.length}
-
- )}
+
+ {hasErrors && }
+ {agents.length > 0 && (
+
+ {activeCount} פעילים מתוך {agents.length}
+
+ )}
+
diff --git a/web-ui/src/lib/api/agents.ts b/web-ui/src/lib/api/agents.ts
index f749ffb..4079ebe 100644
--- a/web-ui/src/lib/api/agents.ts
+++ b/web-ui/src/lib/api/agents.ts
@@ -179,3 +179,25 @@ export function useSubmitInteraction(caseNumber: string | undefined) {
},
});
}
+
+export type AgentResetResult = {
+ ok: boolean;
+ reassigned_issues: { id: string; identifier: string }[];
+ reset_agents: { id: string; name: string; ok: boolean; error?: string }[];
+};
+
+export function useResetCaseAgents(caseNumber: string | undefined) {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: () =>
+ apiRequest
(
+ `/api/cases/${caseNumber}/agents/reset`,
+ { method: "POST" },
+ ),
+ onSuccess: () => {
+ if (caseNumber) {
+ qc.invalidateQueries({ queryKey: agentKeys.activity(caseNumber) });
+ }
+ },
+ });
+}
diff --git a/web/agent_platform_port.py b/web/agent_platform_port.py
index 4af9c16..5f65f05 100644
--- a/web/agent_platform_port.py
+++ b/web/agent_platform_port.py
@@ -52,6 +52,7 @@ from web.paperclip_client import (
post_comment as pc_post_comment,
reject_interaction as pc_reject_interaction,
reset_agent_session as pc_reset_agent_session,
+ reset_case_agents as pc_reset_case_agents,
respond_to_interaction as pc_respond_to_interaction,
restore_project as pc_restore_project,
update_project_name as pc_update_project_name,
@@ -114,4 +115,5 @@ __all__ = [
"pc_get_run_events",
"pc_cancel_run",
"pc_reset_agent_session",
+ "pc_reset_case_agents",
]
diff --git a/web/app.py b/web/app.py
index 2d45b6f..a1b141a 100644
--- a/web/app.py
+++ b/web/app.py
@@ -72,6 +72,7 @@ from web.agent_platform_port import (
pc_reject_interaction,
pc_request,
pc_reset_agent_session,
+ pc_reset_case_agents,
pc_respond_to_interaction,
pc_restore_project,
pc_wake_analyst_for_appraiser_facts,
@@ -4173,6 +4174,19 @@ async def api_post_interaction_response(
raise HTTPException(502, f"שגיאת Paperclip: {e}")
+@app.post("/api/cases/{case_number}/agents/reset")
+async def api_reset_case_agents(case_number: str):
+ """Reset stuck agents for a case.
+
+ Clears writer/QA agents from 'error' status and reassigns any open
+ issues back to the chair user, stopping Paperclip recovery loops.
+ """
+ result = await pc_reset_case_agents(case_number)
+ if not result.get("ok"):
+ raise HTTPException(502, result.get("error", "שגיאה בביצוע האיפוס"))
+ return result
+
+
# ── Settings: MCP Server Configuration ────────────────────────────
#
# Source of truth for legal-ai env vars is Coolify (see memory:
diff --git a/web/paperclip_client.py b/web/paperclip_client.py
index 0186318..cbe83a8 100644
--- a/web/paperclip_client.py
+++ b/web/paperclip_client.py
@@ -791,6 +791,71 @@ async def reset_agent_session(agent_id: str) -> dict:
return resp.json()
+CHAIM_USER_ID = "ZpDWXxFweC3MftuF1Ttyu2VUbSPHbKZd"
+
+
+async def reset_case_agents(case_number: str) -> dict:
+ """Reset agent state for a case: clear error status + reassign stuck issues to user.
+
+ Two actions:
+ 1. Any non-completed issue still assigned to an agent is reassigned to the chair user,
+ stopping Paperclip's source_scoped_recovery_action loop.
+ 2. Every agent in the case's company whose global status is 'error' gets a
+ reset_agent_session call (clears wedged runtime) and its DB status set to 'idle'.
+ """
+ first_digit = case_number.split("-")[0][0] if case_number else "1"
+ company_id = COMPANIES["betterment"] if first_digit in ("8", "9") else COMPANIES["licensing"]
+
+ conn = await asyncpg.connect(PAPERCLIP_DB_URL)
+ try:
+ project = await conn.fetchrow(
+ "SELECT id FROM projects WHERE name LIKE $1 LIMIT 1",
+ f"%{case_number}%",
+ )
+ if not project:
+ return {"ok": False, "error": f"No Paperclip project found for {case_number}"}
+
+ project_id = project["id"]
+
+ reassigned = await conn.fetch(
+ """UPDATE issues
+ SET assignee_agent_id = null, assignee_user_id = $1, updated_at = now()
+ WHERE project_id = $2
+ AND assignee_agent_id IS NOT NULL
+ AND status NOT IN ('done', 'cancelled')
+ RETURNING id, identifier""",
+ CHAIM_USER_ID, project_id,
+ )
+
+ error_agents = await conn.fetch(
+ "SELECT id, name FROM agents WHERE company_id = $1::uuid AND status = 'error'",
+ company_id,
+ )
+
+ if error_agents:
+ await conn.execute(
+ "UPDATE agents SET status = 'idle' WHERE id = ANY($1::uuid[])",
+ [r["id"] for r in error_agents],
+ )
+ finally:
+ await conn.close()
+
+ reset_results = []
+ for agent in error_agents:
+ try:
+ await reset_agent_session(str(agent["id"]))
+ reset_results.append({"id": str(agent["id"]), "name": agent["name"], "ok": True})
+ except Exception as e:
+ logger.warning("reset_agent_session failed for %s: %s", agent["id"], e)
+ reset_results.append({"id": str(agent["id"]), "name": agent["name"], "ok": False, "error": str(e)})
+
+ return {
+ "ok": True,
+ "reassigned_issues": [{"id": str(r["id"]), "identifier": r["identifier"]} for r in reassigned],
+ "reset_agents": reset_results,
+ }
+
+
async def respond_to_interaction(
issue_id: str, interaction_id: str, payload: dict,
) -> dict: