feat(agents): reset button + endpoint to clear stuck agent error state
Adds a one-click 'reset agents' action for cases where writer/QA agents
are stuck in Paperclip's error state (triggered by recovery loop or
failed run). Addresses the root cause documented in
reference_paperclip_recovery_loops: reassigns open issues from agents
back to the chair user, and calls reset_agent_session for each error
agent to clear wedged runtime sessions.
Changes:
- paperclip_client.py: reset_case_agents() — reassigns stuck issues +
clears error status in Paperclip DB + calls reset_agent_session
- agent_platform_port.py: exports pc_reset_case_agents (G12 gate)
- app.py: POST /api/cases/{case_number}/agents/reset endpoint
- agents.ts: useResetCaseAgents mutation hook + AgentResetResult type
- agent-status-widget.tsx: 'אפס' button (shown only when error agents
exist) with Dialog confirmation + loading state + toast feedback
Invariants: G12 (Paperclip only via port), G2 (no parallel path —
uses existing reset_agent_session + pc_get_case_issues).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
14
web/app.py
14
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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user