Document the Paperclip event-shape quirk that silently disabled user-comment→CEO routing: `issue.comment.created` carries the issue id in `event.entityId`, not `payload.issueId` (payload has commentId/bodySnippet/reopened). Includes the empirical log from case 8124-09-24, the issue_assignee_changed cancellation chain, the fix (plugin PR #2), and verification. Invariants: upholds CLAUDE.md "ניתוב comments דרך CEO"; G12/X15 (fix lives in the platform-port shell, not the decision skills). Docs-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
18 KiB
Paperclip Quirks — מלכודות ידועות
הקשר: מה ש-Paperclip עושה בעצמו, מתחת לרגליהם של הסוכנים שלנו, ושאנחנו צריכים לעקוף אותו או לחיות איתו.
כל מלכודת מתועדת עם:
- מה קורה בפועל
- ראיה אמפירית מתוך לוגים
- ההשפעה על הצינור שלנו
- עקיפה / תיקון / קבלה
1. issue.released הופך done ל-todo
מה קורה
לאחר שסוכן מבצע PATCH /api/issues/{id} עם status: done, Paperclip מבצע פעולה נוספת בשם issue.released מספר שניות מאוחר יותר. ל-issue.released יש side-effect לא-מתועד שמחזיר את ה-status ל-todo.
ראיה אמפירית — תיק 8174-24, CMPA-18 (30/04/26)
מתוך activity_log:
ts | action | actor_type | details
----------+---------------------+------------+----------------------------------------
18:14:49 | issue.comment_added | agent | comment by researcher
18:14:57 | issue.updated | agent | {"status": "done", "_previous": {"status": "in_progress"}}
18:15:35 | issue.released | agent | ← here
מצב מ-issues table 38 שניות לאחר ה-released:
identifier | status | updated_at
CMPA-18 | todo | 18:15:35
ה-status חזר מ-done ל-todo למרות שאף סוכן או משתמש לא ביקש זאת.
ההשפעה על הצינור שלנו
Paperclip מזהה issue ב-todo כ"יש עבודה לעשות" → מיד מפעיל wakeup לסוכן הרלוונטי → הסוכן רץ שוב עם prompt cache מלא (~$0.10-0.50 פר-ריצה) → מסתכל סביב ומבין שהעבודה כבר נעשתה → סוגר את ה-issue שוב → issue.released חוזר על עצמו ⇒ פוטנציאל ללולאה.
עקיפה — בצד שלנו (ללא תיקון Paperclip)
הסוכן שלנו עושה זאת כבר היום בהצלחה במקרה שהוא רואה issue ב-todo עם תוצרים קיימים:
- בודק שהקבצים הצפויים קיימים (
Glob /documents/research/*.md) - בודק שה-DB מאוכלס (
mcp__legal-ai__precedent_list,get_claims, וכו') - אם הכל קיים → לא מבצע עבודה כפולה → כותב comment "אין שינוי" →
PATCH issue → done
הראיה: בריצה החוזרת (PID 309786 ב-30/04/26 18:15:54), המנתח של החוקר זיהה תוך 90 שניות שכל 9 התקדימים והקובץ קיימים, וסגר את ה-issue ב-PATCH → done שוב. הריצה הזאת עלתה כ-$0.20 — לא חינם, אבל לא לולאה.
אם תרצה לחקור פנימה
ה-issue.released נרשם ב-activity_log עם actor_type=agent אבל בלי agent_id שמסביר מי. הוא לא נכתב על ידי הסקריפטים שלנו (אנחנו לא קוראים endpoint כזה). מקור אפשרי:
- מנגנון
executionLockedAt/executionWorkspaceIdשל Paperclip שמשחרר משאבים אחרי שריצה מסתיימת ובמקביל מאפס status
האפשרות הנכונה לסגור את הבאג היא ב-Paperclip עצמו — לתקן את issue.released שלא ידרוס status מסוף-מצב כמו done. עד שזה נסגר אצלם, אנחנו חיים עם self-recovery.
סטטוס
- לא נסגר ב-Paperclip (ידוע לפי 30/04/26)
- טופל בצד שלנו דרך self-recovery בסקייל של הסוכן (HEARTBEAT.md §4-recovery)
- לתעד עלות: כל ריצת self-recovery מוסיפה ~$0.20 לתיק
2. Bash backtick trap בעת בניית comment body דרך curl
מה קורה
הסוכן בונה pipeline מורכב כדי לפרסם comment עם markdown ארוך:
curl ... -d "$(python3 -c "
body = '''## כותרת
📁 קובץ: \`/path/to/file.md\`
'''
print(json.dumps({'body': body}))")"
ה-bash שמריץ את ה-$(...) הראשון רואה את ה-backticks ( ) בתוך המחרוזת של Python ומפרש אותם **כ-command substitution של bash**. הוא מנסה להריץ את/path/to/file.md` כפקודה, ומכיוון שהקובץ לא executable — מחזיר:
/bin/bash: line 56: /path/to/file.md: Permission denied
ההטעיה
ההודעה Permission denied היא לא באמת בעיית הרשאות:
ls -laמראה שהקובץ הואchaim:chaimעם-rw-r--r--touchידני באותו נתיב מצליח- ה-Write tool כבר כתב את הקובץ הזה בהצלחה דקה קודם
למה זה קורה דווקא בנתיבי מסמכים
Backticks הם תחביר markdown נפוץ לציטוט נתיבים: `/home/chaim/...`. בפלט markdown זה נכון, אבל כשהסוכן מטמיע את ה-markdown בתוך bash heredoc / command substitution, ה-backticks מפעילים את עצמם.
תיקון — דפוס "כתוב לקובץ זמני אז curl -d @file"
במקום:
curl ... -d "$(python3 -c "...long body with backticks...")"
עשה:
# 1. כתוב את ה-body לקובץ זמני דרך Write tool (בלי שום bash quoting)
Write("/tmp/comment.json", json.dumps({"body": markdown_body}))
# 2. אז curl קורא מהקובץ — אין shell expansion על התוכן
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/issues/{issue-id}/comments" \
-d @/tmp/comment.json
הנתיב -d @file קורא את התוכן של הקובץ בלי שום ניתוח — אין shell, אין quoting, אין backticks-as-commands. זה גם מאפשר body של 10K+ תווים ללא הגבלת ARG_MAX.
סטטוס
- תיעוד ב-HEARTBEAT.md עם הוראה מפורשת להשתמש ב-Write+
-d @fileל-bodies מעל 500 תווים - השפעה היסטורית: לפני התיקון, הריצה ב-CMPA-18 (30/04/26) הצליחה (curl באמת רץ) — אבל ה-
Permission deniedבלוג היה מבלבל וגרם לחקירה. עתה שהסיבה ידועה, אפשר להתעלם.
3. CEO main issue auto-block ב-in_progress
מה קורה
CEO שמסיים turn (פרסם comment "ממתין לסיום של סוכן Y") ומשאיר את ה-issue ב-in_progress יקבל auto-block תוך דקה אחת מ-Paperclip ("live execution disappeared"). הסטטוס יקפוץ ל-blocked ויידרש wakeup ידני להמשיך.
עקיפה
CEO צריך להעביר את ה-issue ל-in_review (לא in_progress) כשהוא ממתין למשאב חיצוני (סוכן אחר, יו"ר). זה מתועד ב-CLAUDE.md זיכרון: feedback_paperclip_enums.md.
סטטוס
- תיקון ב-
legal-ceo.md(commita1969dd) - נצפה עובד ב-CMPA-15 ב-30/04/26 — ה-CEO עבר ל-
in_reviewנכון
4. Wakeup דרך DB ישיר ≠ wakeup דרך API
מה קורה
INSERT INTO agent_wakeup_requests ידני בלי לעבור דרך POST /api/agents/{id}/wakeup יוצר רשומת wakeup אבל לא יוצר heartbeat_run. בלי heartbeat_run, ה-runtime של Paperclip לא מזהה שיש משהו להריץ → הסוכן לעולם לא מתעורר.
עקיפה
תמיד להשתמש ב-API. כל הסקייל שלנו תועדו עם האזהרה הזאת.
סטטוס
- תיקון בכל הסקייל (CLAUDE.md זיכרון:
reference_paperclip_wakeup.md)
5. מחיקת npx cache → crash-loop בהפעלה (השרת מנצח את הפאטצ')
מה קורה
Paperclip מופעל דרך exec npx -y paperclipai@<version> run ב-start-paperclip.sh. npx עושה reuse ל-cache שכבר חולץ (~/.npm/_npx/<hash>/node_modules/@paperclipai/server/) — הוא לא מחלץ מחדש בכל הפעלה. כל עוד ה-cache קיים, הפאטצ'ים שהוחלו עליו פעם אחת נשמרים על פני ריסטארטים.
הבעיה מתחילה כש-ה-cache נמחק (npm cache clean, prune, או ניקוי ידני) בזמן שהתהליך רץ. אז נוצרות שתי תקלות נפרדות:
- התהליך הישן ממשיך "online" אבל שבור — המודולים של node כבר טעונים בזיכרון, אז
/api/healthעדיין מחזיר 200, אבלGET /קורא אתui-dist/index.htmlמהדיסק בכל בקשה (readFileSync) →ENOENT→ HTTP 500 ({"error":"Internal server error"}). גם ה-URL הציבוריpc.nautilus...מחזיר 500. - בריסטארט נכנסים ל-crash-loop — npx מחלץ עותק טרי ולא-מתוקן. השרת מריץ
assertCloudDatabaseContract()(ראה patch §4 ב-start script) שמסרב ל-embedded PG במצב authenticated/public → קורס מיד, לפני שלולאת-הרקע (5/20/60ש') מספיקה להחיל את פאטץ' ה-bypass. כל ריסטארט מחלץ-וקורס מחדש ⇒ עשרות ריסטארטים, שום דבר לא מאזין על 3100.
ראיה אמפירית — 06/06/26
# התהליך הישן: online 5D אבל GET / נכשל
GET / 500 — ENOENT: no such file or directory,
open '.../@paperclipai/server/ui-dist/index.html'
/api/health → 200 # שורד כי לא קורא קבצים
# אחרי restart: crash-loop
pm2 describe paperclip → status: "waiting restart", restarts: 36, nothing on :3100
ERROR log → "Paperclip server failed to start.
authenticated public deployments require DATABASE_URL ...;
refusing embedded PostgreSQL fallback"
הורדת החבילה איטית (~30ש', native builds) — מה שמחמיר את ה-loop: min_uptime של PM2 קוטע את ה-npx באמצע ההורדה לפני שהוא מסיים לחלץ, כך שה-cache לעולם לא מתמלא.
ההשפעה על הצינור שלנו
Paperclip מושבת לגמרי — ה-UI לא עולה לאף משתמש, וכל סוכני Paperclip (14 הסוכנים) לא יכולים לרוץ כי הם חולקים את התהליך הזה.
תיקון — שער סינכרוני לפני הפעלת השרת
שורש הבעיה: פאטץ' ה-cloud-db-bypass חייב להיות על הדיסק לפני שהשרת רץ; לולאת-הרקע מאוחרת מדי. ב-start-paperclip.sh נוספה ensure_patched_before_run() (06/06/26) שרצה סינכרונית לפני exec:
- בודקת אם
@paperclipai/server/ui-dist/index.htmlקיים ב-cache (ראה "מלכודות בדרך" — זה הסמן הנכון, לאdist/index.js). - אם לא — מריצה
npx -y paperclipai@<version> --help. זה מאלץ את npx לחלץ את כל החבילה (כוללui-dist/) כדי להריץ את ה-CLI, שמדפיס help ויוצא לבד ב-exit 0 — לא מפעיל שרת ולא תופס את 3100 (אומת). אין תהליך-רקע, אין שרת לא-מתוקן מוקדם, ואין מה להרוג. - מחילה את כל הפאטצ'ים (כולל bypass) על ה-cache המחולץ — עם guard שלא מפיל את ה-wrapper אם patch נכשל.
- רק אז
exec npx ... run— npx עושה reuse ל-cache המתוקן והשרת עולה נקי.
לולאת-הרקע (post-exec) נשמרה כרשת-ביטחון idempotent.
אומת מקצה-לקצה (06/06/26): מחיקת ה-cache בכוונה + pm2 restart → השער חילץ אוטומטית דרך --help (~64ש'), תיקן, והשרת עלה ל-200 ב-~72ש'. מונה הריסטארטים של PM2 לא זז (אפס crash-loop).
מלכודות שהתגלו בדרך (גרסה ראשונה של הפיקס נכשלה):
- סמן חילוץ שגוי —
dist/index.jsנכתב ~שניות לפניui-dist/. שער שממתין ל-distומריץ מיד → ui-dist עדיין חסר → 500. הסמן הנכון הואui-dist/index.html(הקובץ האחרון, וגם זה שגרם ל-500 המקורי).set -e+ patch כושל — אםapply-hebrew.shרץ בלי ui-dist הוא מחזיר שגיאה, ותחתset -eה-wrapper מת → crash-loop חדש. הפתרון:apply_all_patches || echo WARNING.pkill -f "paperclipai@..."תופס את עצמו — מחרוזת הדפוס מופיעה ב-command line של ה-shell שמריץ את ה-pkill, אז הוא הורג את עצמו (exit 144). זו הסיבה שגישת spawn-run-then-pkillננטשה לטובת--helpשיוצא לבד. אם בכל זאת צריך להרוג — לפי PID (kill $PID; pkill -P $PID), לא לפי-f.
שחזור — עם הפיקס פרוס, מספיק pm2 restart paperclip וה-ensure_patched_before_run() מתאושש לבד. אם צריך לעשות זאת ידנית (fix אחר, דיבוג):
pm2 stop paperclip # לעצור loop אם קיים
export PATH=/home/chaim/.nvm/versions/node/v24.14.0/bin:$PATH
npx -y paperclipai@2026.529.0 --help >/dev/null 2>&1 # חילוץ נקי שיוצא לבד (לא מפעיל שרת)
find ~/.npm/_npx -path "*@paperclipai/server/ui-dist/index.html" -type f # לאמת חילוץ מלא
# להחיל פאטצ'ים על ה-cache, ובמיוחד ה-bypass:
bash ~/.paperclip/hermes-patches/apply-cloud-db-bypass.sh
bash ~/.paperclip/hebrew/apply-hebrew.sh
bash ~/.paperclip/hermes-patches/apply-hermes-fixes.sh
bash ~/.paperclip/hermes-patches/apply-deepseek-reaper-fix.sh
grep -q HEBREW_PATCH_BYPASS_CLOUD_DB \
~/.npm/_npx/*/node_modules/@paperclipai/server/dist/index.js && echo "BYPASS OK"
pm2 start paperclip && pm2 save # reuse ל-cache המתוקן
אל תשתמש ב-
pkill -f "paperclipai@..."/-f "@paperclipai/server"— הדפוס תופס את ה-shell של עצמך (exit 144). אם חייבים להרוג תהליך — לפי PID.
סטטוס
- תוקן ב-start script ע"י
ensure_patched_before_run()(06/06/26) — שער סינכרוני שמחלץ+מתקן לפני exec. - הערה מטעה תוקנה: ההערה הישנה בראש ה-script טענה ש-
npx runמחלץ-מחדש בכל הפעלה (לכן הסתמכו על לולאת-הרקע בלבד) — זה לא נכון, npx עושה reuse ל-cache תקין; הסכנה היא cache מחוק. - לקח כללי: כל patch שה-target שלו הוא assert בזמן-startup חייב להיות מוחל לפני
exec, לא בלולאת-רקע.
6. issue.comment.created — מזהה-ה-issue ב-entityId, לא ב-payload.issueId
מה קורה
ל-event issue.comment.created שה-host שולח לפלאגין, מזהה-ה-issue נמצא ב-event.entityId (ה"ישות הראשית" של ה-event), ולא ב-payload. ה-payload נושא דווקא:
commentId— מזהה התגובהbodySnippet— גוף קצוץ (לא הטקסט המלא)reopened/reopenedFrom— נדלקים כשהתגובה פתחה-מחדש issue ב-done
אין payload.issueId ואין payload.body מלא. מקור-אמת: @paperclipai/plugin-sdk index.d.ts — issueId: event.entityId.
ראיה אמפירית — תיק 8124-09-24, CMPA (17/06/26)
תגובת-משתמש על sub-issue של המנתח המשפטי (במצב done) לא הגיעה ל-CEO. הניתוב ב-plugin-legal-ai/src/worker.ts קרא payload.issueId (תמיד undefined) ולכן דילג בשקט:
WARN [plugin] issue.comment.created event missing issueId in payload, skipping
{ entityId: "6eb905ea-…", ← זה ה-issue id האמיתי
payload: { commentId: "31e35676-…", bodySnippet: "…", reopened: true,
reopenedFrom: "done", identifier: "CMPA-94", … } } ← אין issueId
ההשפעה על הצינור שלנו
הניתוב "תגובת-משתמש → CEO" (CLAUDE.md §"ניתוב comments דרך CEO") היה מת בשקט — כל תגובה דולגה. במקרה של תגובה על issue בבעלות ה-CEO זה "עבד" רק במקרה דרך מנגנון reopen-on-comment הנייטיב של Paperclip (פותח-מחדש done ומעיר את הסוכן-המשויך). על sub-issue של סוכן אחר, ה-wake הנייטיב כיוון לסוכן הלא-נכון וריצת-ה-CEO בתור בוטלה עם errorCode: issue_assignee_changed ("the new owner will be woken instead" — וה"בעלים" ב-done ⇒ כלום).
תיקון — בצד שלנו (PR ezer-mishpati/plugin-legal-ai#2)
ב-handler של issue.comment.created:
issueId = event.entityId(fallback ל-payload.issueIdלעמידות מול גרסאות-host).- גוף-תגובה מלא: התאמת
payload.commentIdמתוךlistComments(ה-payload נושא רק snippet); fallback ל-latest/snippet. - guard לדה-דופ: אם התגובה פתחה-מחדש issue שכבר משויך ל-CEO (
issue.assigneeAgentId === ceoAgentId && payload.reopened), ה-wake הנייטיב כבר מטפל — מדלגים כדי לא להריץ את ה-CEO פעמיים. לכל issue אחר עדיין מנתבים ל-CEO.
אימות (17/06/26)
תגובת-בדיקה על אותו sub-issue של המנתח (543f997b, done) לאחר deploy → לוג Routed user comment to CEO agent עם runId, וריצת-CEO succeeded (לא cancelled).
סטטוס
- תוקן בצד שלנו (PR #2, נפרס דרך
npm run build+pm2 restart paperclip— הפלאגין נטען מ-/home/chaim/plugin-legal-aiלפיpackage_path). - לקח כללי: ל-events של הפלאגין — מזהה-הישות-הראשית הוא תמיד
event.entityId; אל תניח ששדות נמצאים ב-payloadבלי לאמת מול ה-.d.tsשל ה-SDK או מול לוג חי. - TaskMaster:
legal-ai#149.