From 4fa62db192d7ea043641a81f9eda1a17f187c278 Mon Sep 17 00:00:00 2001 From: Chaim Date: Thu, 11 Jun 2026 14:02:38 +0000 Subject: [PATCH] =?UTF-8?q?feat(halacha):=20drain=20=D7=9C=D7=99=D7=9C?= =?UTF-8?q?=D7=99=20(23:00=E2=80=9305:00)=20+=20per-upload=20=D7=97=D7=99?= =?UTF-8?q?=D7=9C=D7=95=D7=A5=20=D7=AA=D7=99=D7=A7-=D7=91=D7=95=D7=93?= =?UTF-8?q?=D7=93=20=D7=93=D7=A8=D7=9A=20=D7=94-CEO=20(#120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit מפריד בין ריקון-באקלוג המוני לבין חילוץ per-upload, ומסיר את ה"פקק" שגרם timeout/process_lost ב-heartbeat של ה-CEO. הבעיה (אבחנה 2026-06-11): לחיצת "חלץ הלכות" על תיק בודד יצרה issue (CMP-165) שהורה ל-CEO להריץ precedent_process_pending(halacha) — בולען סינכרוני שמרוקן את כל התור ההיסטורי (147 ממתינים, שעות) בתוך heartbeat שחסום לשעה. תוצאה: timeout כל שעה → process_lost בפירוק קבוצת-התהליכים → retry → סטורם, והתיק הבודד (FIFO אחרון) לא טופל. לא OOM, לא קוד שבור — אי-התאמה ארכיטקטונית. התיקון: 1. per-upload (web/paperclip_client.py, wake_for_precedent_extraction): גוף ה-issue + תיאור-הפרויקט מורים כעת להריץ precedent_extract_metadata + precedent_extract_halachot ל-case_law_id של ה-issue **בלבד** — עם אזהרה מפורשת לא להריץ process_pending. reextract_halachot כבר מנקה requested_at ומסמן completed → התיק לא יחזור לתור הלילי. 2. הוראות ה-CEO (.claude/agents/legal-ceo.md): אותו שינוי — חילוץ תיק-בודד, לא ריקון-תור. (צריך sync_agents_across_companies.py --apply אחרי מיזוג.) 3. ריקון-באקלוג (scripts/drain_halacha_queue.py): שער חלון-לילה 23:00–05:00 שעון ישראל (zoneinfo, DST-safe — המכונה UTC). מחוץ לחלון ===SKIP===; נעצר ===STOP=== כשהחלון נסגר, השאר ממשיך בלילה הבא (FIFO + per-chunk checkpoint). env: HALACHA_DRAIN_WINDOW_START/_END/_TZ. 4. cron (scripts/legal-halacha-drain.config.cjs): UTC band 20:00–03:00 שמכסה את חלון-ישראל בשני מצבי-DST; הסקריפט גוזם לחלון המדויק. ירייה שעתית מחדשת one-shot שמת (advisory-lock → חפיפה בטוחה). רשת-ביטחון: request_halacha_extraction עדיין מסמן requested_at, כך שאם wakeup ל-CEO נכשל — הדריינר הלילי יתפוס את התיק (בלילה, חסום), אך שום נתיב יומי לא מרוקן את כל התור. Invariants: מקיים G12/INV-PORT1 (paperclip_client = shell; leak_guard עובר). נוגע X16 (durability — מתקציב-זמן heartbeat ל-job ייעודי). בדיקות: py_compile ✓ · window-logic + zoneinfo ✓ (17:00 IDT→False) · leak_guard ✓. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/agents/legal-ceo.md | 17 ++++++----- scripts/SCRIPTS.md | 4 +-- scripts/drain_halacha_queue.py | 39 ++++++++++++++++++++++++++ scripts/legal-halacha-drain.config.cjs | 25 +++++++++++++---- web/paperclip_client.py | 31 ++++++++++++-------- 5 files changed, 91 insertions(+), 25 deletions(-) diff --git a/.claude/agents/legal-ceo.md b/.claude/agents/legal-ceo.md index eb5a47b..9b175bd 100644 --- a/.claude/agents/legal-ceo.md +++ b/.claude/agents/legal-ceo.md @@ -241,12 +241,15 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru **מה לעשות:** 1. קרא את ה-description של ה-issue — מצוין שם `case_law_id` וה-citation. 2. **warmup**: קרא קודם `mcp__legal-ai__workflow_status(case_number="warmup")` (כלי קל שמאלץ MCP להתחבר). אם נכשל ב-"No such tool available" → `Bash sleep 5` ואז retry. רק אחרי שזה עובד, המשך: -3. הרץ פעמיים: +3. חלץ את **הפסיקה הזו בלבד** (לפי ה-`case_law_id` שב-description) — הרץ פעמיים: ``` - mcp__legal-ai__precedent_process_pending(kind="metadata") - mcp__legal-ai__precedent_process_pending(kind="halacha") + mcp__legal-ai__precedent_extract_metadata(case_law_id="") + mcp__legal-ai__precedent_extract_halachot(case_law_id="") ``` - הכלי מעבד את **כל** הפסיקות שבתור — אם תוקיע אחת והגיעו עוד בינתיים, גם הן יעובדו. + ⚠️ **אל תריץ** `precedent_process_pending` — הוא מרוקן את **כל** התור ההיסטורי + (מאות פסיקות, שעות עבודה), חורג מתקציב-הזמן של ה-heartbeat וגורם + timeout/process_lost. ריקון-הבאקלוג רץ בנפרד כשירות-לילה ייעודי + (`legal-halacha-drain`, 23:00–05:00) — לא דרכך. כאן: רק התיק של ה-issue. 4. **תיקוף-ציטוטים (X11, אחרי חילוץ ההלכות):** הרץ ``` mcp__legal-ai__corroboration_rebuild() @@ -257,9 +260,9 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru (reviewer `corroborated …`); הלכה שמאוחר-יותר **בוטלה** (overruled) → חוזרת לשער-היו"ר. הוא idempotent ולא נוגע במצבים סופיים (`published`/`rejected`). אם הכלי לא קיים → ה-MCP server לא עלה מחדש מאז Phase 2; דלג ודווח (אל תיכשל על זה). -5. כשמסתיים: כתוב comment קצר ב-issue (`precedent_process_pending` + `corroboration_rebuild` - מחזירים את התוצאות — סכם בעברית: כמה הלכות חולצו, אילו שדות מטא-דאטה הושלמו, status לכל פסיקה, - וכמה הלכות אושרו/הודחו בתיקוף-ציטוטים — `{approved, demoted}`). +5. כשמסתיים: כתוב comment קצר ב-issue (`precedent_extract_metadata`/`precedent_extract_halachot` + + `corroboration_rebuild` מחזירים את התוצאות — סכם בעברית: כמה הלכות חולצו, אילו שדות מטא-דאטה + הושלמו, status הפסיקה, וכמה הלכות אושרו/הודחו בתיקוף-ציטוטים — `{approved, demoted}`). 6. סמן את ה-issue כ-`done`. **אל**: אל תיצור issues של ביצוע בתיקי ערר, אל תיכנס לתהליך כתיבת החלטה — זו רק עבודת תחזוקה של ספריית הפסיקה. diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index 94a53df..3131a25 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -101,8 +101,8 @@ | `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 סבבים ריקים). חילוץ-הלכות נשאר על claude_session (לא Gemini). self-heal ל-orphaned `processing`. ההלכות נוחתות `pending_review` (שער-יו"ר). | דרך `legal-halacha-drain.config.cjs` (pm2 cron) / ידני | -| `legal-halacha-drain.config.cjs` | pm2/js | **תזמון כל שעתיים של `drain_halacha_queue.py`** (cron `47 */2 * * *`, `HALACHA_DRAIN_CRON` לעקיפה) — מונע סתימה של תור חילוץ-ההלכות. קצב שמרני (Claude איטי + כל ריצה מוסיפה לתור-אישור-היו"ר). דורש claude CLI. התקנה: `pm2 start scripts/legal-halacha-drain.config.cjs && pm2 save`. | pm2 cron (host-side) | +| `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`. חילוץ-הלכות נשאר על 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 `0 20,21,22,23,0,1,2,3 * * *` = superset שמכסה את 23:00–05:00 ישראל בקיץ ובחורף; הסקריפט גוזם לחלון המדויק ב-zoneinfo). `HALACHA_DRAIN_CRON` לעקיפה. ירייה כל שעה גם מחדשת one-shot שמת באמצע (advisory-lock הופך חפיפה לבטוחה). דורש claude CLI. התקנה: `pm2 start scripts/legal-halacha-drain.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` | | `legal-digest-drain.config.cjs` | pm2/js | **תזמון כל שעתיים של `drain_digests.py`** (cron `0 */2 * * *`, `DIGEST_DRAIN_CRON` לעקיפה) — הועבר מ-crontab של המערכת ל-pm2 כדי שיופיע ויהיה שליט בדף `/operations` (הרץ-עכשיו/הפעל/כבה). `autorestart:false` (one-shot per tick). דורש claude CLI + `VOYAGE_API_KEY`. התקנה: `pm2 start scripts/legal-digest-drain.config.cjs && pm2 save`. | pm2 cron (host-side) | diff --git a/scripts/drain_halacha_queue.py b/scripts/drain_halacha_queue.py index 334f45f..690ff55 100644 --- a/scripts/drain_halacha_queue.py +++ b/scripts/drain_halacha_queue.py @@ -4,18 +4,48 @@ Calls the canonical process_pending_extractions(kind='halacha') in small batches until the queue is empty (two consecutive zero-progress rounds). Serial + global advisory-lock coordinated inside the service — avoids concurrent Claude load spikes. +NIGHT-WINDOW: halacha extraction is slow (Opus, ~10 min/case) and token-heavy, so +the backlog drain runs ONLY in an off-hours window (default 23:00–05:00 Israel +time) — it never competes with daytime interactive work or other agents. A tick +that starts at 23:00 keeps going until the queue empties OR the window closes +(checked before every round); whatever's left resumes the next night (FIFO + +per-chunk checkpoint → no lost or duplicated work). Single-case extraction +requested by the chair goes through the CEO immediately and is NOT gated here. +Window is DST-safe (zoneinfo) — the host runs in UTC. Env overrides: +HALACHA_DRAIN_WINDOW_START / _END (hours, 0–23) · HALACHA_DRAIN_TZ. + Run: mcp-server/.venv/bin/python scripts/drain_halacha_queue.py """ import asyncio import os import sys +from datetime import datetime +from zoneinfo import ZoneInfo sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "mcp-server", "src")) from legal_mcp.services import db from legal_mcp.services import precedent_library as pl +_TZ = ZoneInfo(os.environ.get("HALACHA_DRAIN_TZ", "Asia/Jerusalem")) +_WINDOW_START = int(os.environ.get("HALACHA_DRAIN_WINDOW_START", "23")) +_WINDOW_END = int(os.environ.get("HALACHA_DRAIN_WINDOW_END", "5")) + + +def _in_window() -> bool: + """True iff the current Israel-time hour is inside [START, END). + + Handles the midnight wrap (e.g. 23→5): the window is the union of + [START,24) and [0,END). If START == END the window is treated as 'always'. + """ + if _WINDOW_START == _WINDOW_END: + return True + hour = datetime.now(_TZ).hour + if _WINDOW_START < _WINDOW_END: # same-day window, e.g. 1→5 + return _WINDOW_START <= hour < _WINDOW_END + return hour >= _WINDOW_START or hour < _WINDOW_END # wraps midnight, e.g. 23→5 + async def main(): # /operations "disable" switch — no-op immediately if turned off (pm2 @@ -23,10 +53,19 @@ async def main(): if await db.is_drain_disabled("legal-halacha-drain"): print("===SKIP=== legal-halacha-drain disabled via /operations", flush=True) return + if not _in_window(): + now = datetime.now(_TZ).strftime("%H:%M %Z") + print(f"===SKIP=== outside drain window {_WINDOW_START:02d}:00–" + f"{_WINDOW_END:02d}:00 (now {now})", flush=True) + return total = 0 empty_rounds = 0 rnd = 0 while empty_rounds < 2: + if not _in_window(): + print(f"===STOP=== drain window closed ({_WINDOW_END:02d}:00) — " + f"{total} cases this run; rest resumes next night", flush=True) + break rnd += 1 out = await pl.process_pending_extractions(kind="halacha", limit=4) processed = out.get("processed", 0) diff --git a/scripts/legal-halacha-drain.config.cjs b/scripts/legal-halacha-drain.config.cjs index b228aac..0ca85ce 100644 --- a/scripts/legal-halacha-drain.config.cjs +++ b/scripts/legal-halacha-drain.config.cjs @@ -10,18 +10,33 @@ * * Pattern: cron_restart fires the script; autorestart:false → one-shot per tick * (pm2 shows "stopped" between ticks). Cheap no-op when the queue is empty. - * Cadence is conservative (every 2h) because Claude extraction is slow/rate- - * limited and each run adds to the chair's review queue. + * + * NIGHT-WINDOW (23:00–05:00 Israel time): backlog extraction is slow (Opus) and + * token-heavy, so it runs only off-hours — never competing with daytime + * interactive work / other agents (avoids the heartbeat timeout+process_lost + * storm we hit when the CEO drained the whole queue mid-day). Single-case + * extraction requested by the chair still runs immediately via the CEO; only + * the bulk backlog is gated to the night. + * + * TIMEZONE: pm2's daemon runs in the host TZ (UTC here), so cron strings are + * UTC. We fire across a UTC *superset* band (20:00–03:00 UTC) that covers the + * Israel window in BOTH DST states (UTC+3 summer / UTC+2 winter); the script's + * zoneinfo('Asia/Jerusalem') window-guard then trims to exactly 23:00–05:00 + * and no-ops on the margin ticks. Firing each hour also re-arms the one-shot if + * a tick died mid-drain — the global advisory lock makes overlap safe. * * Requires the local ``claude`` CLI + host ~/.env (POSTGRES_URL, etc.). * * Install (once): * pm2 start /home/chaim/legal-ai/scripts/legal-halacha-drain.config.cjs * pm2 save - * Run now (manual): mcp-server/.venv/bin/python scripts/drain_halacha_queue.py - * Schedule override: HALACHA_DRAIN_CRON (default every 2 hours at :47). + * Run now (manual, ignores window only if env-overridden): + * mcp-server/.venv/bin/python scripts/drain_halacha_queue.py + * Overrides: HALACHA_DRAIN_CRON (UTC cron) · HALACHA_DRAIN_WINDOW_START/_END + * (Israel hours, default 23/5) · HALACHA_DRAIN_TZ. */ -const cron = process.env.HALACHA_DRAIN_CRON || "47 */2 * * *"; +// UTC band covering Israel 23:00–05:00 across DST; script trims to exact window. +const cron = process.env.HALACHA_DRAIN_CRON || "0 20,21,22,23,0,1,2,3 * * *"; module.exports = { apps: [ diff --git a/web/paperclip_client.py b/web/paperclip_client.py index 56678fe..a2b9ec1 100644 --- a/web/paperclip_client.py +++ b/web/paperclip_client.py @@ -856,9 +856,10 @@ async def _get_or_create_library_project( """INSERT INTO projects (id, company_id, name, description, status, color) VALUES ($1, $2::uuid, $3, $4, 'backlog', $5)""", project_id, company_id, _LIBRARY_PROJECT_NAME, - "תור אוטומטי לחילוץ הלכות ומטא-דאטה מפסיקה שהועלתה לספריה. " - "כל issue כאן מייצג פסק דין שצריך לעבד — להריץ " - "mcp__legal-ai__precedent_process_pending.", + "תור אוטומטי לחילוץ הלכות ומטא-דאטה מפסיקה שהועלתה לספריה. כל issue " + "כאן מייצג פסק דין יחיד שצריך לעבד — להריץ " + "precedent_extract_metadata + precedent_extract_halachot עבור ה-case_law_id " + "של ה-issue בלבד (לא precedent_process_pending, שמרוקן את כל התור).", "#a17a3a", # gold-ish ) await _ensure_default_workspace(conn, project_id, company_id) @@ -876,12 +877,17 @@ async def wake_for_precedent_extraction( Creates a Paperclip issue under the per-company "ספריית פסיקה" project, assigns it to the company CEO, links the case_law_id via plugin_state, - and wakes the CEO via the Board API. The CEO instructions tell it to - run `mcp__legal-ai__precedent_process_pending` and close the issue. + and wakes the CEO via the Board API. The issue tells the CEO to extract + THIS case only — ``precedent_extract_metadata`` + ``precedent_extract_halachot`` + for the linked ``case_law_id`` — NOT ``precedent_process_pending``, which + drains the whole historical backlog (hours) and blows the heartbeat's + time budget (→ timeout/process_lost). The backlog is drained separately by + the nightly ``legal-halacha-drain`` service (23:00–05:00). Best-effort: any failure is logged and swallowed so a partial Paperclip - outage doesn't block the upload itself. The user can always invoke - `precedent_process_pending` manually. + outage doesn't block the upload itself. ``request_halacha_extraction`` still + stamps ``halacha_extraction_requested_at`` as a safety net, so the nightly + drain picks the case up even if this wakeup fails. """ if not PAPERCLIP_BOARD_API_KEY: logger.warning( @@ -914,10 +920,13 @@ async def wake_for_precedent_extraction( f"**case_law_id:** `{case_law_id}`\n" f"**citation:** {citation}\n\n" f"---\n\n" - f"**משימה:** הרץ את הכלי `mcp__legal-ai__precedent_process_pending` " - f"פעמיים — פעם עם `kind='metadata'` ופעם עם `kind='halacha'`. " - f"הכלי יעבד את כל הפסיקות בתור (כולל זו), כך שגם אם הופעל מאוחר " - f"יותר עבור פסיקות אחרות — אין בעיה.\n\n" + f"**משימה — חלץ את הפסיקה הזו בלבד** (לא את התור):\n" + f"1. `mcp__legal-ai__precedent_extract_metadata(case_law_id=\"{case_law_id}\")`\n" + f"2. `mcp__legal-ai__precedent_extract_halachot(case_law_id=\"{case_law_id}\")`\n\n" + f"⚠️ **אל תריץ** `precedent_process_pending` — הוא מרוקן את **כל** התור " + f"ההיסטורי (מאות פסיקות, שעות) וחורג מתקציב-הזמן של ה-heartbeat → " + f"timeout/process_lost. ריקון-הבאקלוג רץ בנפרד כשירות-לילה ייעודי " + f"(`legal-halacha-drain`, 23:00–05:00). כאן — רק התיק הזה.\n\n" f"לאחר ריצה: סמן את ה-issue כ-done ופתח comment קצר עם מספר ההלכות " f"שחולצו ושדות המטא-דאטה שהושלמו." ) -- 2.49.1