feat(halacha): עצירה-רכה של הדריינר בסף-ניצול + מקור-אמת יחיד למכסה
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
Lint — undefined names / undefined-names (pull_request) Successful in 11s

בהגיע סף-רך (5-שעות ≥75% / שבועי ≥65%) הדריינר מסיים את התיק שרץ ועוצר
לבד בין תיקים (===STOP===) — במקום שהסופרוויזר יקטול אותו ב-pm2 stop באמצע
חילוץ. ב-75% יש מכסה לסיים את התיק; הקטילה נשמרת רק ל-429 טרי אמיתי.

- חדש legal_mcp/services/usage_limits.py (stdlib-only): מקור-אמת יחיד —
  subscription_usage / USAGE_CEILINGS / ceiling_status. מיובא גם מ-system-python
  (supervisor) וגם מה-venv (drain). __init__ ריקים → import בטוח מחוץ ל-venv.
- supervisor: מייבא מהמודול (הסרת ההעתקים המקומיים, ~50 שורות פחות);
  quota_exhausted/quota_available הפכו wrappers דקים; ענף cooldown — קטילה
  (hold-stopped) רק אם log_rl (429 טרי), אחרת hold-soft בלי pm2 stop.
- drain: limit=4→1 (בדיקת-סף בין כל תיק); שער-סף ב-run_in_executor, fail-OPEN
  כש-endpoint None (הסופרוויזר מגבה ב-429-kill); שמירת קצב 30ש' בין תיקים
  (pl.INTER_PRECEDENT_COOLDOWN_SEC — limit=1 ביטל את המרווח הפנימי-לסבב).
- SCRIPTS.md עודכן (limit=1, שער-סף, hold-soft, מקור-אמת משותף).

אומת end-to-end (endpoint חי): (1) drain עם סף מורד → ===STOP=== usage ceiling
בלי לעבד תיק; (2) supervisor status=online+סף-רך → action=hold-soft, stop_drain
נקרא 0; (3) 429 טרי → hold-stopped, stop_drain נקרא 1. py_compile עובר.

court_fetch_service/usage_status (העתק שלישי, async/aiohttp, רגיש-דיפלוי) נדחה
במכוון לאיחוד-עתידי — לא נוגעים בגשר-המארח כאן.

Invariants: G1 (נרמול-במקור — endpoint יחיד), G2 (אין מסלול-בקרה מקביל — מודול
משותף יחיד, drain+supervisor קוראים אותו דבר), X16 (עמידות — עצירה לפני 429
מונעת חילוץ-מחדש משחית). G12 לא רלוונטי.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 04:11:09 +00:00
parent 7043de0ac2
commit 4ac661b0a6
4 changed files with 174 additions and 88 deletions

View File

