feat(case-ui): agents tab v2 + dismiss/reaper for stale interactions (#215)
מוקאפ 18i v2 (מאושר ב-Claude Design) + תיקון-מקור להצטברות pending interactions. UI (agent-activity-feed.tsx): - רוסטר-סוכנים קומפקטי, כל הסוכנים בשורה אחת (grid-cols-9); הוסרה שורת "מה הסוכן עושה עכשיו" — היא שייכה תגובה לפי role-fallback, כך שסוכנים בעלי אותו תפקיד הציגו טקסט זהה. - כל אירוע "ממתין לתשובתך" מקופל כאקורדיון (הראשון פתוח). - כפתור "התעלם" לכל בקשה ממתינה — מבטל שאלה כפולה/מיושנת בלי להעיר את הסוכן. - ציר-הזמן מקובץ לפי משימה (issue), כל קבוצה אקורדיון עם מזהה+סטטוס+מונה. Backend (#215 — מניעת הצטברות pending): - cancel_interaction: ביטול interaction בודד ישירות ל-cancelled, ללא wake_assignee (resolve רגיל היה מעיר את הסוכן). אין triggers על הטבלה. - reap_stale_interactions: reaper שמבטל pending על issues סגורים (done/cancelled) — המקור הדומיננטי לערימה. רץ כל 15 דק' ב-lifespan. - endpoint POST /api/cases/{case}/agents/interaction-dismiss (מאחורי כפתור "התעלם"). הכל דרך agent_platform_port (G12). supersede-at-creation נדחה (תועד ב-#215): ביטול-אוטומטי של שאלה על issue פעיל אחד על-פני אחר אינו דטרמיניסטי; הליבה הבטוחה = reaper-ליתומים + כפתור-ביטול, והשורש האמיתי הוא ריסון לולאות-recovery של Paperclip. Invariants: מקיים G12 (מגע-Paperclip רק דרך הפורט), G2 (אין מסלול מקביל — מרחיב את endpoints הסוכנים הקיימים), §6 (אין בליעת-שגיאות שקטה — הביטול מחזיר {ok,error}, ה-reaper מתעד warning). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
53
web/app.py
53
web/app.py
@@ -57,6 +57,7 @@ from web.agent_platform_port import (
|
||||
get_project_url,
|
||||
pc_accept_interaction,
|
||||
pc_archive_project,
|
||||
pc_cancel_interaction,
|
||||
pc_cancel_run,
|
||||
pc_create_project,
|
||||
pc_create_workflow_issue,
|
||||
@@ -69,6 +70,7 @@ from web.agent_platform_port import (
|
||||
pc_get_run_log,
|
||||
pc_list_live_runs,
|
||||
pc_post_comment,
|
||||
pc_reap_stale_interactions,
|
||||
pc_reject_interaction,
|
||||
pc_request,
|
||||
pc_reset_agent_session,
|
||||
@@ -101,19 +103,38 @@ PROGRESS_TTL_SECONDS = 300
|
||||
_progress = ProgressStore(config.REDIS_URL, ttl_seconds=PROGRESS_TTL_SECONDS)
|
||||
|
||||
|
||||
async def _interaction_reaper_loop():
|
||||
"""Safety-net reaper for stranded pending interactions (TaskMaster #215).
|
||||
|
||||
Every 15 min, cancel pending interactions on closed (done/cancelled) issues
|
||||
so the chair's "awaiting you" count never accumulates dead questions.
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(900)
|
||||
await pc_reap_stale_interactions()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
logger.warning("interaction reaper sweep failed", exc_info=True)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
await db.init_schema()
|
||||
sync_task = asyncio.create_task(git_sync.sweep_loop())
|
||||
reaper_task = asyncio.create_task(_interaction_reaper_loop())
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sync_task.cancel()
|
||||
try:
|
||||
await sync_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
reaper_task.cancel()
|
||||
for _t in (sync_task, reaper_task):
|
||||
try:
|
||||
await _t
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
await db.close_pool()
|
||||
await _progress.close()
|
||||
|
||||
@@ -4411,6 +4432,30 @@ async def api_post_interaction_response(
|
||||
raise HTTPException(502, f"שגיאת Paperclip: {e}")
|
||||
|
||||
|
||||
class InteractionDismissRequest(BaseModel):
|
||||
issue_id: str
|
||||
interaction_id: str
|
||||
|
||||
|
||||
@app.post("/api/cases/{case_number}/agents/interaction-dismiss")
|
||||
async def api_dismiss_interaction(
|
||||
case_number: str, req: InteractionDismissRequest,
|
||||
):
|
||||
"""Dismiss a pending interaction WITHOUT waking the issue assignee.
|
||||
|
||||
For stale/duplicate questions (TaskMaster #215). Unlike interaction-response,
|
||||
this does not resolve via Paperclip's wake_assignee path — it cancels the row
|
||||
so the chair can clear noise without triggering an agent run.
|
||||
"""
|
||||
issues = await pc_get_case_issues(case_number)
|
||||
if not any(i["id"] == req.issue_id for i in issues):
|
||||
raise HTTPException(404, f"Issue {req.issue_id} לא שייך לתיק {case_number}")
|
||||
result = await pc_cancel_interaction(req.issue_id, req.interaction_id)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(404, result.get("error", "ביטול הבקשה נכשל"))
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/cases/{case_number}/agents/reset")
|
||||
async def api_reset_case_agents(case_number: str):
|
||||
"""Reset stuck agents for a case.
|
||||
|
||||
Reference in New Issue
Block a user