Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/mcp-server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ usable=8)`` מול ``cpu quota cgroup v2: 0.50 cpu``, כלומר 12 חוטים
המספרים מחדש **לשני הפרסרים** — מריצים אותו כשאחד מהם משתנה.
- **ההחזקה הגדולה ביותר של חוט קריאה אינה הפרסור, וזו החלטה מתועדת.** כלי
האדמין ``codekeeper_get_repo_file`` עם ``outline=true`` או ``lines=`` קורא עד
``RANGE_READ_MAX_BYTES`` (‏10MiB), והמראה טוענת את ה-blob כולו לזיכרון
**לפני** בדיקת הגודל. נמדד (2026-09-20): outline של קובץ RST בגודל 10MB
``RANGE_READ_MAX_BYTES`` (‏10MiB), והמראה טוענת blob **שמתחת לתקרה** כולו
לזיכרון לפני הפענוח והחיתוך (blob שמעליה נדחה מ-``git cat-file -s`` בלי
להיקרא — ראו #3433 למטה). נמדד (2026-09-20): outline של קובץ RST בגודל 10MB
מחזיק 61–77MiB לאורך המסלול כולו — הטקסט של 10MB בזיכרון ועליו הפרסור; שורת
``outline_densest_real`` של הסקריפט מראה את הפרסור לבדו מעל השיא שלפניו,
51MiB — וקריאת ``lines=`` של ``webapp/static/js/md_preview.bundle.js``
Expand All @@ -128,7 +129,10 @@ usable=8)`` מול ``cpu quota cgroup v2: 0.50 cpu``, כלומר 12 חוטים
(הגדול ביותר כאן 169KB). תמחור לפי ``RANGE_READ_MAX_BYTES`` היה מצמצם את
המסלול הציבורי ל-4 חוטים (לפי 77MiB) או ל-9 (לפי 37MiB) בגלל צורה שאין
לה מקור. מה שכן השתנה בגלל זה הוא התקרה (למטה); והשורש — לחסום את הקריאה
במקור, בבדיקת גודל לפני ``git show`` — נמצא ב-#3433.
במקור — נסגר ב-#3433: ``get_file_at_commit`` שואל את מאגר האובייקטים לגודל
(``git cat-file -s <sha>:<path>``) לפני ``git show``, ולכן ``max_size`` הוא
חסם על מה שנקרא ולא בדיקה בדיעבד. נמדד: קובץ של 12MB שנדחה עלה 20MiB שיא
לפני, ואפס אחרי; קובץ של 7MB שמתחת לתקרה נשאר 14MiB, כי אותו כן קוראים.
- **רצפה** (``_READ_POOL_FLOOR``, ‏2): כדי שקריאת מונגו תקועה אחת לא תשבית
את השאר. שניים הוא הגודל הקטן ביותר עם התכונה הזאת, וכל יחידה מעליו היא
זיכרון שהחשבון אמר שאין. **תקרה** (``_READ_POOL_CAP``, ‏12): הרוחב שהייצור
Expand Down
1 change: 1 addition & 0 deletions docs/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ What's New

