feat(mcp): codekeeper_multi_edit_file — כמה עריכות בקובץ אחד, בגרסה אחת — ו-expected_content_sha256 בכלי העריכה

Merged
AM
22d ago
+1425-71

תבנית Pull Request

✨ תיאור קצר

  • כלי כתיבה חדש, codekeeper_multi_edit_file: רשימת זוגות (old_string, new_string, replace_all) שמוחלים לפי הסדר, כל זוג על התוצאה של קודמו, ונשמרים כגרסה אחת — הכל או כלום. עד היום עדכון אחד בכמה מקומות בקובץ היה כמה קריאות ל-codekeeper_edit_file: גרסה, התראה והזדקנות של התיאור לכל אחת, וקובץ מעודכן בחלקו כשאחת נכשלה באמצע.
  • לצידו, שער אופטימי בכלי העריכה (codekeeper_edit_file, codekeeper_multi_edit_file, codekeeper_append_file): expected_content_sha256 — הכתיבה מסורבת ב-conflict, בלי לכתוב, אם הקובץ השתנה מאז שהסוכן קרא אותו.

📦 שינויים עיקריים

  • קוד (Backend)
  • בוט טלגרם
  • מסד נתונים/מיגרציות
  • תיעוד (docs/)
  • DevOps/CI/CD

פירוט נקודות:

  • mcp_server/handlers.py — multi_edit_file. כל מה שזול ומקומי נבדק לפני הקריאה מהמסד: missing_edits, too_many_edits מעל MAX_EDIT_PAIRS (עם max — נדחה ולא נחתך), המבנה של כל הזוגות (EditPair, extra="forbid", strict=True, דרך TypeAdapter(list[EditPair]) אחד) — invalid_edit עם index ו-problems בלי להדהד את הערך — וצורת ה-hash. אחר כך not_found, empty_file, השער, הזוגות דרך אותו _apply_edit של edit_file, ושמירה אחת ב-_resave_edited, שקורא ל-backend.save_file. זוג שנכשל מחזיר את הקוד של edit_file עם index ו-hint; ב-no_match וב-ambiguous_match גם occurrences (אחרי הזוגות הקודמים) ו-occurrences_in_stored_version (בקובץ השמור), והרמז אומר מתי זוג קודם שינה את הטקסט. תשובת ההצלחה דרך _resaved_answer, עם edits_applied ו-replacements (החלפות בפועל).
  • השער expected_content_sha256 (_invalid_expected_sha256, _changed_since_read): משווה ל-content_sha256 שה-backend כבר חישב (_full, אותה נקודה שממנה get_file מחזיר אותו), לפני הזוג הראשון. conflict נושא file בלי תוכן (_file_meta), עם version ו-content_sha256 הנוכחיים. ערך שאינו 64 ספרות הקס — invalid_expected_content_sha256, לפני כל קריאה. hash ולא version, כי check_file_sync ב-database/bookmarks_manager.py כותב code במקום בלי להעלות גרסה.
  • _apply_edit בודק את אורך התוצאה לפני שהיא נבנית (max_size): בלי זה replace_all עם new_string ארוך בנה בזיכרון מחרוזת גדולה פי כמה מהתקרה לפני שנדחה, ובבאץ' זוג שמחליף "a" ב-"aa" כופל את הטקסט בכל חזרה. חל גם על codekeeper_edit_file (אותו code_too_large עם max, רק לפני ההקצאה).
  • validation_problems — פונקציה אחת ל-invalid_item של codekeeper_read_batch ול-invalid_edit.
  • mcp_server/server.py — רישום הכלי (def סינכרוני, _WRITE_TOOL, require_write ראשון, התיאור נגמר ב-_WRITE_SHA256_DOC); edits: list[Any] עם סכימת הזוג שנגזרת מ-EditPair; תיאור פרמטר משותף ל-expected_content_sha256 בשלושת כלי העריכה; משפט הפניה בתיאור של edit_file; בלוק ה-instructions — 2,038 מתוך 2,048.
  • תיעוד — docs/mcp-server.rst (שורת הכלי, תיקון שורת edit_file עם replace_all, MAX_EDIT_PAIRS בטבלת הקבועים, סעיף חדש "כמה עריכות בגרסה אחת" עם תת-סעיף על expected_content_sha256, משקל 1 ליד "באץ' נשקל כמספר הפריטים", שורות בפתרון תקלות, וניסוח בלי ספירה במקומות שאמרו "שלושת הכלים"), docs/whats-new.rst, docs/deployment/workers.rst, mcp_server/README.md.

