fix(routing): הוראת-יו"ר תעבוד בדרך הטבעית — הערה פותחת הרצת-CEO
**התסמין:** היו"ר כותב הוראה על תיק והסוכן "מתעלם". ההרצה מדווחת succeeded/exit 0 ואפילו עולה כסף — ולכן זה שרד חודשים בלי שמישהו שם לב. **שתי סיבות-שורש בלתי-תלויות, שתיהן מאומתות:** 1. ה-wakeup הישיר על ה-issue של התיק מבוטל תמיד: תיק שממתין ליו"ר הוא `in_review` ומשויך *לאדם*, ו-Paperclip עונה `issue_assignee_changed` → "אעיר את הבעלים החדש" → הבעלים אדם → אף סוכן לא מתעורר. 2. רשת-הביטחון בפלאגין מוסרת את ההוראה כ-`payload.prompt`, וה-runner קורא רק `payload.issueId` — הפרומפט נבלע. 18/18 מדידה ב-DB: 0% מסירה, אי-פעם. **התיקון:** המסלול הטבעי (הערה בטאב-הסוכנים) מפרסם את ההערה בשרשור כרגיל **וגם** פותח הרצת-CEO עם ההוראה, דרך `pc_open_ceo_run` — הפרימיטיב היחיד שאומת כמגיע לסוכן (הוכח היום על 1043-02-26: 3,212→7,697 מילים). אותו תיקון ל-interaction-response, שסובל מאותה תקלה (החצי השני של #228). **G2 — הסרת מסלול מקביל:** ה-wakeup הנדון-לביטול הוסר מ-`post_comment`; הוא היה המסלול השני, זה ששותק. כעת מסלול-מסירה קנוני אחד. `mark_comment_routed` מסמן את ההערה כמנותבת כדי שה-sweep לא יירה הרצת-סרק על מה שכבר טופל. **G12 — שער-הפלטפורמה:** המגע החדש (`mark_comment_routed`) נחשף דרך `agent_platform_port.py` בלבד; leak-guard עובר. התיקון יושב ב-backend (REST) ולא בפלאגין, ולכן אינו חשוף לבאג ה-invocation-scope שמפיל את ה-sweep. תועד ב-docs/paperclip-quirks.md §7 עם המלכודות שנתקלנו בהן. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -327,6 +327,40 @@ async def _create_issue(
|
||||
return issue_id, identifier
|
||||
|
||||
|
||||
async def mark_comment_routed(issue_id: str, comment_id: str) -> dict:
|
||||
"""Claim a chair comment as already routed, so the plugin sweep skips it.
|
||||
|
||||
The plugin's `route-pending-comments` sweep re-routes any user comment newer
|
||||
than the newest agent comment on the same issue. A chair instruction answered
|
||||
on a CEO **child** issue leaves the parent with no agent reply, so the sweep
|
||||
would keep re-routing it forever. Writing the sweep's own marker
|
||||
(`last-routed-comment-id`, plugin state, issue scope) is what tells it the
|
||||
comment is handled — this is the same key `markCommentRouted` sets in
|
||||
`plugin-legal-ai/src/worker.ts`.
|
||||
"""
|
||||
if not comment_id:
|
||||
return {"ok": False, "error": "no_comment_id"}
|
||||
try:
|
||||
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
||||
try:
|
||||
await conn.execute(
|
||||
"""INSERT INTO plugin_state (plugin_id, scope_kind, scope_id, namespace, state_key, value_json)
|
||||
VALUES ($1::uuid, 'issue', $2, 'default', 'last-routed-comment-id', $3::jsonb)
|
||||
ON CONFLICT (plugin_id, scope_kind, scope_id, namespace, state_key)
|
||||
DO UPDATE SET value_json = $3::jsonb""",
|
||||
PLUGIN_ID, issue_id, json.dumps(comment_id),
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
return {"ok": True, "issue_id": issue_id, "comment_id": comment_id}
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"mark_comment_routed failed for issue %s comment %s: %s",
|
||||
issue_id, comment_id, e,
|
||||
)
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
|
||||
async def _link_case_to_issue(conn: asyncpg.Connection, issue_id: str, case_number: str) -> None:
|
||||
"""Store the legal-ai case number in plugin state, linked to the issue."""
|
||||
await conn.execute(
|
||||
@@ -655,10 +689,14 @@ async def get_agents_for_case(company_id: str, issue_ids: list[str]) -> list[dic
|
||||
|
||||
|
||||
async def post_comment(issue_id: str, company_id: str, body: str) -> dict:
|
||||
"""Post a comment on a Paperclip issue.
|
||||
"""Post a comment on a Paperclip issue. Records only — wakes nobody.
|
||||
|
||||
Tries the Board API first (triggers plugin events for CEO routing).
|
||||
Falls back to direct DB insert + CEO wakeup if API fails.
|
||||
Delivering the instruction is the caller's job, via :func:`open_ceo_run`. This
|
||||
function used to also wake the CEO on ``issue_id``, but that wakeup is dead on
|
||||
arrival for the case issues the chair actually comments on: they are
|
||||
`in_review` and owned by her, so Paperclip cancels the run with
|
||||
`issue_assignee_changed` before it starts. Keeping it would leave two parallel
|
||||
delivery paths — one that works and one that silently doesn't (INV-G2).
|
||||
"""
|
||||
# Try Board API first — this triggers the event bus
|
||||
if PAPERCLIP_BOARD_API_KEY:
|
||||
@@ -675,7 +713,6 @@ async def post_comment(issue_id: str, company_id: str, body: str) -> dict:
|
||||
except Exception:
|
||||
logger.debug("Board API comment failed for issue %s, falling back to DB", issue_id)
|
||||
|
||||
# Fallback: direct DB insert + explicit CEO wakeup
|
||||
comment_id = str(uuid.uuid4())
|
||||
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
||||
try:
|
||||
@@ -688,23 +725,6 @@ async def post_comment(issue_id: str, company_id: str, body: str) -> dict:
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
# Wake the correct CEO for this company
|
||||
ceo_id = CEO_AGENTS.get(company_id, CEO_AGENT_ID)
|
||||
try:
|
||||
await pc_request(
|
||||
"POST",
|
||||
f"/api/agents/{ceo_id}/wakeup",
|
||||
json={
|
||||
"source": "on_demand",
|
||||
"triggerDetail": "manual",
|
||||
"reason": f"user_comment_{issue_id}",
|
||||
"payload": {"issueId": issue_id, "mutation": "comment"},
|
||||
},
|
||||
raise_on_error=True,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Failed to wake CEO after DB comment on issue %s", issue_id)
|
||||
|
||||
return {"comment_id": comment_id, "issue_id": issue_id, "method": "db_fallback"}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user