@@ -114,9 +114,9 @@
| `run_curator_deepseek_test_v2.sh` | A/B test #2 (2026-05-05) — אותו run אבל עם interaction. תוצאה: 9:08 דק׳, 5 ממצאים, היחיד מ-4 הריצות שזיהה תוצאה עובדתית נכונה (קבלה חלקית). interaction נכשל ב-API ("Agent run id required" בריצה ידנית). | החלפת Curator לאדפטר DeepSeek מקומי |
| `run_curator_sonnet_rerun.sh` | A/B test #3 (2026-05-05) — ריצה חוזרת של Sonnet 4.5 על אותו CMP-78. תוצאה: 12:52 דק׳ (לעומת 20:13 בריצה המקורית — כי בלי לולאת interaction.json). זיהה תוצאה שגויה ("דחייה") **בעקביות עם הריצה המקורית** — Sonnet עקבי-בטעות, DeepSeek אקראי. | בדיקה חד-פעמית — לא להריץ שוב |
| `ingest_incoming_batch.py` | python | קליטת batch של החלטות ועדת ערר מ-`data/precedents/incoming/` דרך המסלול הקנוני (`ingest_internal_decision`) + חילוץ מטא-דאטה לכל תיק (המסלול הפנימי לא מתזמן metadata — INV-ING3). רצף (לא מקבילי, להימנע מעומס CLI). רשימת `DECISIONS` נערכת ידנית לכל batch. config מ-`~/.env`. תומך תהליך [[project_precedent_incoming_workflow]]. | ידני, per-batch (חלופה ל-MCP `internal_decision_upload` כש-batch גדול) |
| `drain_halacha_queue.py` | python | ריקון תור חילוץ ההלכות (`process_pending_extractions kind='halacha'`) ב-batches של 4 עד שהתור ריק (2 סבבים ריקים). **רץ רק בחלון-לילה 23:0005:00 שעון ישראל** (`_in_window`, zoneinfo DST-safe — המכונה UTC); מחוץ לחלון `===SKIP===`, ונעצר `===STOP===` כשהחלון נסגר (השאר ממשיך בלילה הבא, FIFO+checkpoint). env: `HALACHA_DRAIN_WINDOW_START`/`_END`/`HALACHA_DRAIN_TZ`. **kill-switch `/operations`:** בודק `is_drain_disabled` בעלייה **וגם בתחילת כל סבב** — כיבוי באמצע-ריצה עוצר את הלולאה בגבול-הסבב הבא (התהליך עצמו נהרג מיד דרך ה-UI-toggle/סופרוייזר). חילוץ-הלכות נשאר על claude_session (לא Gemini). self-heal ל-orphaned `processing`. ההלכות נוחתות `pending_review` (שער-יו"ר). **חילוץ תיק-בודד שהיו"ר מבקש רץ מיד דרך ה-CEO (`precedent_extract_halachot`) ואינו מגודר כאן.** | דרך `legal-halacha-drain.config.cjs` (pm2 cron) / ידני |
| `drain_halacha_queue.py` | python | ריקון תור חילוץ ההלכות (`process_pending_extractions kind='halacha'`) **תיק-בודד לסבב (`limit=1`)** עד שהתור ריק (2 סבבים ריקים), עם קצב `pl.INTER_PRECEDENT_COOLDOWN_SEC` בין תיקים (נשמר אחרי המעבר ל-limit=1 שביטל את המרווח הפנימי-לסבב). **רץ רק בחלון-לילה 23:0005:00 שעון ישראל** (`_in_window`, zoneinfo DST-safe — המכונה UTC); מחוץ לחלון `===SKIP===`, ונעצר `===STOP===` כשהחלון נסגר (השאר ממשיך בלילה הבא, FIFO+checkpoint). env: `HALACHA_DRAIN_WINDOW_START`/`_END`/`HALACHA_DRAIN_TZ`. **שער-סף-ניצול (נוהל-יו"ר):** בין כל תיק בודק את ניצול-claude.ai דרך מקור-האמת המשותף `legal_mcp.services.usage_limits` (`USAGE_CEILINGS` — אותו שה-supervisor קורא) ועוצר `===STOP=== usage ceiling reached` כשנחצה סף-רך (5-שעות ≥75% / שבועי ≥65%, env `HALACHA_DRAIN_CEILING_5H`/`_WEEKLY`). הבדיקה **בין** תיקים → התיק שרץ תמיד מסתיים על המכסה שנותרה ופשוט לא מתחיל חדש — עצירה **לפני** 429, לא קטילה. fail-OPEN אם ה-endpoint לא-זמין (ה-supervisor מגבה בקטילת-429). **kill-switch `/operations`:** בודק `is_drain_disabled` בעלייה **וגם בתחילת כל סבב** — כיבוי באמצע-ריצה עוצר את הלולאה בגבול-הסבב הבא (התהליך עצמו נהרג מיד דרך ה-UI-toggle/סופרוייזר). חילוץ-הלכות נשאר על claude_session (לא Gemini). self-heal ל-orphaned `processing`. ההלכות נוחתות `pending_review` (שער-יו"ר). **חילוץ תיק-בודד שהיו"ר מבקש רץ מיד דרך ה-CEO (`precedent_extract_halachot`) ואינו מגודר כאן.** | דרך `legal-halacha-drain.config.cjs` (pm2 cron) / ידני |
| `legal-halacha-drain.config.cjs` | pm2/js | **תזמון חלון-לילה של `drain_halacha_queue.py`** (cron UTC `10 20,21,22,23,0,1,2,3 * * *` = superset שמכסה את 23:0005:00 ישראל בקיץ ובחורף; הסקריפט גוזם לחלון המדויק ב-zoneinfo). דקת-הצתה `:10` (לא `:00`) כדי לא לחלוק דקה עם metadata-drain (`:00`) או supervisor (`:05`) — מונע deadlock של DDL-המיגרציה כששני דריינים עולים יחד. `HALACHA_DRAIN_CRON` לעקיפה. ירייה כל שעה גם מחדשת one-shot שמת באמצע (advisory-lock הופך חפיפה לבטוחה). דורש claude CLI. התקנה: `pm2 start scripts/legal-halacha-drain.config.cjs && pm2 save`. | pm2 cron (host-side) |
| `halacha_drain_supervisor.py` | python | **מנהל-בריאות קבוע ל-`legal-halacha-drain`** (אפס צריכת-Claude — קורא DB/לוגים/pm2 ומצית את הדריינר הקיים). טיק יחיד: **מכבד `is_drain_disabled` בעדיפות עליונה — אם כבוי ב-/operations עוצר את הדריינר ולא מצית** · מצית כשבטל+תור≠ריק · restart ל-run תקוע (liveness לפי checkpoints-per-chunk, **לא** mtime-לוג שמתעדכן רק בסיום תיק ~10 דק') · **זיהוי rate-limit: ה-PRIMARY הוא `quota_exhausted()` הסמכותי (קורא `subscription_usage()` — endpoint לא-מתועד `GET /api/oauth/usage`, token מ-`~/.claude/.credentials.json` + UA `claude-code/*` חובה אחרת 429 — אותו אחוז-ניצול 5-שעות/שבועי/שבועי-Sonnet שה-UI מציג. **ספי-עצירה-רכים `USAGE_CEILINGS` (נוהל-יו"ר 2026-06-15): עוצר לפני 429 — 5-שעות ≥75%, שבועי+שבועי-Sonnet ≥65% (עקיפה: `HALACHA_DRAIN_CEILING_5H`/`HALACHA_DRAIN_CEILING_WEEKLY`); הגעה לסף = בדיוק כמו מיצוי, cooldown עד `resets_at` של אותו חלון. הנימוק: 429 באמצע-תיק כופה חילוץ-מחדש של תיק שהושלם ומשחית אותו**), durable ואינו תלוי בעומק-זנב-הלוג; `scan_rate_limit` (429 + parse איפוס, מגודר-טריות) רק כ-fallback כש-endpoint לא-זמין — תיקון הבאג שבו 429 שגלל מתחת ל-churn של ה-restart נקרא כ"hung" וגרר restart-storm שבזבז מכסה.** בזמן מוגבל **עוצר את הדריינר** (`hold-stopped`) כדי לא להלום 429, ומצית-מחדש כשהמכסה חוזרת (כל החלונות חזרו מתחת לסף; fallback `claude -p` זעיר אם ה-endpoint לא זמין). `status` מדפיס ניצול/סף-עצירה · **חלון catch-up בוקר `[05:0007:00 IDT)` (`CATCHUP_END`) שנפתח רק לניקוי backlog שנותר כשהמכסה חזרה — איפוס-5-שעות מאוחר נוחת לרוב מעט אחרי 05:00; מגודר ב-(לא-מוגבל)+(תור≠ריק), והקצה המורחב מועבר לדריינר כך ש-window-self-guard שלו מקבל את סבבי-ה-catch-up** · מאמת ש-staging מתחייב. **BURST** (חלון "רוץ ברצף עכשיו" ידני): מקור-אמת = `drain_controls.burst_until` ב-DB — אותו ערך ש-/operations קורא/כותב (G1 מקור-יחיד, G2 בלי מסלול מקביל); בעתיד→חלון מורם, אחרת חלון-לילה 23-05; פג-תוקף אוטומטי במועד. תת-פקודות: `tick` (ברירת-מחדל), `burst-on [--until]`, `burst-off`, `status`. | דרך `legal-halacha-supervisor.config.cjs` (pm2 cron) / ידני / כפתור /operations |
| `halacha_drain_supervisor.py` | python | **מנהל-בריאות קבוע ל-`legal-halacha-drain`** (אפס צריכת-Claude — קורא DB/לוגים/pm2 ומצית את הדריינר הקיים). טיק יחיד: **מכבד `is_drain_disabled` בעדיפות עליונה — אם כבוי ב-/operations עוצר את הדריינר ולא מצית** · מצית כשבטל+תור≠ריק · restart ל-run תקוע (liveness לפי checkpoints-per-chunk, **לא** mtime-לוג שמתעדכן רק בסיום תיק ~10 דק') · **זיהוי rate-limit: ה-PRIMARY הוא `quota_exhausted()` הסמכותי (wrapper דק מעל מקור-האמת המשותף `legal_mcp.services.usage_limits` — `subscription_usage()`+`ceiling_status()`+`USAGE_CEILINGS`, אותו שהדריינר קורא, G1/G2 בלי כפילות; endpoint לא-מתועד `GET /api/oauth/usage`, token מ-`~/.claude/.credentials.json` + UA `claude-code/*` חובה אחרת 429 — אותו אחוז-ניצול 5-שעות/שבועי/שבועי-Sonnet שה-UI מציג. **ספי-עצירה-רכים `USAGE_CEILINGS` (נוהל-יו"ר 2026-06-15): עוצר לפני 429 — 5-שעות ≥75%, שבועי+שבועי-Sonnet ≥65% (עקיפה: `HALACHA_DRAIN_CEILING_5H`/`HALACHA_DRAIN_CEILING_WEEKLY`); הגעה לסף = בדיוק כמו מיצוי, cooldown עד `resets_at` של אותו חלון. הנימוק: 429 באמצע-תיק כופה חילוץ-מחדש של תיק שהושלם ומשחית אותו**), durable ואינו תלוי בעומק-זנב-הלוג; `scan_rate_limit` (429 + parse איפוס, מגודר-טריות) רק כ-fallback כש-endpoint לא-זמין — תיקון הבאג שבו 429 שגלל מתחת ל-churn של ה-restart נקרא כ"hung" וגרר restart-storm שבזבז מכסה.** **שתי תגובות ל-cooldown:** בסף-רך (≥75/65, **בלי** 429 טרי) — `hold-soft`: **אינו קוטל**, הדריינר קורא את אותו סף ועוצר לבד בין תיקים אחרי שמסיים את התיק הנוכחי (יש מכסה לסיים); קטילה (`hold-stopped`, `pm2 stop`) רק ב-**429 טרי אמיתי** (`log_rl` — הקריאות באמת נכשלות). מצית-מחדש כשהמכסה חוזרת (כל החלונות מתחת לסף; fallback `claude -p` זעיר אם ה-endpoint לא זמין). `status` מדפיס ניצול/סף-עצירה · **חלון catch-up בוקר `[05:0007:00 IDT)` (`CATCHUP_END`) שנפתח רק לניקוי backlog שנותר כשהמכסה חזרה — איפוס-5-שעות מאוחר נוחת לרוב מעט אחרי 05:00; מגודר ב-(לא-מוגבל)+(תור≠ריק), והקצה המורחב מועבר לדריינר כך ש-window-self-guard שלו מקבל את סבבי-ה-catch-up** · מאמת ש-staging מתחייב. **BURST** (חלון "רוץ ברצף עכשיו" ידני): מקור-אמת = `drain_controls.burst_until` ב-DB — אותו ערך ש-/operations קורא/כותב (G1 מקור-יחיד, G2 בלי מסלול מקביל); בעתיד→חלון מורם, אחרת חלון-לילה 23-05; פג-תוקף אוטומטי במועד. תת-פקודות: `tick` (ברירת-מחדל), `burst-on [--until]`, `burst-off`, `status`. | דרך `legal-halacha-supervisor.config.cjs` (pm2 cron) / ידני / כפתור /operations |
| `legal-halacha-supervisor.config.cjs` | pm2/js | **תזמון כל 15 דק' של `halacha_drain_supervisor.py`** (cron `5-59/15 * * * *` = `:05,:20,:35,:50`, `HALACHA_SUPERVISOR_CRON` לעקיפה; דקת-הצתה `:05` כדי לא לחלוק דקה עם metadata-drain `:00` או halacha-drain `:10` — מונע deadlock של DDL-המיגרציה). `autorestart:false` (one-shot per tick). מצב-state ב-`~/halacha-drain-monitor/` (מחוץ ל-repo). התקנה: `pm2 start scripts/legal-halacha-supervisor.config.cjs && pm2 save`. | pm2 cron (host-side) |
| `ingest_digests_batch.py` | python | קליטת batch של יומוני "כל יום" מ-`data/digests/incoming/` דרך המסלול העצמאי של קורפוס-הגילוי (`digest_library.ingest_digest`) — חילוץ-LLM (תג-מושג, כותרת-הלכה, מראה-מקום, שני-תאריכים), embedding יחיד, ו-autolink לפסק המקורי (X12/INV-DIG3). רצף (לא מקבילי). מזהה-יומון+תאריך נגזרים משם-הקובץ; העלון החודשי מדולג. **לא מעביר קבצים** — ה-DB (content_hash) הוא מקור-האמת היחיד; הרצה חוזרת מדלגת על קיימים (`exists`). config מ-`~/.env`. | ידני, per-batch (חלופה ל-MCP `digest_upload`) |
| `drain_digests.py` | python | ריקון תור ההעשרה של יומונים (X12): מעבד כל digest בסטטוס `pending` דרך `digest_library.enrich_digest` (חילוץ-LLM Sonnet + embedding + autolink). מקבילי (CONCURRENCY=3, env-tunable), idempotent. מוסיף `~/.local/bin` ל-PATH כדי שה-claude CLI יימצא תחת cron. בודק דגל `drain_controls('legal-digest-drain')` ב-startup → no-op כשכבוי מ-/operations. | דרך `legal-digest-drain.config.cjs` (pm2 cron) + ידני אחרי backfill. חלופת-MCP: `digest_process_pending` |