🧪 בדיקות

  • Unit

  • Integration

  • Manual

  • tests/test_mcp_multi_edit.py (חדש, 75 טסטים) — הכל דרך mcp.call_tool על build_mcp אמיתי, מעל ProductionBackend, DatabaseManager ו-Repository אמיתיים עם config.py של הייצור (_save_layer_harness); ה-hash בכל בדיקה מחושב ב-hashlib על מה שבאוסף, ו"שום דבר לא נכתב" נמדד בשלושה מקומות (מספר הגרסאות, ה-hash, push_events). מכסה: הכל או כלום (4 זוגות, השלישי נכשל), גרסה אחת + התראה אחת + description_age_versions עולה ב-1, סדר, replace_all לכל זוג, edits כמחרוזת JSON, "null"/"[1, 2]" בתוך זוג, זוג פגום (6 צורות × רשימה/JSON) בלי הדהוד, ריק/מעל התקרה, save_file שמחזיר db_error, code_too_large עם max_code_size מוקטן, שיא זיכרון ב-tracemalloc לשרשרת כפילה ול-replace_all ארוך, השער בשלושת הכלים (נכון בשתי רישיות, ישן, כתיבה במקום, חסר, 7 צורות פגומות), ו-backend סופר שמוכיח אפס קריאות מהמסד על קלט פגום.

  • מול mongod אמיתי (8.0.15 מקומי): tests/test_mcp_content_sha256_real_mongo.py — מטריצת 12 המקרים גם דרך הכלי החדש, הכל או כלום על אוסף אמיתי, ו-update_one במקום שנענה conflict בשלושת הכלים. 29 passed, 0 skipped (-rs). ב-CI הקובץ מדולג, כמו היום.

  • 15 מוטציות ב-git worktree נפרד — כולן נתפסו: שמירה כשזוג נכשל, זוגות על המקור, חיתוך במקום too_many_edits, השמטת occurrences_in_stored_version, ולידציה אחרי הקריאה, הדלפת הערך (שתי צורות), ביטול ההשוואה, בלי בדיקת הגודל החזויה, שמירה לכל זוג, בלי בדיקת צורת ה-hash, replacements שסופר זוגות, strict=False, extra="ignore", תקרה שהשתנתה בלי התיעוד, והשער אחרי הזוגות.

  • ריצת בקרה על main: 69 מתוך 75 הטסטים החדשים נופלים. ה-6 שעוברים הם בקרות חיוביות של edit_file/append_file (hash נכון, או בלי הפרמטר — הכתיבה עוברת), ועל main הם עוברים כי ה-SDK מתעלם שם מארגומנט לא מוכר.

  • שרת חי: uvicorn mcp_server.app:app במצב PAT על מסד אימות, ולקוח MCP אמיתי על streamable HTTP: 20 בדיקות עברו — הסכימה ב-tools/list, גרסה אחת והתראה אחת, edits כמחרוזת JSON, ו-conflict/invalid_edit שמשאירים את המסד כמו שהיה (נקרא ישירות ב-pymongo).

  • מקביליות (asyncio.gather, שני סשנים, אותו hash, 30 סבבים): 30/30 — אחד עובר ואחד conflict, 0 מספרי גרסה כפולים. בקרה: תור הכתיבה הורחב ל-8 עובדים — 30/30 שניהם עוברים, ו-30 מספרי גרסה כפולים. כלומר העובד היחיד של תור הכתיבה הוא מה שהופך את השער לאטומי בין כותבי MCP, וזה גם מראה את הפער שנשאר מול כתיבה מהוובאפ או מהבוט.

  • כל החבילה: 7,233 passed, 19 skipped, 5 failed — כולם ב-tests/test_infrastructure.py (isort/autopep8 לא היו ב-PATH בסביבה שלי); עם ה-venv ב-PATH — 12/12. אחרי שינוי התיעוד: 2,851 passed בטסטים שקוראים את docs/. scripts/generate_ai_map.py --check — עדכני. flake8 בקונפיג של CI — נקי.

