fix(agents): ניתוב הערת-יו"ר לא ל-issue סגור (cancelled) — תמיד ל-CEO החי
בעיה חוזרת: הקלדת הוראה בתיבת "כתוב הוראה לסוכנים..." נבלעה בשקט. השרת בחר את היעד ב-`active[-1]` תוך סינון `done` בלבד — לא `cancelled`. כשה-child האחרון בוטל (למשל 1027-04-26: "חישוב טיעונים" cancelled שנוצר אחרי ה-issue הראשי), ההערה נותבה אליו; Paperclip מדלג על wakeup ל-issue מבוטל → ההוראה לא הגיעה לאיש. תיקון (G1 — נרמול במקור, לא תיקון-בקריאה): - `pick_default_comment_target` חדש ב-paperclip_client — מעדיף את ה-issue הראשי החי של ה-CEO (top-level+open), נופל ל-open כלשהו, ואז top-level, ואף פעם לא מעדיף child-סגור. `get_case_issues` מחזיר כעת `parent_id`. - app.py מנתב דרכו; מחזיר `issue_status` כדי שה-UI יוכל לסמן יעד-סגור. - נחשף דרך ה-Port (G12) כ-`pc_pick_default_comment_target`. - 6 בדיקות ל-picker (כולל תרחיש 1027-04-26 המדויק). Invariants: G1 (נרמול-במקור), G2 (אין מסלול-ניתוב מקביל), G12 (מגע-Paperclip דרך ה-Port). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -484,7 +484,8 @@ async def get_case_issues(case_number: str) -> list[dict]:
|
||||
"""SELECT DISTINCT ON (i.id)
|
||||
i.id, i.title, i.status, i.identifier, i.priority,
|
||||
i.assignee_agent_id, a.name AS assignee_name,
|
||||
i.started_at, i.completed_at, i.created_at, i.company_id
|
||||
i.started_at, i.completed_at, i.created_at, i.company_id,
|
||||
i.parent_id
|
||||
FROM issues i
|
||||
LEFT JOIN agents a ON i.assignee_agent_id = a.id
|
||||
LEFT JOIN plugin_state ps ON ps.scope_id = i.id::text
|
||||
@@ -510,6 +511,7 @@ async def get_case_issues(case_number: str) -> list[dict]:
|
||||
"completed_at": r["completed_at"].isoformat() if r["completed_at"] else None,
|
||||
"created_at": r["created_at"].isoformat() if r["created_at"] else None,
|
||||
"company_id": str(r["company_id"]),
|
||||
"parent_id": str(r["parent_id"]) if r["parent_id"] else None,
|
||||
}
|
||||
for r in sorted_rows
|
||||
]
|
||||
@@ -517,6 +519,43 @@ async def get_case_issues(case_number: str) -> list[dict]:
|
||||
await conn.close()
|
||||
|
||||
|
||||
# A comment posted to a closed issue never wakes an agent — Paperclip skips the
|
||||
# wakeup for done/cancelled issues, so the chair's instruction is silently
|
||||
# swallowed. The default-target picker MUST therefore avoid these statuses.
|
||||
CLOSED_ISSUE_STATUSES = frozenset({"done", "cancelled"})
|
||||
|
||||
|
||||
def pick_default_comment_target(issues: list[dict]) -> dict:
|
||||
"""Choose which issue a chair comment routes to when none is specified.
|
||||
|
||||
``issues`` is ordered oldest→newest (see :func:`get_case_issues`). Comment
|
||||
routing is meant to reach the CEO, whose live "התחל תהליך ניסוח" issue is
|
||||
top-level (``parent_id is None``) and open. Preference order:
|
||||
1. newest OPEN top-level issue — the live CEO main issue;
|
||||
2. newest OPEN issue of any depth — a live sub-issue if no main is open;
|
||||
3. newest top-level issue even if closed — better than a closed child;
|
||||
4. newest issue overall — last resort.
|
||||
Excluding closed statuses is the fix for the recurring bug where the newest
|
||||
non-done issue was a *cancelled* child, so the wakeup was skipped and the
|
||||
instruction vanished. ``issues`` must be non-empty.
|
||||
"""
|
||||
def is_open(i: dict) -> bool:
|
||||
return i["status"] not in CLOSED_ISSUE_STATUSES
|
||||
|
||||
def is_top(i: dict) -> bool:
|
||||
return i.get("parent_id") is None
|
||||
|
||||
for pred in (
|
||||
lambda i: is_open(i) and is_top(i),
|
||||
is_open,
|
||||
is_top,
|
||||
):
|
||||
matches = [i for i in issues if pred(i)]
|
||||
if matches:
|
||||
return matches[-1]
|
||||
return issues[-1]
|
||||
|
||||
|
||||
async def get_issue_comments(issue_ids: list[str]) -> list[dict]:
|
||||
"""Get all comments on a list of Paperclip issues, with agent metadata."""
|
||||
if not issue_ids:
|
||||
|
||||
Reference in New Issue
Block a user