Captures the full deletion procedure we worked out empirically while
wiping case 8174-24 for a clean rerun. Covers all four systems where
case state lives, in dependency order:
1. legal-ai DB + on-disk dir — DELETE /api/cases?remove_files=true
(now actually works after 903fb4d added the missing db.delete_case)
2. Paperclip DB — no API; raw SQL with explicit FK-blocker ordering
(issue_comments, cost_events, finance_events, feedback_votes,
issue_inbox_archives, issue_read_states must go before issues;
heartbeat_runs.wakeup_request_id must be NULLed before
agent_wakeup_requests can be deleted)
3. Gitea — DELETE /api/v1/repos/cases/{N}
4. Verification queries for each system
Two gotchas worth highlighting in the doc:
• The case directory inside /data/cases is owned by root because the
container runs as root — host-side rm needs sudo, or use the API
(rmtree happens inside the container).
• Paperclip projects are referenced via name LIKE '%{N}%' since
there's no slug column. Stricter matching is recommended if N
appears in multiple project names.
Linked from legal-ai/CLAUDE.md docs index. A future scripts/delete-case.sh
that automates the runbook with a confirmation prompt is noted as TODO
inside the runbook itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.6 KiB
מחיקת תיק — runbook
מתי להשתמש: reset שלם של תיק (לבדיקות end-to-end), מחיקת תיק שנפתח בטעות, או ניקיון לפני העלאה חוזרת של מסמכים.
חשוב: ה-API
DELETE /api/casesבלבד לא מספיק — הוא מטפל רק בצד legal-ai (DB + on-disk dir). תיק חי במקביל ב-4 מערכות והכול חייב להתנקות יחד.
איפה ה-state של תיק חי
| מערכת | מה נשמר | איך מנקים |
|---|---|---|
| legal-ai DB (port 5433) | cases + documents + document_chunks + claims + appraiser_facts + decisions + qa_results + case_precedents |
API DELETE (cascade על FK) |
| legal-ai disk | /data/cases/{N}/ בתוך ה-container — מכיל drafts/, documents/, .git/ |
API עם remove_files=true (shutil.rmtree בתוך ה-container) |
| Paperclip DB (port 54329) | projects + issues + issue_comments + agent_wakeup_requests + heartbeat_runs (audit) + עוד 6+ טבלאות |
SQL ידני (אין API) |
| Gitea | repo cases/{N} אם נוצר ב-case-create |
Gitea API |
ה-API לא מטפל ב-Paperclip ו-Gitea כי אלה מערכות חיצוניות שלגמרי מחוץ ל-DB של legal-ai. תועד מפורשות ב-docstring של services/db.py:delete_case.
תהליך מחיקה מלא — שלב אחרי שלב
הצב את מספר התיק במשתנה לפני שמתחילים:
CASE_NUMBER=8174-24
שלב 1 — legal-ai (DB + disk)
curl -s -X DELETE \
"https://legal-ai.nautilus.marcusgroup.org/api/cases?case_number=${CASE_NUMBER}&remove_files=true" \
-w "\nhttp=%{http_code}\n"
תוצאה צפויה: 200 עם {"deleted": true, "removed_files": true, ...}.
מה זה עושה מאחורי הקלעים:
DELETE FROM cases— מפעיל CASCADE ל-7 טבלאות, SET NULL ל-audit_logו-chair_feedback.shutil.rmtree(/data/cases/{N})— מסיר את כל הספרייה כולל.git.
הערה: עד לפני commit
903fb4dה-endpoint הזה החזיר 500 כיdb.delete_caseלא היה מוגדר. אם נתקלת ב-500 בגרסה ישנה, השתמש ב-SQL הישיר (ראה Fallback בסוף).
שלב 2 — Paperclip
אין API. SQL ישיר:
PGPASSWORD=paperclip psql -h localhost -p 54329 -U paperclip -d paperclip <<SQL
BEGIN;
-- 1. מצא את כל ה-issues של הפרויקט (לפי שם)
CREATE TEMP TABLE _issue_ids AS
SELECT i.id, i.identifier
FROM issues i
JOIN projects p ON i.project_id = p.id
WHERE p.name LIKE '%${CASE_NUMBER}%';
SELECT identifier FROM _issue_ids ORDER BY identifier; -- וידוא לפני המחיקה
-- 2. מחק blockers ל-FK עם NO ACTION (אסור למחוק issue אם יש להם reference)
DELETE FROM issue_comments WHERE issue_id IN (SELECT id FROM _issue_ids);
DELETE FROM cost_events WHERE issue_id IN (SELECT id FROM _issue_ids);
DELETE FROM finance_events WHERE issue_id IN (SELECT id FROM _issue_ids);
DELETE FROM feedback_votes WHERE issue_id IN (SELECT id FROM _issue_ids);
DELETE FROM issue_inbox_archives WHERE issue_id IN (SELECT id FROM _issue_ids);
DELETE FROM issue_read_states WHERE issue_id IN (SELECT id FROM _issue_ids);
-- 3. מחק את ה-issues. CASCADE מטפל ב-7 טבלאות נוספות:
-- issue_approvals, issue_attachments, issue_documents,
-- issue_execution_decisions, issue_labels, issue_relations,
-- issue_work_products
DELETE FROM issues WHERE id IN (SELECT id FROM _issue_ids);
-- 4. שבור FK מ-heartbeat_runs כדי שאפשר יהיה למחוק wakeup_requests.
-- heartbeat_runs נשמרים כ-audit log לא משויך.
UPDATE heartbeat_runs
SET wakeup_request_id = NULL
WHERE wakeup_request_id IN (
SELECT id FROM agent_wakeup_requests
WHERE payload->>'issueId' IN (SELECT id::text FROM _issue_ids)
);
DELETE FROM agent_wakeup_requests
WHERE payload->>'issueId' IN (SELECT id::text FROM _issue_ids);
-- 5. מחק blockers ברמת ה-project (NO ACTION FK ל-projects)
DELETE FROM cost_events WHERE project_id IN (SELECT id FROM projects WHERE name LIKE '%${CASE_NUMBER}%');
DELETE FROM finance_events WHERE project_id IN (SELECT id FROM projects WHERE name LIKE '%${CASE_NUMBER}%');
-- 6. מחק את הפרויקט. CASCADE מטפל ב:
-- execution_workspaces, project_goals, project_workspaces, routines
DELETE FROM projects WHERE name LIKE '%${CASE_NUMBER}%' RETURNING id, name;
COMMIT;
SQL
למה Paperclip לא הוסיף API למחיקה? כי זאת מערכת רב-משתמשית ומחיקה היא הרסנית מטבעה — Paperclip מעדיף
archive(projects.archived_at). אנחנו אכן רוצים מחיקה אמיתית רק לסביבת בדיקות.
שלב 3 — Gitea (אם repo נוצר)
GITEA_TOKEN=$(infisical secrets get GITEA__API_TOKEN --silent || \
echo "$GITEA_TOKEN") # סגדור מ-Infisical או ENV
curl -s -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"https://gitea.nautilus.marcusgroup.org/api/v1/repos/cases/${CASE_NUMBER}" \
-w "http=%{http_code}\n"
תוצאה צפויה: 204 (deleted) או 404 (לא נוצר מעולם).
שלב 4 — וידוא ניקיון
echo "=== legal-ai ==="
PGPASSWORD=$LEGAL_AI_PG psql -h localhost -p 5433 -U legal_ai -d legal_ai -t -c "
SELECT count(*) FROM cases WHERE case_number = '${CASE_NUMBER}';
" # → 0
ls /home/chaim/legal-ai/data/cases/${CASE_NUMBER} 2>&1 | head -1
# → "No such file or directory"
echo "=== Paperclip ==="
PGPASSWORD=paperclip psql -h localhost -p 54329 -U paperclip -d paperclip -t -c "
SELECT 'projects:'||count(*) FROM projects WHERE name LIKE '%${CASE_NUMBER}%'
UNION ALL SELECT 'issues:'||count(*) FROM issues WHERE title LIKE '%${CASE_NUMBER}%'
UNION ALL SELECT 'comments:'||count(*) FROM issue_comments WHERE body LIKE '%${CASE_NUMBER}%'
UNION ALL SELECT 'wakeups:'||count(*) FROM agent_wakeup_requests WHERE payload::text LIKE '%${CASE_NUMBER}%';
" # → all 0
echo "=== Gitea ==="
curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"https://gitea.nautilus.marcusgroup.org/api/v1/repos/cases/${CASE_NUMBER}" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('full_name','NOT FOUND'))"
# → NOT FOUND
Fallback — אם ה-API נשבר
אם משום מה ה-API DELETE לא עובד (ראינו את זה בעבר עם delete_case החסר), עשה DELETE ישיר ב-DB. ה-FK constraints יבצעו את העבודה:
PGPASSWORD=$LEGAL_AI_PG psql -h localhost -p 5433 -U legal_ai -d legal_ai -c "
DELETE FROM cases WHERE case_number = '${CASE_NUMBER}' RETURNING case_number, title;
"
לאחר מכן הסר את הספרייה מהדיסק. הספרייה בבעלות root כי ה-container רץ כ-root, אז תצטרך sudo:
sudo rm -rf /home/chaim/legal-ai/data/cases/${CASE_NUMBER}
הערות שנלמדו תוך כדי
-
heartbeat_runs.wakeup_request_idהוא ה-trap היחיד. הוא NO ACTION FK, ולכן חוסם מחיקה שלagent_wakeup_requests. הפתרון:UPDATE ... SET wakeup_request_id = NULLלפני המחיקה. ה-runs עצמם נשמרים כ-audit log (לא הפסד). -
פרויקט "name" ב-Paperclip — לפי הקונבנציה הוא מתחיל ב-"ערר {N}" — לכן
LIKE '%{N}%'מספיק. אם יש מספר תיקים שמכילים את אותו מספר, להחמיר עם match מלא או לפיid. -
Container ↔ host file ownership — קבצים שיוצר ה-container (כולל ספריית התיק) שייכים ל-
root. מחיקה מהמארח דורשתsudo, או דרך docker exec, או דרך ה-API (שמבצעתrmtreeבתוך ה-container). -
audit_logו-chair_feedbackנשארים — FK שלהם הוא SET NULL כדי לשמור היסטוריה גם אחרי שהתיק נמחק. אם אתה צריך מחיקה היסטרית מוחלטת, מחק שורות אלה ידנית.
TODO — אוטומציה
ה-runbook הזה ניתן להמרה לסקריפט scripts/delete-case.sh שמקבל CASE_NUMBER ומבצע את 4 השלבים עם prompt confirmation. עדיין לא הוטמע — נכון להיום העבודה ידנית.
מי שמטמיע: שמור את הסקריפט כ-destructive ב-SCRIPTS.md ודרוש --confirm או prompt אינטראקטיבי. אסור שיעבוד בלי אישור מפורש.