🧪 בדיקות נדרשות ב‑PR

  • 🔍 Code Quality & Security
  • Unit Tests (3.11)
  • Unit Tests (3.12)

📝 סוג שינוי

  • feat: פיצ'ר חדש
  • fix: תיקון באג (הגודל נבדק לפני שהתוצאה נבנית)
  • docs: שינוי תיעוד בלבד
  • refactor: שינוי קוד ללא שינוי התנהגות
  • perf: שיפור ביצועים
  • chore/ci: תשתית/CI
  • breaking change: שינוי שובר תאימות — הפרמטר החדש אופציונלי, ובלעדיו אין בדיקה

✅ צ'קליסט

  • הקוד עוקב אחרי הסגנון — flake8 בקונפיג של CI נקי. black ו-ruff אינם נאכפים בריפו (|| true ב-ci.yml), והקבצים הקיימים עצמם אינם נקיים מהם
  • בדיקות רצות ועוברות
  • תיעוד עודכן (README/Docs)
  • ג'ובים חדשים — לא רלוונטי
  • משתני סביבה — לא נוספו
  • טוקנים של עיצוב — לא רלוונטי
  • אין סודות/מפתחות בקוד
  • אין מחיקות מסוכנות/פעולות על root
  • הודעת הקומיט תואמת Conventional Commits
  • CHANGELOG עודכן (docs/whats-new.rst)
  • כל ה‑Required Checks ירוקים — ממתין ל-CI
  • צילום/וידאו UI — לא רלוונטי
  • עיינתי במסמכי אתר התיעוד — נתיב: docs/mcp-server.rst | המשפט: "אותה גרסה מחזירה תמיד אותו hash. hash שמשתנה בלי שהגרסה משתנה פירושו שמשהו כתב את המסמך במקום, במקום לשמור גרסה חדשה."

עיון ב-CodeBot – Project Docs: כן. AI-MAP.md; docs/mcp-server.rst (טבלת הכלים, הקבועים, גבולות הבקשה והקצב, עדכון תיאור, גיל התיאור, content_sha256, התראה כשסוכן שומר קובץ, פתרון תקלות); docs/doc-authoring.rst; docs/versioning-stable-anchors.rst; docs/style-glossary.rst; docs/testing.rst (מונגו אמיתי); docs/deployment/workers.rst. ובנוסף מסמכי העבודה ב-CodeKeeper (Ck-MCP-Handoff.md, ידע-משיחת-התכנון-MCP.md) ודפוסי amir-bug-patterns שנדלקו (K11, K13, K15, U1, U3, H6, R6, עשרה קבצי bugbot-rules, BY-STACK/external-sdk.md, TESTING-PATTERNS.md ו-claude-md-snippets/testing.md).

