diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index 864192d..e0c013f 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -110,7 +110,7 @@ | `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:00–05: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) / ידני | | `legal-halacha-drain.config.cjs` | pm2/js | **תזמון חלון-לילה של `drain_halacha_queue.py`** (cron UTC `10 20,21,22,23,0,1,2,3 * * *` = superset שמכסה את 23:00–05: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 דק') · backoff ב-rate-limit (429 + parse איפוס, מגודר-טריות; `cost=0`=מנוי) · מאמת ש-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 דק') · backoff ב-rate-limit (429 + parse איפוס, מגודר-טריות; `cost=0`=מנוי) — **אך לא ממתין בעיוורון לשעה המדווחת: בכל טיק-בהמתנה מריץ `quota_available()` (בדיקת `claude -p` זעירה, `cost=0`) ומתחדש מיד כשהמכסה באמת חזרה (≤ טיק אחד), כי claude.ai משחרר לרוב מוקדם מהמדווח** · מאמת ש-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` | diff --git a/scripts/halacha_drain_supervisor.py b/scripts/halacha_drain_supervisor.py index e593910..4e78767 100644 --- a/scripts/halacha_drain_supervisor.py +++ b/scripts/halacha_drain_supervisor.py @@ -70,10 +70,48 @@ def pm2_bin(): return "pm2" +def claude_bin(): + """Resolve the claude CLI — PATH may be bare under pm2 cron, so fall back to + the known install location (same binary the drain's claude_session uses).""" + for c in ["claude", "/home/chaim/.local/bin/claude", + *glob("/home/chaim/.nvm/versions/node/*/bin/claude")]: + try: + if subprocess.run([c, "--version"], capture_output=True, + timeout=10).returncode == 0: + return c + except Exception: + continue + return "claude" + + PM2 = pm2_bin() +CLAUDE = claude_bin() _ENV = {**os.environ, "HOME": "/home/chaim"} +def quota_available() -> bool: + """Cheap live probe: is the claude.ai quota actually usable right now? + + The 429 reset time claude.ai reports is often conservative — quota frees up + earlier. Rather than trust that timestamp and wait blindly, we re-probe with + a tiny `claude -p` call and resume the moment it succeeds. Conservative on + failure: any non-zero exit, timeout, or limit message → treat as still + limited (so a flaky probe never resumes the drain into a real 429).""" + try: + r = subprocess.run([CLAUDE, "-p", "Reply with exactly: OK"], + capture_output=True, text=True, timeout=60, env=_ENV, + cwd=REPO) + except Exception: + return False + if r.returncode != 0: + return False + out = ((r.stdout or "") + (r.stderr or "")) + low = out.lower() + if "usage limit" in low or "session limit" in low or 'api_error_status":429' in out: + return False + return "OK" in out + + # ── DB access (via the repo venv; the module self-configures) ──────────────── def _venv_py(code: str, timeout: int = 120) -> str: r = subprocess.run([VENV_PY, "-c", code], capture_output=True, text=True, @@ -319,8 +357,20 @@ def tick(): cd_dt = datetime.fromisoformat(prev["cooldown_until"]) except Exception: cd_dt = None - cooldown_until = cd_dt.isoformat() if cd_dt else None in_cooldown = bool(cd_dt and now < cd_dt) + # Don't trust the reported reset time — re-probe. claude.ai usually frees up + # quota EARLIER than the 429 message claims, and the old code then sat idle + # until that (conservative) timestamp. When we'd otherwise hold, a tiny live + # probe lets us resume the instant quota is actually back (≤ one tick), no + # manual kick. Runs at most once per tick and only while we think we're + # limited, so the cost is negligible. + if in_cooldown and quota_available(): + notes.append( + f"בדיקת-מכסה הצליחה — המכסה חזרה לפני האיפוס המדווח " + f"({cd_dt.astimezone(IDT):%H:%M IDT}); מתחדש מיד.") + cd_dt = None + in_cooldown = False + cooldown_until = cd_dt.isoformat() if cd_dt else None weekly = bool(cd_dt and (cd_dt - now) > timedelta(hours=WEEKLY_GAP_HOURS)) # progress-based liveness (chunk checkpoints, NOT log mtime)