diff --git a/mcp-server/src/legal_mcp/services/db.py b/mcp-server/src/legal_mcp/services/db.py index 3848a7b..bf856ae 100644 --- a/mcp-server/src/legal_mcp/services/db.py +++ b/mcp-server/src/legal_mcp/services/db.py @@ -1512,8 +1512,27 @@ ALTER TABLE drain_controls ADD COLUMN IF NOT EXISTS burst_until TIMESTAMPTZ; """ +# Stable, arbitrary key for the session-level advisory lock that serialises +# schema DDL across processes. Every short-lived process (cron drains, services) +# re-runs the idempotent migrations on startup; without this lock two processes +# that fire at the same minute race on AccessExclusiveLock and Postgres kills one +# with DeadlockDetectedError. The lock makes a concurrent migrator wait instead. +_MIGRATION_LOCK_KEY = 778899001 + + async def _run_schema_migrations(pool: asyncpg.Pool) -> None: async with pool.acquire() as conn: + # Serialise DDL across processes: block until any sibling migrator + # finishes, then run against the (now up-to-date, idempotent) schema. + await conn.execute("SELECT pg_advisory_lock($1)", _MIGRATION_LOCK_KEY) + try: + await _apply_schema_ddl(conn) + finally: + await conn.execute("SELECT pg_advisory_unlock($1)", _MIGRATION_LOCK_KEY) + logger.info("Database schema initialized (v1-v37)") + + +async def _apply_schema_ddl(conn: asyncpg.Connection) -> None: await conn.execute(SCHEMA_SQL) await conn.execute(MIGRATIONS_SQL) await conn.execute(SCHEMA_V2_SQL) @@ -1552,7 +1571,6 @@ async def _run_schema_migrations(pool: asyncpg.Pool) -> None: await conn.execute(SCHEMA_V35_SQL) await conn.execute(SCHEMA_V36_SQL) await conn.execute(SCHEMA_V37_SQL) - logger.info("Database schema initialized (v1-v37)") async def init_schema() -> None: diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index 81c9619..2943551 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -109,12 +109,12 @@ | `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: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) | +| `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 ומצית את הדריינר הקיים). טיק יחיד: מצית כשבטל+תור≠ריק · 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 | -| `legal-halacha-supervisor.config.cjs` | pm2/js | **תזמון כל 15 דק' של `halacha_drain_supervisor.py`** (cron `*/15 * * * *`, `HALACHA_SUPERVISOR_CRON` לעקיפה). `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) | +| `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` | -| `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) | +| `legal-digest-drain.config.cjs` | pm2/js | **תזמון כל שעתיים של `drain_digests.py`** (cron `12 */2 * * *`, `DIGEST_DRAIN_CRON` לעקיפה; דקת-הצתה `:12` כדי לא לחלוק דקה עם metadata-drain `:00` — מונע deadlock של DDL-המיגרציה) — הועבר מ-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) | | `renumber_cases.py` | python | **מיגרציה חד-פעמית (בוצעה 2026-06-12)** — תיקון 11 מספרי-תיקים לפורמט קנוני `NNNN-MM-YY` (הוספת ספרות-חודש; 1046-26→1024-02-26 תיקון-סידורי). רץ על ה-host (לא בקונטיינר): DB pool של האפליקציה + `mcli` (MinIO) + Gitea API + Paperclip DB. אטומי per-case עם גיבוי ל-`data/audit/` ואימות-אחרי. FK-ים על `cases.id` (UUID) לא נגעו; משכתב כל עמודה עם `cases/{old}/` (file_path **וגם** image_thumbnail_path שהוא storage-key בלי `/data`), מנרמל זהות חוצת-קורפוס (case_law/style_corpus/style_exemplars/citations — לא תוכן/full_text), מעביר מפתחי-MinIO ב-3 buckets (legal-immutable=WORM copy-only), משנה-שם repo ב-Gitea, ומעדכן שם-פרויקט ב-Paperclip. dry-run כברירת-מחדל; `--apply --tier clean\|archive`. **מיצוי — לא להריץ שוב** (ה-MAPPING היסטורי). | חד-פעמי — בוצע | diff --git a/scripts/legal-digest-drain.config.cjs b/scripts/legal-digest-drain.config.cjs index 83ed966..91d9f94 100644 --- a/scripts/legal-digest-drain.config.cjs +++ b/scripts/legal-digest-drain.config.cjs @@ -17,9 +17,12 @@ * pm2 start /home/chaim/legal-ai/scripts/legal-digest-drain.config.cjs * pm2 save * Run now (manual): mcp-server/.venv/bin/python scripts/drain_digests.py - * Schedule override: DIGEST_DRAIN_CRON (default every 2 h at :00). + * Schedule override: DIGEST_DRAIN_CRON (default every 2 h at :12). */ -const cron = process.env.DIGEST_DRAIN_CRON || "0 */2 * * *"; +// Minute :12 (not :00) so it never shares a firing minute with the every-hour +// legal-metadata-drain (:00) — avoids the schema-migration DDL deadlock when +// sibling drains start at the same instant. +const cron = process.env.DIGEST_DRAIN_CRON || "12 */2 * * *"; module.exports = { apps: [ diff --git a/scripts/legal-halacha-drain.config.cjs b/scripts/legal-halacha-drain.config.cjs index 0ca85ce..8c247fe 100644 --- a/scripts/legal-halacha-drain.config.cjs +++ b/scripts/legal-halacha-drain.config.cjs @@ -36,7 +36,10 @@ * (Israel hours, default 23/5) · HALACHA_DRAIN_TZ. */ // 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 * * *"; +// Fires at minute :10 (not :00) so it never shares a firing minute with +// legal-metadata-drain (:00) or legal-halacha-supervisor (:05) — avoids the +// schema-migration DDL deadlock when sibling drains start at the same instant. +const cron = process.env.HALACHA_DRAIN_CRON || "10 20,21,22,23,0,1,2,3 * * *"; module.exports = { apps: [ diff --git a/scripts/legal-halacha-supervisor.config.cjs b/scripts/legal-halacha-supervisor.config.cjs index 1fb7109..726939b 100644 --- a/scripts/legal-halacha-supervisor.config.cjs +++ b/scripts/legal-halacha-supervisor.config.cjs @@ -27,7 +27,10 @@ * pm2 start /home/chaim/legal-ai/scripts/legal-halacha-supervisor.config.cjs * pm2 save */ -const cron = process.env.HALACHA_SUPERVISOR_CRON || "*/15 * * * *"; +// Staggered to minute :05 of the */15 cycle (:05,:20,:35,:50) so it never shares +// a firing minute with legal-metadata-drain (:00) or legal-halacha-drain (:10) — +// avoids the schema-migration DDL deadlock when sibling drains start together. +const cron = process.env.HALACHA_SUPERVISOR_CRON || "5-59/15 * * * *"; module.exports = { apps: [