feat(mcp): codekeeper_multi_edit_file — כמה עריכות בקובץ אחד, בגרסה אחת — ו-expected_content_sha256 בכלי העריכה
תבנית 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_fileMCP tool to apply ordered find-and-replace edits and save them as a single version atomically. - Add optional
expected_content_sha256optimistic 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.