🧩 השפעות/סיכונים

  • ביצועים: קריאה אחת מהמסד ושמירה אחת לבאץ'; הזוגות הם פעולות על מחרוזת. במכסת הקצב הקריאה שוקלת 1 — אין ענף ב-call_tool.
  • אבטחה: invalid_edit אינו מהדהד ערכים (נבדק במוטציות). edits שאינו רשימה נדחה בוולידציה של ה-SDK, שמהדהדת את הקלט ב-$mcp_error_message — התנהגות קיימת לכל פרמטר מוקלד (כמו items של הבאץ'), שהחשיפה שלה אושרה ב-mcp_server/analytics.py.
  • גוף הבקשה: באץ' גדול יכול להיתקל ב-body_too_large. התקרה נגזרת מבקשת השמירה הגדולה ביותר (request_bytes_for), ובכלי העריכה old_string ו-new_string יחד יכולים לעבור אותה — זה נכון כבר היום ל-edit_file. מתועד בסעיף החדש; לא שיניתי את הגזירה.
  • האטומיות מול כותבי MCP נשענת על פריסה בתהליך אחד (כמו מגביל הקצב, numInstances: 1 בהגדרות Render, מחוץ לריפו).

ממצאים שלא תוקנו כאן, בכוונה (מחוץ לתוכנית):

  • ⚠️ codekeeper_note_str_replace עדיין בונה את התוצאה לפני שהוא מודד אותה — אותו שורש כמו בעריכת קובץ. נמדד: פתק של 2,000 תווים, replace_all על "a" עם new_string של 10,000 תווים — שיא של 20,010,567 בתים לפני content_too_long. בתקרות של היום (20,000 תווים לפתק, גוף בקשה של 1MiB) זה עשרות GB. התיקון השורשי: max_size חובה ב-_apply_edit, ו-note_str_replace מעביר MAX_NOTE_CONTENT. ממתין לאישור.
  • טבלת ה-PRים ב-Ck-MCP-Handoff.md חסרה את #3491–#3493.
  • התיאור של פריטי codekeeper_read_batch בסכימה (SectionItem/FileItem) הוא ה-docstring העברי של המודל, עם בקטיקים של RST — pydantic מעתיק אותו ל-description.
  • #3391 סגור ב-GitHub (נסגר ב-#3467 על ממצא הפרסר), אבל הכתיבה האטומית עצמה לא תוקנה. בתיעוד כתוב "הכיוון שמתואר ב-#3391", בלי לטעון שהוא פתוח.

🔗 קישורים

  • Issues קשורים: #3391 (הכיוון לכתיבה אטומית)
  • Docs Preview: ה-check של Read the Docs על ה-PR
  • מסמכים/מפרטים רלוונטיים: docs/mcp-server.rst — "כמה עריכות בגרסה אחת" ו-"עריכה על מה שקראתם"
  • Branch Protection & PR Rules

🧯 סיכון / החזרה לאחור (Rollback)

  • Revert של ה-PR מסיר את הכלי ואת הפרמטר. אין מיגרציה, אין אינדקס ואין אוסף חדש. לקוח שלא שולח expected_content_sha256 אינו מושפע גם עכשיו.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KzqebZxV4sxJbtGYA7bBFG


Generated by Claude Code

Summary by Sourcery

Enable atomic multi-edit file updates and optimistic protection against overwriting content that changed after it was read.

New Features:

  • Add the codekeeper_multi_edit_file MCP tool to apply ordered find-and-replace edits and save them as a single version atomically.
  • Add optional expected_content_sha256 optimistic concurrency checks to file edit, multi-edit, and append tools.

Bug Fixes:

  • Reject edits that would exceed the file size limit before constructing the resulting string, preventing excessive memory use.

Enhancements:

  • Standardize validation error reporting without echoing submitted values and reuse it across batch-reading and multi-edit validation.
  • Expose conflict metadata and multi-edit result counts through the MCP tool responses.

Documentation:

  • Document the new multi-edit tool, content-hash conflict handling, limits, and updated file-writing behavior.

Tests:

  • Add comprehensive unit, integration, real-Mongo, concurrency, validation, atomicity, and memory-use coverage for multi-edit and content-hash safeguards.