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>
278 lines
18 KiB
Markdown
278 lines
18 KiB
Markdown
# Paperclip Quirks — מלכודות ידועות
|
||
|
||
> **הקשר:** מה ש-Paperclip עושה בעצמו, מתחת לרגליהם של הסוכנים שלנו, ושאנחנו צריכים לעקוף אותו או לחיות איתו.
|
||
>
|
||
> כל מלכודת מתועדת עם:
|
||
> 1. מה קורה בפועל
|
||
> 2. ראיה אמפירית מתוך לוגים
|
||
> 3. ההשפעה על הצינור שלנו
|
||
> 4. עקיפה / תיקון / קבלה
|
||
|
||
---
|
||
|
||
## 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` עם תוצרים קיימים:
|
||
|
||
1. בודק שהקבצים הצפויים קיימים (`Glob /documents/research/*.md`)
|
||
2. בודק שה-DB מאוכלס (`mcp__legal-ai__precedent_list`, `get_claims`, וכו')
|
||
3. אם הכל קיים → לא מבצע עבודה כפולה → כותב 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 ארוך:
|
||
|
||
```bash
|
||
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"
|
||
|
||
במקום:
|
||
```bash
|
||
curl ... -d "$(python3 -c "...long body with backticks...")"
|
||
```
|
||
|
||
עשה:
|
||
```python
|
||
# 1. כתוב את ה-body לקובץ זמני דרך Write tool (בלי שום bash quoting)
|
||
Write("/tmp/comment.json", json.dumps({"body": markdown_body}))
|
||
```
|
||
```bash
|
||
# 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`** (commit a1969dd)
|
||
- נצפה עובד ב-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](../../.paperclip/scripts/start-paperclip.sh). npx **עושה reuse** ל-cache שכבר חולץ (`~/.npm/_npx/<hash>/node_modules/@paperclipai/server/`) — הוא **לא** מחלץ מחדש בכל הפעלה. כל עוד ה-cache קיים, הפאטצ'ים שהוחלו עליו פעם אחת נשמרים על פני ריסטארטים.
|
||
|
||
הבעיה מתחילה כש-ה-cache **נמחק** (`npm cache clean`, prune, או ניקוי ידני) בזמן שהתהליך רץ. אז נוצרות שתי תקלות נפרדות:
|
||
|
||
1. **התהליך הישן ממשיך "online" אבל שבור** — המודולים של node כבר טעונים בזיכרון, אז `/api/health` עדיין מחזיר 200, אבל `GET /` קורא את `ui-dist/index.html` **מהדיסק בכל בקשה** (`readFileSync`) → `ENOENT` → **HTTP 500** (`{"error":"Internal server error"}`). גם ה-URL הציבורי `pc.nautilus...` מחזיר 500.
|
||
2. **בריסטארט נכנסים ל-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](../../.paperclip/scripts/start-paperclip.sh) נוספה `ensure_patched_before_run()` (06/06/26) שרצה סינכרונית לפני `exec`:
|
||
|
||
1. בודקת אם `@paperclipai/server/ui-dist/index.html` קיים ב-cache (ראה "מלכודות בדרך" — זה הסמן הנכון, לא `dist/index.js`).
|
||
2. אם לא — מריצה `npx -y paperclipai@<version> --help`. זה מאלץ את npx **לחלץ את כל החבילה** (כולל `ui-dist/`) כדי להריץ את ה-CLI, שמדפיס help ו**יוצא לבד ב-exit 0** — **לא** מפעיל שרת ולא תופס את 3100 (אומת). אין תהליך-רקע, אין שרת לא-מתוקן מוקדם, ואין מה להרוג.
|
||
3. מחילה את **כל** הפאטצ'ים (כולל bypass) על ה-cache המחולץ — עם guard שלא מפיל את ה-wrapper אם patch נכשל.
|
||
4. רק אז `exec npx ... run` — npx עושה reuse ל-cache המתוקן והשרת עולה נקי.
|
||
|
||
לולאת-הרקע (post-exec) נשמרה כרשת-ביטחון idempotent.
|
||
|
||
**אומת מקצה-לקצה (06/06/26):** מחיקת ה-cache בכוונה + `pm2 restart` → השער חילץ אוטומטית דרך `--help` (~64ש'), תיקן, והשרת עלה ל-200 ב-~72ש'. מונה הריסטארטים של PM2 **לא זז** (אפס crash-loop).
|
||
|
||
> **מלכודות שהתגלו בדרך (גרסה ראשונה של הפיקס נכשלה):**
|
||
> 1. **סמן חילוץ שגוי** — `dist/index.js` נכתב ~שניות **לפני** `ui-dist/`. שער שממתין ל-`dist` ומריץ מיד → ui-dist עדיין חסר → 500. הסמן הנכון הוא `ui-dist/index.html` (הקובץ האחרון, וגם זה שגרם ל-500 המקורי).
|
||
> 2. **`set -e` + patch כושל** — אם `apply-hebrew.sh` רץ בלי ui-dist הוא מחזיר שגיאה, ותחת `set -e` ה-wrapper מת → crash-loop חדש. הפתרון: `apply_all_patches || echo WARNING`.
|
||
> 3. **`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 אחר, דיבוג):
|
||
```bash
|
||
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`:
|
||
1. `issueId = event.entityId` (fallback ל-`payload.issueId` לעמידות מול גרסאות-host).
|
||
2. גוף-תגובה מלא: התאמת `payload.commentId` מתוך `listComments` (ה-payload נושא רק snippet); fallback ל-latest/snippet.
|
||
3. **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.
|