2026-09-20
----------
- **fix (mirror): ‏``get_file_at_commit`` בודק את גודל הקובץ במאגר האובייקטים לפני ``git show``, ולכן קובץ מעל התקרה אינו נטען לזיכרון כלל (#3433, פריט 9).** עד היום ``git show`` נטען כולו לזיכרון ורק אז ``max_size`` נבדק — כלומר התקרה הייתה בדיקה בדיעבד: קובץ של 12MB שנדחה עלה 20MiB שיא בחוט הקריאה לפני שהתשובה הייתה ``file_too_large``. מעכשיו ``git cat-file -s <sha>:<path>`` מחזיר את גודל האובייקט בלי לקרוא אותו, שתי הפקודות פונות לאותו קומיט שנפתר פעם אחת (אין חלון לסנכרון להיכנס בו), וקובץ מעל התקרה נדחה לפני שנקרא בית אחד — אותה תשובה, אותו ``size``, אפס זיכרון. כשל בבדיקת הגודל אינו נופל לקריאה בלי תקרה, ומה שמוחזר עדיין נבדק מול התקרה בנפרד, לכל סוג אובייקט. נמדד: 12MB מעל תקרה של 500KB ושל 10MiB — 20.2 ו-20.4MiB לפני, אפס אחרי; 7MB מתחת לתקרה — 14MiB לפני ואחרי, כי אותו כן קוראים. ראו "מודל הריצה של הכלים" ב-:doc:`mcp-server`.
- **feat: ``codekeeper_docs_get_section`` קורא גם קובצי Markdown, ולכל ריפו מדיניות נתיבים משלו.** עד היום הכלי ידע לקרוא ``.rst`` בלבד, ורק מתחת ל-``docs/``. הפארסר של Markdown נכתב ונמדד כבר קודם — ואף אחד לא קרא לו; זה השינוי שמחווט אותו. מעכשיו הסיומת בוחרת את הפארסר, ולכל ריפו מוצהר **איפה התיעוד שלו יושב ובאיזה פורמט**: ``CodeBot`` נשאר ``docs/`` עם ``.rst`` ללא שינוי, ו-``amir-bug-patterns`` מגיש ``.md`` משורש הריפו. **וזו הרחבה מכוונת של גבול הקריאה** — הכלי ציבורי, כלומר כל משתמש מאומת מפעיל אותו בלי אדמין, והריפו שנוסף ציבורי ב-GitHub. מה ש**לא** השתנה: מדיניות סינון הסודות חלה על המסלול הזה במלואה, ולכן ``path="secrets"`` מחזיר ``path_denied`` ולא ``not_found``, וגם ``secrets.md`` ו-``credentials.md`` חסומים; ותקרת 500KB של שירות המראה נשארת ההגנה על הפרסור, כי הכלי קורא את הקובץ כולו בלי ``lines``. **שני שערים ולא אחד:** ``MCP_DOCS_REPO`` מתיר בזמן ריצה, טבלת המדיניות בקוד יודעת לקרוא, וריפו שנמצא במשתנה הסביבה ואין לו מדיניות נדחה ב-``repo_not_configured`` במקום ליפול לברירת מחדל מתירנית. **ובקשה לפורמט שהריפו אינו מגיש נדחית בשמה** — ``suffix_not_allowed`` עם רשימת הסיומות שכן מוגשות, במקום להשלים סיומת בשקט מעל הקיימת ולחזור כ"לא נמצא" על קובץ שקיים. שני סירובים חדשים מגיעים מהפרסור עצמו: ``inconsistent_line_endings`` על ``\r`` בודד (CRLF עובד במלואו), ו-``too_many_sections`` על קובץ שחוצה את תקרת 50,000 הכותרות — Markdown דרך ברירת המחדל של הפרסר, ו-RST דרך התקרה שהכלי מעביר מאז #3429; סירוב שהפארסר מספק לו מספר שורה נושא גם ``line`` — תמיד ב-Markdown, ולא ב-``too_many_sections`` של RST, כי ``rst_parser`` מרים אותה בלי מספר. ``includes`` הוא שדה של RST ולכן בעמוד ``.md`` הוא תמיד ריק. **ומסלול ה-RST לא זז**: אפס דיף מדוד על 5,920 רשומות מ-208 קובצי RST, לפני ואחרי. ראו :ref:`mcp-docs-path-policy`.
- **fix: מאגר הקריאות של ה-MCP נגזר ממכסת הזיכרון של הקונטיינר, ולא ממספר המעבדים של המארח (#3391).** כלי הקריאה רצים ב-``asyncio.to_thread``, כלומר ב-executor ברירת המחדל של הלולאה, שגודלו ``min(32, os.cpu_count() + 4)`` — ו-``os.cpu_count()`` בקונטיינר הוא מספר הליבות של **המארח**, כפי ש-CPython מתעד במפורש. שורת הקיבולת מהייצור אמרה את זה במספרים: ``read pool 12 threads (os.cpu_count=8, usable=8)`` על מכסה של ``0.50 cpu``. לקריאת מונגו זה לא מזיק; לפרסור בפייתון טהור אין מקביליות ב-CPU בגלל ה-GIL, ולכן 12 חוטים היו 12 עצי פרסור בזיכרון בו-זמנית — יותר ממה שנכנס ב-512MiB — ורגע לפני שהפרסור הופך לכלי ציבורי. מעכשיו ``attach_read_pool`` מתקין בעליית ה-lifespan של אפליקציית ה-ASGI מאגר משלו, בגודל ``(מגבלת הזיכרון − קו בסיס − שוליים) ÷ עלות פרסור אחד``: המגבלה נקראת מ-cgroup בזמן ריצה (``memory.max`` ב-v2, ‏``memory.limit_in_bytes`` ב-v1) ולא מקובעת בקוד, קו הבסיס והשוליים הם מדידות מהשירות בייצור עם תאריך, ועלות הפרסור נגזרת מ-``MAX_FILE_SIZE_FOR_DISPLAY`` כי הכלי דוחה ``too_large`` לפני שהוא מפרסר. על תוכנית הייצור זה **10** חוטים במקום 12 — עלות הפרסור נמדדה מחדש על הפרסר הנעוץ (``scripts/measure_md_parse_cost.py``; הקבוע נמדד על ``services.md_parser`` כהכנה לכלי ה-Markdown של PR 5, בעוד הכלי הציבורי היום מפרסר RST ב-``services.rst_parser``, שעולה כ-6 בתים לבית גם על העמוד הצפוף ביותר — והסקריפט מודד את שניהם), והיא תלויה בצפיפות הטוקנים של המסמך ולא בגודלו: המסמך הצפוף ביותר בקורפוס הוא הקבוע, וצורה עוינת של שורות-תבליט בודדות עולה פי ארבעה ממנו — מה ששום רוחב מאגר אינו סופג, ומחכה לתקרת טוקנים בפרסר עצמו. **ובעקבות הסקירה:** ``codekeeper_docs_get_section`` מעביר לפרסר את תקרת הסקשנים של האאוטליין (50,000) ומחזיר ``too_many_sections`` במקום לפרסר את הקובץ כולו — צורה עוינת של RST (כותרת בת תו אחד בכל שורה) עלתה 44.0MiB לפרסור, יותר מ-35.2MiB שהמאגר מקצה לחוט, ועכשיו נעצרת ב-20.1MiB; אף עמוד אמיתי אינו מתקרב לתקרה. רצפה של 2 (קריאה תקועה אחת לא משביתה את השאר), תקרה של 12, הרוחב שהייצור כבר הריץ (מעבר לתוכנית גדולה לא מרחיב את המאגר בלי החלטה; הגרסה הראשונה נקבה ב-32, והסקירה של #3429 הורידה אותה כי המחלק מתאר את הפרסור ולא את קריאות הטווח של האדמין; חסימת הקריאה במקור — #3433), וכשאין מגבלה קריאה — הרצפה, **לא** ``cpu_count + 4``, עם WARNING שאומר שזה קרה. **ושורת הקיבולת מפסיקה לחשב בעצמה:** היא נרשמת מתוך ה-lifespan, קוראת את ``_max_workers`` של המאגר שהותקן בפועל, ומדווחת גם את מגבלת הזיכרון לצד מכסת ה-CPU — כי רשומה שמתארת מצב ומתעדכנת בנפרד ממנו היא בדיוק מה שמשקר בשקט. ה-seam הוא עטיפת ``router.lifespan_context``, אותו seam שניקוז ה-PostHog כבר משתמש בו, כי ``FastMCP.streamable_http_app()`` בונה את Starlette עם ``lifespan=`` משלה ו-``on_startup`` אינו רץ. הטסט המרכזי נכנס ל-lifespan של האפליקציה האמיתית ומוכיח שקריאת ``to_thread`` נוחתת על חוט של המאגר החדש — לא שפונקציה נקראה; שורת הקיבולת נבדקת בתת-תהליך נקי דרך הפלט האמיתי ולא דרך ``caplog``. ראו "מודל הריצה של הכלים" ב-:doc:`mcp-server`.
- **feat: ``codekeeper_docs_get_section`` מוצא סעיף גם לפי המזהה שלו, ולא רק לפי השם המלא.** הסוכנים מפנים זה לזה לפי מזהה — "קרא K11", "ראו U3" — ועד היום ``section="K11"`` החזיר ``section_not_found`` עם רשימת הצעות **ריקה**, כי ההתאמה הייתה שוויון מלא ו-``difflib`` עם ``cutoff=0.5`` אינו מוצא קרבה בין שלושה תווים לכותרת עברית ארוכה. כלומר הזרימה המרכזית של הכלי לא עבדה. מעכשיו, **ורק אחרי שהשוויון המלא לא מצא כלום**, שאילתה שבנויה כמזהה (עד שלוש אותיות ואחריהן עד שלוש ספרות, עם נקודה אופציונלית) מותאמת לכותרת שנפתחת באותו מזהה. **והמזהה נקרא כיחידה ולא כקידומת של מחרוזת**, ולכן ``K1`` מחזיר את ``K1`` בלבד ולעולם לא את ``K10`` עד ``K15`` — הצורה השבורה של הבדיקה הזאת היא אותה מחלקה בדיוק שבה גבול נתיב נבדק כרצף תווים. שאילתה שאינה בנויה כמזהה אינה עוברת במסלול הזה כלל, ולכן ``section="איך"`` ממשיך להחזיר אפס התאמות גם בעמוד שיש בו חמש-עשרה כותרות שמתחילות במילה הזאת. מזהה שחוזר פעמיים הוא ``ambiguous_section`` עם מועמדים, כמו כל כותרת כפולה, ושאילתת מזהה שלא נמצאה מקבלת ב-``suggestions`` קודם כול כותרת קרובה, אם יש כזו בעמוד, **ורק כשאין אף כותרת קרובה** את המזהים שכן קיימים — עד 50, ועם ``suggestions_truncated`` כשהיו עוד. **והתקרה גבוהה בכוונה**, כי רשימת מזהים אינה מדורגת: לשאלה "אלה שכן קיימים" אין תשובה "החמש הטובות", וחיתוך שלה מסתיר פריטים בלי קריטריון ובלי דרך לבקש את השאר. **והדגל הזה אומר את אותו דבר בשני המסלולים**: גם רשימת מזהים שנחתכה וגם רשימת כותרות קרובות שנחתכה מדליקות אותו. שדה בשם ``truncated`` שמשמעותו "נחתך" במסלול אחד ו"שקט" בשני הוא בדיוק ההפתעה שאנחנו מנסים למנוע. **ונקודה עוד יותר קלה לפספס: הנקודה שפותחת תת-מספור אינה גבול.** בכותרת ``K11. טקסט`` הנקודה **מסיימת** את המזהה, ובכותרת ``K11.1 טקסט`` היא **מפרידה** בתוך מזהה ארוך יותר — ולכן ``K11`` לעולם אינו מחזיר את ``K11.1``, וכותרת בתת-מספור נגישה בשמה המלא בלבד. זו אותה מחלקה בדיוק כמו ``K1`` שתופס את ``K10``: גבול שנבדק כתו בודד, בלי לשאול מה בא אחריו. **ההתנהגות על התיעוד כאן לא זזה, וזה נמדד ולא הונח:** הפלט של הכלי על כל קובצי ה-RST — כל כותרת, בכל צורותיה — זהה בית-בית לפני ואחרי. וההנחה שהכול נשען עליה, שאף כותרת בעמודים כאן אינה נפתחת במזהה, **אינה ספירה שנרשמה פעם אחת אלא טסט שרץ בכל CI**: ``tests/test_docs_headings_carry_no_identifier.py`` סורק את כל העמודים ונופל על כותרת שנפתחת במזהה, עם דוגמת autodoc אמיתית שמוכיחה שהוא מסוגל ליפול. **ומה שלא נבדק בקורפוס הזה נבדק בבקרה סינתטית**, כי סוללה שכל תשובותיה "לא נמצא" אינה מבדילה בין ענף כבוי לבין פרובים עיוורים. שני הדברים שמפתיעים קורא — קיצור המזהה, וכותרת שנושאת בקטיקים ולכן דורשת אותם בשאילתה — עברו לתיאור הפרמטר ``section``, שאינו נחתך אצל הלקוח כמו תיאור הכלי. ראו :ref:`mcp-docs-section`.
Expand Down
6 changes: 4 additions & 2 deletions mcp_server/repo_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@
#: ``too_large`` עם הפרמטר בדיוק כמו בלעדיו (#3317). ``webapp/app.py`` בן
#: 805,594 הבייטים, הקובץ שהכי הרבה עובדים עליו, היה בלתי קריא דרך הכלי.
#:
#: **למה תקרה אחרת ולא ביטול.** הבלוב עדיין נקרא ומפוענח במלואו לפני
#: **למה תקרה אחרת ולא ביטול.** בלוב שמתחת לתקרה נקרא ומפוענח במלואו לפני
#: החיתוך, אז "בלי תקרה" פירושו שקובץ פתולוגי בריפו יגיע ל-RAM כמו שהוא.
#: נמדד: קריאה ופענוח צורכים כפי שלושה מגודל הקובץ — 6.6MB הגיעו ל-35.7MB
#: שיא. תקרה של 10MB חוסמת את זה בערך ב-30MB, ונותנת פי 12 מרווח מעל
#: הקובץ הגדול ביותר שבאמת קוראים.
#: הקובץ הגדול ביותר שבאמת קוראים. ומאז #3433 התקרה נבדקת מול גודל
#: האובייקט במאגר **לפני** ``git show``, ולכן קובץ שמעליה אינו נטען כלל —
#: החסם הוא על מה שנקרא, לא בדיקה בדיעבד.
#:
#: זו החלטת מדיניות של שכבת ה-MCP, לא של שירות המראה — ולכן היא כאן ולא
#: שם, ואינה מייתרת את 500KB שממשיכה לחול על קריאה מלאה ועל הוובאפ.
Expand Down
5 changes: 4 additions & 1 deletion mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,10 @@ def _memory_limit() -> tuple[int | None, str]:
#: Pricing by ``RANGE_READ_MAX_BYTES`` would have cut the public path to 4
#: threads (at 77MiB) or 9 (at 37MiB) for a shape without a source. What did
#: change because of this is :data:`_READ_POOL_CAP`; the root fix — bounding
#: the read at its source with a size probe before ``git show`` — is #3433.
#: the read at its source with a size probe before ``git show`` (#3433) — has
#: landed in ``get_file_at_commit``: a blob over ``max_size`` is refused from
#: ``git cat-file -s`` without being read (12MB: 20MiB peak before, 0 after,
#: measured), so the hold above is now only what a file *under* the cap costs.
_PARSE_COST_BYTES = _PARSE_RSS_PER_INPUT_BYTE * MAX_FILE_SIZE_FOR_DISPLAY


Expand Down
Loading
Loading