From 7d86ed4a627afd2cc72cd0350ca24a245e12e84c Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 27 Apr 2026 19:14:12 +0000 Subject: [PATCH] Archive: also cancel open Paperclip issues to clear agent widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a case is archived, the legal-ai UI's AgentStatusWidget kept showing "agents started working, waiting for first report" because related Paperclip issues remained in 'todo' / 'in_progress' status. Concrete example: case 1130-25 had two open issues (CMP-15 ניתוח תכנוני, CMP-21 כתיבת החלטה) that lingered after the case was finalized; 1194-25 had two more (CMP-37, CMP-44). Extended pc_archive_project to also UPDATE issues SET status='cancelled', cancelled_at=now() WHERE project_id matches AND status IN ('backlog','todo','in_progress','blocked','in_review'). Returns the list of cancelled issues so the toast can announce the count. Updated cases.ts ArchiveResult.paperclip.issues_cancelled type and the toast message in case-archive-action to surface "(N משימות פתוחות בוטלו)" when relevant. Restore is intentionally unchanged — we don't auto-recreate cancelled issues; if work needs to resume, a fresh issue should be created. Stale issues for 1130-25 / 1194-25 cancelled directly in DB as a one-off cleanup (CMP-15, CMP-21, CMP-37, CMP-44). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/cases/case-archive-action.tsx | 6 ++++- web-ui/src/lib/api/cases.ts | 8 +++++- web/paperclip_client.py | 27 ++++++++++++++++--- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/web-ui/src/components/cases/case-archive-action.tsx b/web-ui/src/components/cases/case-archive-action.tsx index 3c10c1e..a4d267e 100644 --- a/web-ui/src/components/cases/case-archive-action.tsx +++ b/web-ui/src/components/cases/case-archive-action.tsx @@ -19,7 +19,11 @@ import { useArchiveCase, useRestoreCase, type ArchiveResult } from "@/lib/api/ca function paperclipMessage(res: ArchiveResult, action: "archive" | "restore"): string { const verb = action === "archive" ? "אורכן" : "שוחזר"; switch (res.paperclip?.status) { - case "archived": + case "archived": { + const cancelled = res.paperclip.issues_cancelled?.length ?? 0; + const issuesNote = cancelled > 0 ? ` (${cancelled} משימות פתוחות בוטלו)` : ""; + return `התיק ${verb}. גם הפרויקט ב-Paperclip${issuesNote}.`; + } case "restored": return `התיק ${verb}. גם הפרויקט ב-Paperclip ${verb}.`; case "not_found": diff --git a/web-ui/src/lib/api/cases.ts b/web-ui/src/lib/api/cases.ts index b3316fb..6018342 100644 --- a/web-ui/src/lib/api/cases.ts +++ b/web-ui/src/lib/api/cases.ts @@ -100,7 +100,13 @@ export type ArchiveResult = { status: string; case_number: string; archived_at?: string | null; - paperclip?: { status: string; project_id?: string; archived_at?: string | null; message?: string }; + paperclip?: { + status: string; + project_id?: string; + archived_at?: string | null; + message?: string; + issues_cancelled?: Array<{ identifier: string; title: string }>; + }; }; export function useArchiveCase(caseNumber: string | undefined) { diff --git a/web/paperclip_client.py b/web/paperclip_client.py index 4325f03..7e1bf8d 100644 --- a/web/paperclip_client.py +++ b/web/paperclip_client.py @@ -139,12 +139,18 @@ async def create_project( async def archive_project(case_number: str) -> dict: - """Set archived_at on the Paperclip project matching this case number. + """Set archived_at on the Paperclip project matching this case number, + and cancel any open issues so the legal-ai UI's agent widget stops + reporting "agents are working" on a closed case. The project is identified by `name LIKE '%{case_number}%'` (consistent with - `create_project`'s lookup). Idempotent — if already archived, returns the - existing timestamp. + `create_project`'s lookup). Idempotent — re-archiving a project that's + already archived returns the existing timestamp without re-cancelling + issues that have already been completed. """ + # Issue statuses considered "open" — anything not done/cancelled. + OPEN_STATUSES = ("backlog", "todo", "in_progress", "blocked", "in_review") + conn = await asyncpg.connect(PAPERCLIP_DB_URL) try: row = await conn.fetchrow( @@ -156,11 +162,26 @@ async def archive_project(case_number: str) -> dict: ) if not row: return {"status": "not_found", "case_number": case_number} + + cancelled = await conn.fetch( + """UPDATE issues + SET status = 'cancelled', + cancelled_at = now(), + updated_at = now() + WHERE project_id = $1 AND status = ANY($2::text[]) + RETURNING identifier, title""", + row["id"], list(OPEN_STATUSES), + ) + return { "status": "archived", "project_id": str(row["id"]), "name": row["name"], "archived_at": row["archived_at"].isoformat() if row["archived_at"] else None, + "issues_cancelled": [ + {"identifier": r["identifier"], "title": r["title"]} + for r in cancelled + ], } finally: await conn.close()