דילוג לתוכן
  • חוקי הפורום
  • פופולרי
  • לא נפתר
  • משתמשים
  • חיפוש גוגל בפורום
  • צור קשר
עיצובים
  • בהיר
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • כהה
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • ברירת מחדל (ללא עיצוב (ברירת מחדל))
  • ללא עיצוב (ברירת מחדל)
כיווץ
מתמחים טופ
  1. דף הבית
  2. קטגוריות בהרצה
  3. תכנות
  4. בינה מלאכותית - AI
  5. עזרה הדדית - בינה מלאכותית
  6. שיתוף | צ'אט של Claude Code נחסם בנטפרי (418) בכל הודעה? הסבר, מניעה, וסקריפט שמתקן את הצ'אט לבד

שיתוף | צ'אט של Claude Code נחסם בנטפרי (418) בכל הודעה? הסבר, מניעה, וסקריפט שמתקן את הצ'אט לבד

מתוזמן נעוץ נעול הועבר עזרה הדדית - בינה מלאכותית
2 פוסטים 2 כותבים 143 צפיות 3 עוקבים
  • מהישן לחדש
  • מהחדש לישן
  • הכי הרבה הצבעות
תגובה
  • תגובה כנושא
התחברו כדי לפרסם תגובה
נושא זה נמחק. רק משתמשים עם הרשאות מתאימות יוכלו לצפות בו.
  • א מנותק
    א מנותק
    אוויר
    כתב נערך לאחרונה על ידי אוויר
    #1

    שלום לכולם,

    הודעה זו והסקריפטים שבה נכתבו על ידי Claude (הבינה המלאכותית של Anthropic), לבקשתי ובהנחייתי, ונבדקו כמתואר בסוף ההודעה.

    למי ש-Claude Code (או Claude בדפדפן) "נתקע" אצלו בנטפרי עם API Error: 418, או JSON Parse error: Unexpected EOF, ואחרי "נסה שוב" זה חוזר שוב ושוב. הנה מה שלמדתי, ומה שבניתי כדי שלא אצטרך לתקן ידנית.

    לא יודעים איך להתקין? תנו ל-Claude Code לעשות את זה

    אם אתם לא יודעים איך להתקין את הסקריפט וה-hook שבהודעה, פשוט העתיקו את הקישור להודעה הזו ל-Claude Code, והוא יתאים אותם למחשב שלכם. זו ההנחיה שאפשר לתת לו:

    קרא את ההודעה בקישור הזה: https://mitmachim.top/topic/103008
    
    התקן אצלי את שני הדברים שמופיעים בה: (1) הסקריפט netfree_recover.py ו-(2) ה-hook size_guard.py. התאם אותם למחשב שלי (נתיב פייתון, שם המשתמש, תיקיית .claude).
    
    כללים:
    - לפני כל שינוי תסביר לי בקצרה מה אתה הולך לעשות.
    - קודם גבה את הקובץ settings.json. את ה-hooks למזג לקובץ הקיים, בלי למחוק הגדרות שכבר יש לי.
    - את הסקריפט להריץ קודם עם --dry-run ולהראות לי את התוצאה.
    - את המשימה המתוזמנת (schtasks) אל תרשום בעצמך. תן לי את הפקודה המתאימה ואני אריץ אותה.
    - אל תשנה אף קובץ של צ'אט אמיתי בלי לשאול אותי.
    - אם אתה לא מצליח לפתוח את הקישור, תגיד לי, ואדביק לך את תוכן ההודעה.
    

    שימו לב: Claude Code (או האפליקציה) עלול לבקש אישור, או אפילו לסרב, לכתוב סקריפט שנוגע בקבצי ההיסטוריה של הצ'אטים. זה צפוי, ואפשר לאשר בעצמכם. לא נבדק על מחשבים אחרים.

    למה זה קורה

    בכל הודעה שאתם שולחים, כל היסטוריית הצ'אט נשלחת מחדש ל-api.anthropic.com, ונטפרי סורק אותה. אם קטע אחד בהיסטוריה (בדרך כלל תוצאה של כלי: קובץ שנקרא, דף, תמונה) הפעיל את הסינון, הוא ימשיך להפעיל אותו בכל הודעה אחריו. לכן "נסה שוב" לא עוזר, ולכן גם צ'אט חדש שנפתח כ"המשך" או מסיכום של הישן עלול לרשת את הבעיה.

    מה מגדיל את הסיכון

    • קריאת תמונות (גם תמימות) וצילומי מסך. התמונה נכנסת כ-base64 ונסרקת.
    • קריאה של קובץ או ספר גדול בבת אחת, במיוחד עם הרבה טקסט רגיש.
    • פלטים ארוכים של כלים.
    • הדבקת דף החסימה של נטפרי לתוך הצ'אט. הוא נשאר בהיסטוריה וממשיך להרעיל.

    מניעה (עובד אצלי)

    • לקרוא במנות קטנות (עד כמה עשרות שורות), לא פרקים שלמים.
    • לעבד תוכן רגיש בסקריפט מקומי, ולהביא ל-Claude רק סיכום.
    • לא לצלם ולא להדביק דפי חסימה. לתאר אותם במילים.
    • וכדי שלא אצטרך לזכור את זה בעצמי, יש לי hook אוטומטי (ההסבר והקוד מיד למטה).

    Hook שמונע מראש כניסה של בלוקים גדולים

    hook הוא סקריפט קטן ש-Claude Code מריץ אוטומטית לפני ואחרי כל כלי. אני משתמש בו כשומר גודל:

    • לפני קריאה: אם Claude מנסה לקרוא קובץ גדול (מעל כ-60KB) בבת אחת, ה-hook חוסם ואומר לו לקרוא בחלקים (offset ו-limit), לחפש בקובץ, או לעבד אותו בסקריפט מקומי.
    • לפני כתיבה: כתיבה של יותר מ-30,000 תווים בבת אחת נחסמת, וצריך לכתוב שלד ולהוסיף בחלקים. גם פלט גדול שנשלח אל נטפרי יכול לגרום לחסימה.
    • אחרי כלי: אם התוצאה גדולה מ-25,000 תווים, הוא מוסיף ל-Claude תזכורת לעבוד במנות קטנות.
    • אם הסקריפט עצמו נכשל, הוא לא חוסם כלום.

    אפשר לשנות את שלושת המספרים שבראש הקובץ.

    איך מתקינים:

    1. לשמור את הקוד למטה כ-size_guard.py בתיקייה .claude\hooks שבתיקיית המשתמש.
    2. להוסיף ל-.claude\settings.json (אם כבר יש שם "hooks" אז למזג, לא להכפיל):
    "hooks": {
      "PreToolUse": [
        { "matcher": "Read|Write",
          "hooks": [ { "type": "command",
            "command": "\"C:\\Users\\<שם המשתמש>\\AppData\\Local\\Programs\\Python\\Python312\\python.exe\" \"C:\\Users\\<שם המשתמש>\\.claude\\hooks\\size_guard.py\"",
            "timeout": 10 } ] }
      ],
      "PostToolUse": [
        { "hooks": [ { "type": "command",
            "command": "\"C:\\Users\\<שם המשתמש>\\AppData\\Local\\Programs\\Python\\Python312\\python.exe\" \"C:\\Users\\<שם המשתמש>\\.claude\\hooks\\size_guard.py\"",
            "timeout": 10 } ] }
      ]
    }
    
    1. לפתוח צ'אט חדש. ב-Claude Code אפשר לראות ב-/hooks אם הוא נטען.
    """שומר גודל: מונע כניסה ויציאה של טקסט גדול מדי מההקשר (נטפרי חוסם צ'אט שמעביר בלוקים גדולים).
    
    PreToolUse  : חוסם קריאה גדולה (Read בלי limit לקובץ גדול) וכתיבה גדולה (Write).
    PostToolUse : אם תוצאת כלי גדולה מדי - מזריק תזכורת לעבוד במנות ובסקריפט מקומי.
    כשל בסקריפט לא חוסם כלום (יוצאים בשקט).
    """
    import json
    import os
    import sys
    
    MAX_READ_FILE_BYTES = 60_000  # Read בלי limit: מעל זה חוסמים
    MAX_WRITE_CHARS = 30_000    # Write: מעל זה מפצלים
    MAX_RESULT_CHARS = 25_000   # תוצאת כלי: מעל זה מזכירים
    
    SKIP_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf", ".ipynb"}
    
    
    def deny(reason):
        print(json.dumps({"hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": reason}}, ensure_ascii=False))
        sys.exit(0)
    
    
    def remind(text):
        print(json.dumps({"hookSpecificOutput": {
            "hookEventName": "PostToolUse",
            "additionalContext": text}}, ensure_ascii=False))
        sys.exit(0)
    
    
    def main():
        sys.stdout.reconfigure(encoding="utf-8")
        data = json.loads(sys.stdin.buffer.read().decode("utf-8"))
        event = data.get("hook_event_name", "")
        name = data.get("tool_name", "")
        inp = data.get("tool_input") or {}
    
        if event == "PreToolUse":
            if name == "Read":
                path = inp.get("file_path", "")
                if (inp.get("limit") is None and not inp.get("allow_large")
                        and os.path.splitext(path)[1].lower() not in SKIP_EXT
                        and os.path.isfile(path) and os.path.getsize(path) > MAX_READ_FILE_BYTES):
                    deny(f"הקובץ גדול ({os.path.getsize(path) // 1000}KB). לקרוא בחלקים עם offset ו-limit, "
                         "או לחפש בו עם Grep, או לעבד אותו בסקריפט מקומי ולהביא רק סיכום.")
            elif name == "Write":
                if len(inp.get("content", "")) > MAX_WRITE_CHARS:
                    deny(f"כתיבה גדולה מדי בבת אחת (מעל {MAX_WRITE_CHARS} תווים). "
                         "לכתוב שלד קטן ולהוסיף בחלקים עם Edit, או לבנות את הקובץ בסקריפט.")
        elif event == "PostToolUse":
            size = len(json.dumps(data.get("tool_response", ""), ensure_ascii=False))
            if size > MAX_RESULT_CHARS:
                remind(f"תזכורת: תוצאת הכלי האחרון הייתה {size // 1000}K תווים. פלט/קלט גדול עלול לגרום לנטפרי "
                       "לחסום את הצ'אט. מכאן: לקרוא ולהדפיס במנות קטנות, לעבד בסקריפט מקומי, "
                       "ולהביא להקשר רק את מה שצריך.")
    
    
    try:
        main()
    except SystemExit:
        raise
    except Exception:
        sys.exit(0)
    

    חשוב לדעת: ה-hook מצמצם את הסיכון, לא מבטל אותו. גם קובץ קטן יכול להפעיל את הסינון אם התוכן שלו רגיש. את הגרסה הכללית שבהודעה (בלי החלק שלי לקריאה מספרייה מסוימת) בדקתי עכשיו עם קלטים מדומים: קובץ גדול נחסם, קריאה עם limit עוברת, פלט גדול מקבל תזכורת, וכתיבה גדולה נחסמת.

    התיקון: סקריפט שמזהה צ'אט חסום ומחליף את הקטע המרעיל

    Claude Code שומר כל צ'אט כקובץ מקומי (~/.claude/projects/<פרויקט>/<מזהה>.jsonl), והקובץ הזה לא עובר דרך נטפרי. הסקריפט:

    1. מזהה צ'אט שהשורה האחרונה בו היא API Error: 418.
    2. הולך אחורה אל תוצאות הכלי האחרונות (גם כמה במקביל), ומחליף אותן בהערה קצרה. כך ההיסטוריה כבר לא מפעילה את הסינון.
    3. לפני שהוא משנה, הוא שומר גיבוי מלא (.bak-before-scrub) ואת הטקסט שהוסר (.removed.txt), וכותב בצורה בטוחה (קובץ זמני ואז החלפה).
    4. הגנות: לא נוגע בצ'אט שכתבו בו ב-45 השניות האחרונות, רק בצ'אטים שהשתנו בשעתיים האחרונות, ועד 3 תיקונים בכל הרצה, כדי לא "להעיר" צ'אטים ישנים. הוא שומר את תאריך השינוי המקורי של הקובץ. אם החסימה היא לא אחרי תוצאת כלי, הוא רק רושם ביומן "דרוש טיפול ידני".
    5. הוא כותב ליומן ~/.claude/logs/netfree_recover.log, ואפשר להריץ אותו עם --dry-run כדי לראות מה היה מתוקן בלי לשנות כלום.

    קודם כל תריצו עם --dry-run:

    python netfree_recover.py --dry-run
    

    להרצה אוטומטית כל 2 דקות (חלון CMD או PowerShell, לא Git Bash), עם pythonw כדי שלא יקפוץ חלון. את הנתיבים להתאים אליכם (where pythonw מראה איפה הוא):

    schtasks /create /tn NetfreeRecover /sc minute /mo 2 /f /tr "\"C:\Users\<שם המשתמש>\AppData\Local\Programs\Python\Python312\pythonw.exe\" -I \"C:\Users\<שם המשתמש>\.claude\scripts\netfree_recover.py\""
    

    לביטול: schtasks /delete /tn NetfreeRecover /f

    הקוד המלא

    (לשמור כ-netfree_recover.py בתיקייה .claude\scripts שבתיקיית המשתמש. פייתון 3, בלי התקנת חבילות.)

    # -*- coding: utf-8 -*-
    """מזהה צ'אט של Claude Code שנחסם בנטפרי (API Error 418) ומתקן אותו.
    
    הרצה חד-פעמית (מתוזמנת כל ~2 דקות). לכל קובץ תמליל אחרון-לאחרונה:
      1. "נחסם" = שורת השיחה האחרונה היא assistant שטקסטה מתחיל ב-API Error: 418.
      2. הולכים אחורה (מדלגים על 418 ועל הודעות משתמש רגילות) עד שורת user עם תוצאות כלי:
         זו הרעל. אם נתקלים קודם בתשובה רגילה של Claude -> רק יומן "דרוש טיפול ידני".
      3. כל תוצאות הכלי באותה קבוצת שורות (גם במקביל, גם תמונות) מוחלפות בהערה,
         ו-toolUseResult מוסר. שאר השורות נשארות כפי שהן (בית-בבית).
      4. גיבוי פעם אחת (.bak-before-scrub), הטקסט שהוסר ל-.removed.txt, כתיבה אטומית.
    
    שימוש: pythonw netfree_recover.py [--dir D] [--dry-run] [--min-age S] [--max-age-hours H] [--log-dir L]
    """
    import argparse
    import glob
    import json
    import os
    import re
    import shutil
    import sys
    import time
    
    HOME = os.path.expanduser('~')
    DEFAULT_DIR = os.path.join(HOME, '.claude', 'projects')
    DEFAULT_LOG_DIR = os.path.join(HOME, '.claude', 'logs')
    NOTE = "[הקטע הוסר מהצ'אט כי נטפרי חסם אותו (418). לא לקרוא שוב את אותו טווח בספר.]"
    ERR_RE = re.compile(r'^\s*API Error:\s*418\b')
    LOCK_STALE_SECONDS = 600
    
    
    def say(msg, enabled):
        if enabled and sys.stdout:
            try:
                print(msg)
            except Exception:
                pass
    
    
    class Logger:
        def __init__(self, log_dir, dry_run, echo):
            self.path = os.path.join(log_dir, 'netfree_recover.log')
            self.prefix = '[dry-run] ' if dry_run else ''
            self.echo = echo
    
        def write(self, text):
            line = '%s %s%s' % (time.strftime('%Y-%m-%d %H:%M:%S'), self.prefix, text)
            try:
                with open(self.path, 'a', encoding='utf-8') as f:
                    f.write(line + '\n')
            except Exception:
                pass
            say(line, self.echo)
    
    
    # ---------- ניתוח שורות ----------
    
    def parse(line):
        try:
            row = json.loads(line.decode('utf-8'))
        except Exception:
            return None
        return row if isinstance(row, dict) else None
    
    
    def content_of(row):
        msg = row.get('message')
        return msg.get('content') if isinstance(msg, dict) else None
    
    
    def assistant_text(row):
        c = content_of(row)
        if isinstance(c, str):
            return c
        if isinstance(c, list):
            return ' '.join(b.get('text', '') for b in c
                            if isinstance(b, dict) and b.get('type') == 'text')
        return ''
    
    
    def is_418(row):
        return row.get('type') == 'assistant' and (
            bool(ERR_RE.match(assistant_text(row))) or row.get('apiErrorStatus') == 418)
    
    
    def result_blocks(row):
        c = content_of(row)
        if row.get('type') != 'user' or not isinstance(c, list):
            return []
        return [b for b in c if isinstance(b, dict) and b.get('type') == 'tool_result']
    
    
    def block_text(block):
        c = block.get('content')
        if isinstance(c, str):
            return c
        parts = []
        for x in c or []:
            if not isinstance(x, dict):
                continue
            if x.get('type') == 'text':
                parts.append(x.get('text', ''))
            elif x.get('type') == 'image':
                parts.append('[תמונה]')
        return '\n'.join(parts)
    
    
    def already_note(block):
        return block_text(block).strip() == NOTE
    
    
    def analyse(lines):
        """מחזיר (kind, info). kind: ok | skip | manual | already | poison."""
        idx = [i for i, l in enumerate(lines) if l.strip()]
        if not idx:
            return 'ok', None
        last_nonempty = parse(lines[idx[-1]])
        if last_nonempty is None:
            return 'skip', 'שורה אחרונה לא תקינה (אולי באמצע כתיבה)'
        # שורת השיחה האחרונה
        conv = None
        rows = {}
        for i in reversed(idx):
            r = parse(lines[i])
            rows[i] = r
            if r and r.get('type') in ('user', 'assistant'):
                conv = i
                break
        if conv is None or not is_418(rows[conv]):
            return 'ok', None
        # הליכה אחורה אל שורת הרעל
        i = conv
        poison = None
        while i >= 0:
            i -= 1
            if i < 0 or not lines[i].strip():
                continue
            r = parse(lines[i])
            if not r or r.get('type') not in ('user', 'assistant'):
                continue
            if r['type'] == 'assistant':
                if is_418(r):
                    continue
                return 'manual', i
            if result_blocks(r):
                poison = i
                break
            # הודעת משתמש רגילה ("נסה שוב") - ממשיכים אחורה
        if poison is None:
            return 'manual', -1
        # קבוצה רציפה של שורות user עם תוצאות כלי (תוצאות מקבילות בשורות נפרדות)
        group = [poison]
        j = poison
        while j > 0:
            j -= 1
            if not lines[j].strip():
                continue
            r = parse(lines[j])
            if not r or r.get('type') not in ('user', 'assistant'):
                continue
            if r['type'] == 'user' and result_blocks(r):
                group.append(j)
                continue
            break
        group.sort()
        if all(already_note(b) for g in group for b in result_blocks(parse(lines[g]))):
            return 'already', poison
        return 'poison', group
    
    
    # ---------- תיקון ----------
    
    def scrub_lines(lines, group):
        """מחזיר (שורות חדשות, כמה תוצאות, כמה תווים, טקסט שהוסר)."""
        out = list(lines)
        n_results = 0
        n_chars = 0
        removed = []
        for g in group:
            row = parse(lines[g])
            changed = False
            for b in result_blocks(row):
                if already_note(b):
                    continue
                text = block_text(b)
                n_results += 1
                n_chars += len(text)
                removed.append('--- שורה %d, tool_use_id=%s ---\n%s' % (g, b.get('tool_use_id'), text))
                b['content'] = NOTE
                changed = True
            if 'toolUseResult' in row:
                del row['toolUseResult']
                changed = True
            if changed:
                ending = b'\r\n' if lines[g].endswith(b'\r\n') else (b'\n' if lines[g].endswith(b'\n') else b'')
                out[g] = json.dumps(row, ensure_ascii=False, separators=(',', ':')).encode('utf-8') + ending
        return out, n_results, n_chars, '\n\n'.join(removed)
    
    
    def stat_sig(path):
        st = os.stat(path)
        return st.st_mtime_ns, st.st_size
    
    
    def process(path, args, log, state):
        name = os.path.basename(path)
        sig0 = stat_sig(path)
        with open(path, 'rb') as f:
            data = f.read()
        if stat_sig(path) != sig0:
            return  # משתנה תוך כדי קריאה
        lines = data.splitlines(keepends=True)
        kind, info = analyse(lines)
        if kind in ('ok', 'skip'):
            state.pop(path, None)
            return
        if kind in ('manual', 'already'):
            signature = '%s:%s' % (kind, info)
            if state.get(path) == signature:
                return
            if kind == 'already' and state.get(path) == 'fixed:%d:%d' % (info, sig0[1]):
                return  # זה הקובץ שרק תיקנו ועוד לא נעשה בו ניסיון חוזר
            if kind == 'manual':
                log.write('%s | נחסם (418) אבל לא אחרי תוצאת כלי (שורה %s) | דרוש טיפול ידני, לא שונה'
                          % (name, info + 1 if info >= 0 else '?'))
            else:
                log.write('%s | שורה %d כבר הוחלפה בהערה והשגיאה נמשכת | לא שונה'
                          % (name, info + 1))
            if not args.dry_run:
                state[path] = signature
            return
        # poison
        group = info
        new_lines, n_results, n_chars, removed = scrub_lines(lines, group)
        where = ','.join(str(g + 1) for g in group)
        if args.fixed >= args.max_fixes:
            if args.fixed == args.max_fixes:
                args.fixed += 1
                log.write('יותר מ-%d צ\'אטים חסומים בבת אחת, לא נוגע בשאר (כדי לא להעיר צ\'אטים ישנים). '
                          'אפשר להריץ ידנית עם --max-fixes גבוה יותר' % args.max_fixes)
            return
        args.fixed += 1
        if args.dry_run:
            log.write('%s | היה מתוקן: שורות %s | %d תוצאות | %d תווים' % (name, where, n_results, n_chars))
            return
        backup = path + '.bak-before-scrub'
        if not os.path.exists(backup):
            shutil.copy2(path, backup)
        with open(path + '.removed.txt', 'a', encoding='utf-8') as f:
            f.write('===== %s =====\n%s\n\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), removed))
        tmp = path + '.scrub-tmp'
        with open(tmp, 'wb') as f:
            f.write(b''.join(new_lines))
        if stat_sig(path) != sig0:  # מישהו כתב בינתיים
            os.remove(tmp)
            log.write('%s | הקובץ השתנה בזמן העבודה, ננסה ברוץ הבא' % name)
            return
        try:
            os.replace(tmp, path)
            # שומרים את תאריך השינוי המקורי: הצ'אט לא "קופץ" לראש הרשימה כאילו נעשה בו שימוש
            os.utime(path, ns=(sig0[0], sig0[0]))
        except OSError as e:
            try:
                os.remove(tmp)
            except OSError:
                pass
            log.write('%s | הקובץ נעול (%s), ננסה ברוץ הבא' % (name, e.__class__.__name__))
            return
        state[path] = 'fixed:%d:%d' % (group[-1], os.path.getsize(path))
        log.write('%s | תוקן: שורות %s | %d תוצאות | %d תווים' % (name, where, n_results, n_chars))
    
    
    # ---------- ראשי ----------
    
    def load_state(path):
        try:
            with open(path, encoding='utf-8') as f:
                d = json.load(f)
            return d if isinstance(d, dict) else {}
        except Exception:
            return {}
    
    
    def save_state(path, state):
        tmp = path + '.tmp'
        with open(tmp, 'w', encoding='utf-8') as f:
            json.dump(state, f, ensure_ascii=False, indent=1)
        os.replace(tmp, path)
    
    
    def acquire_lock(path):
        try:
            if os.path.exists(path) and time.time() - os.path.getmtime(path) > LOCK_STALE_SECONDS:
                os.remove(path)
            fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
            os.close(fd)
            return True
        except OSError:
            return False
    
    
    def main(argv=None):
        ap = argparse.ArgumentParser()
        ap.add_argument('--dir', default=DEFAULT_DIR)
        ap.add_argument('--dry-run', action='store_true')
        ap.add_argument('--min-age', type=float, default=45)
        ap.add_argument('--max-age-hours', type=float, default=2)
        ap.add_argument('--max-fixes', type=int, default=3)
        ap.add_argument('--log-dir', default=DEFAULT_LOG_DIR)
        args = ap.parse_args(argv)
        args.fixed = 0
    
        os.makedirs(args.log_dir, exist_ok=True)
        log = Logger(args.log_dir, args.dry_run, echo=args.dry_run)
        lock = os.path.join(args.log_dir, 'netfree_recover.lock')
        if not acquire_lock(lock):
            return 0
        state_path = os.path.join(args.log_dir, 'netfree_recover_state.json')
        state = load_state(state_path)
        try:
            now = time.time()
            # קבצי הצ'אט הראשיים וגם תמלילי סוכני העזר (<צ'אט>/subagents/agent-*.jsonl)
            paths = glob.glob(os.path.join(args.dir, '*', '*.jsonl'))
            paths += glob.glob(os.path.join(args.dir, '*', '*', 'subagents', 'agent-*.jsonl'))
            for path in paths:
                try:
                    age = now - os.path.getmtime(path)
                    if age < args.min_age or age > args.max_age_hours * 3600:
                        continue
                    process(path, args, log, state)
                except Exception as e:
                    log.write('%s | שגיאה: %s: %s' % (os.path.basename(path), e.__class__.__name__, e))
            state = {p: v for p, v in state.items() if os.path.exists(p)}
            if not args.dry_run:
                save_state(state_path, state)
        finally:
            try:
                os.remove(lock)
            except OSError:
                pass
        return 0
    
    
    if __name__ == '__main__':
        sys.exit(main())
    

    מגבלות, בכנות

    • נבדק על תמלילים מומצאים (33 בדיקות, כולל כמה תוצאות במקביל ותמונות), והורץ בפועל אצלי על שני קבצי צ'אט אמיתיים (של סוכני עזר). לא נבדק על גרסאות אחרות של Claude Code. הוא תלוי בפורמט קבצי הצ'אט של Claude Code, ואם הפורמט ישתנה הוא עלול להפסיק לעבוד.
    • צ'אט שפתוח כרגע בחלון חי לא תמיד חוזר לפעול גם אחרי התיקון, כי כנראה ההיסטוריה הישנה נשארת בזיכרון שלו. אצלי, אחרי תיקון ידני באותה שיטה, צ'אט אחד חזר לפעול וצ'אט אחד לא. במקרה כזה פותחים צ'אט חדש (לא "המשך") וממשיכים מנקודת העצירה.
    • הסקריפט משנה את קבצי ההיסטוריה של הצ'אטים. הגיבוי נשמר לידם, אבל האחריות עליכם.
    • Claude "ישכח" מה היה בתוצאה שהוסרה. ההערה שנכתבה במקומה אומרת לו לא לקרוא שוב את אותו טווח.
    • הוא לא מונע חסימות, הוא רק מתקן אחרי. המניעה היא במה שנכנס להקשר.

    אשמח מאוד אם למישהו יש שיטה יעילה יותר להתמודד עם זה, או תיקונים והערות לסקריפט. שימו לב: מה שכתבתי כאן הוא ניקוי של ההיסטוריה המקומית ומניעה של מה שנכנס להקשר, ולא עקיפה או פריצה של הסינון עצמו.

    YAHBDKY תגובה 1 תגובה אחרונה
    5
    • א אוויר

      שלום לכולם,

      הודעה זו והסקריפטים שבה נכתבו על ידי Claude (הבינה המלאכותית של Anthropic), לבקשתי ובהנחייתי, ונבדקו כמתואר בסוף ההודעה.

      למי ש-Claude Code (או Claude בדפדפן) "נתקע" אצלו בנטפרי עם API Error: 418, או JSON Parse error: Unexpected EOF, ואחרי "נסה שוב" זה חוזר שוב ושוב. הנה מה שלמדתי, ומה שבניתי כדי שלא אצטרך לתקן ידנית.

      לא יודעים איך להתקין? תנו ל-Claude Code לעשות את זה

      אם אתם לא יודעים איך להתקין את הסקריפט וה-hook שבהודעה, פשוט העתיקו את הקישור להודעה הזו ל-Claude Code, והוא יתאים אותם למחשב שלכם. זו ההנחיה שאפשר לתת לו:

      קרא את ההודעה בקישור הזה: https://mitmachim.top/topic/103008
      
      התקן אצלי את שני הדברים שמופיעים בה: (1) הסקריפט netfree_recover.py ו-(2) ה-hook size_guard.py. התאם אותם למחשב שלי (נתיב פייתון, שם המשתמש, תיקיית .claude).
      
      כללים:
      - לפני כל שינוי תסביר לי בקצרה מה אתה הולך לעשות.
      - קודם גבה את הקובץ settings.json. את ה-hooks למזג לקובץ הקיים, בלי למחוק הגדרות שכבר יש לי.
      - את הסקריפט להריץ קודם עם --dry-run ולהראות לי את התוצאה.
      - את המשימה המתוזמנת (schtasks) אל תרשום בעצמך. תן לי את הפקודה המתאימה ואני אריץ אותה.
      - אל תשנה אף קובץ של צ'אט אמיתי בלי לשאול אותי.
      - אם אתה לא מצליח לפתוח את הקישור, תגיד לי, ואדביק לך את תוכן ההודעה.
      

      שימו לב: Claude Code (או האפליקציה) עלול לבקש אישור, או אפילו לסרב, לכתוב סקריפט שנוגע בקבצי ההיסטוריה של הצ'אטים. זה צפוי, ואפשר לאשר בעצמכם. לא נבדק על מחשבים אחרים.

      למה זה קורה

      בכל הודעה שאתם שולחים, כל היסטוריית הצ'אט נשלחת מחדש ל-api.anthropic.com, ונטפרי סורק אותה. אם קטע אחד בהיסטוריה (בדרך כלל תוצאה של כלי: קובץ שנקרא, דף, תמונה) הפעיל את הסינון, הוא ימשיך להפעיל אותו בכל הודעה אחריו. לכן "נסה שוב" לא עוזר, ולכן גם צ'אט חדש שנפתח כ"המשך" או מסיכום של הישן עלול לרשת את הבעיה.

      מה מגדיל את הסיכון

      • קריאת תמונות (גם תמימות) וצילומי מסך. התמונה נכנסת כ-base64 ונסרקת.
      • קריאה של קובץ או ספר גדול בבת אחת, במיוחד עם הרבה טקסט רגיש.
      • פלטים ארוכים של כלים.
      • הדבקת דף החסימה של נטפרי לתוך הצ'אט. הוא נשאר בהיסטוריה וממשיך להרעיל.

      מניעה (עובד אצלי)

      • לקרוא במנות קטנות (עד כמה עשרות שורות), לא פרקים שלמים.
      • לעבד תוכן רגיש בסקריפט מקומי, ולהביא ל-Claude רק סיכום.
      • לא לצלם ולא להדביק דפי חסימה. לתאר אותם במילים.
      • וכדי שלא אצטרך לזכור את זה בעצמי, יש לי hook אוטומטי (ההסבר והקוד מיד למטה).

      Hook שמונע מראש כניסה של בלוקים גדולים

      hook הוא סקריפט קטן ש-Claude Code מריץ אוטומטית לפני ואחרי כל כלי. אני משתמש בו כשומר גודל:

      • לפני קריאה: אם Claude מנסה לקרוא קובץ גדול (מעל כ-60KB) בבת אחת, ה-hook חוסם ואומר לו לקרוא בחלקים (offset ו-limit), לחפש בקובץ, או לעבד אותו בסקריפט מקומי.
      • לפני כתיבה: כתיבה של יותר מ-30,000 תווים בבת אחת נחסמת, וצריך לכתוב שלד ולהוסיף בחלקים. גם פלט גדול שנשלח אל נטפרי יכול לגרום לחסימה.
      • אחרי כלי: אם התוצאה גדולה מ-25,000 תווים, הוא מוסיף ל-Claude תזכורת לעבוד במנות קטנות.
      • אם הסקריפט עצמו נכשל, הוא לא חוסם כלום.

      אפשר לשנות את שלושת המספרים שבראש הקובץ.

      איך מתקינים:

      1. לשמור את הקוד למטה כ-size_guard.py בתיקייה .claude\hooks שבתיקיית המשתמש.
      2. להוסיף ל-.claude\settings.json (אם כבר יש שם "hooks" אז למזג, לא להכפיל):
      "hooks": {
        "PreToolUse": [
          { "matcher": "Read|Write",
            "hooks": [ { "type": "command",
              "command": "\"C:\\Users\\<שם המשתמש>\\AppData\\Local\\Programs\\Python\\Python312\\python.exe\" \"C:\\Users\\<שם המשתמש>\\.claude\\hooks\\size_guard.py\"",
              "timeout": 10 } ] }
        ],
        "PostToolUse": [
          { "hooks": [ { "type": "command",
              "command": "\"C:\\Users\\<שם המשתמש>\\AppData\\Local\\Programs\\Python\\Python312\\python.exe\" \"C:\\Users\\<שם המשתמש>\\.claude\\hooks\\size_guard.py\"",
              "timeout": 10 } ] }
        ]
      }
      
      1. לפתוח צ'אט חדש. ב-Claude Code אפשר לראות ב-/hooks אם הוא נטען.
      """שומר גודל: מונע כניסה ויציאה של טקסט גדול מדי מההקשר (נטפרי חוסם צ'אט שמעביר בלוקים גדולים).
      
      PreToolUse  : חוסם קריאה גדולה (Read בלי limit לקובץ גדול) וכתיבה גדולה (Write).
      PostToolUse : אם תוצאת כלי גדולה מדי - מזריק תזכורת לעבוד במנות ובסקריפט מקומי.
      כשל בסקריפט לא חוסם כלום (יוצאים בשקט).
      """
      import json
      import os
      import sys
      
      MAX_READ_FILE_BYTES = 60_000  # Read בלי limit: מעל זה חוסמים
      MAX_WRITE_CHARS = 30_000    # Write: מעל זה מפצלים
      MAX_RESULT_CHARS = 25_000   # תוצאת כלי: מעל זה מזכירים
      
      SKIP_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf", ".ipynb"}
      
      
      def deny(reason):
          print(json.dumps({"hookSpecificOutput": {
              "hookEventName": "PreToolUse",
              "permissionDecision": "deny",
              "permissionDecisionReason": reason}}, ensure_ascii=False))
          sys.exit(0)
      
      
      def remind(text):
          print(json.dumps({"hookSpecificOutput": {
              "hookEventName": "PostToolUse",
              "additionalContext": text}}, ensure_ascii=False))
          sys.exit(0)
      
      
      def main():
          sys.stdout.reconfigure(encoding="utf-8")
          data = json.loads(sys.stdin.buffer.read().decode("utf-8"))
          event = data.get("hook_event_name", "")
          name = data.get("tool_name", "")
          inp = data.get("tool_input") or {}
      
          if event == "PreToolUse":
              if name == "Read":
                  path = inp.get("file_path", "")
                  if (inp.get("limit") is None and not inp.get("allow_large")
                          and os.path.splitext(path)[1].lower() not in SKIP_EXT
                          and os.path.isfile(path) and os.path.getsize(path) > MAX_READ_FILE_BYTES):
                      deny(f"הקובץ גדול ({os.path.getsize(path) // 1000}KB). לקרוא בחלקים עם offset ו-limit, "
                           "או לחפש בו עם Grep, או לעבד אותו בסקריפט מקומי ולהביא רק סיכום.")
              elif name == "Write":
                  if len(inp.get("content", "")) > MAX_WRITE_CHARS:
                      deny(f"כתיבה גדולה מדי בבת אחת (מעל {MAX_WRITE_CHARS} תווים). "
                           "לכתוב שלד קטן ולהוסיף בחלקים עם Edit, או לבנות את הקובץ בסקריפט.")
          elif event == "PostToolUse":
              size = len(json.dumps(data.get("tool_response", ""), ensure_ascii=False))
              if size > MAX_RESULT_CHARS:
                  remind(f"תזכורת: תוצאת הכלי האחרון הייתה {size // 1000}K תווים. פלט/קלט גדול עלול לגרום לנטפרי "
                         "לחסום את הצ'אט. מכאן: לקרוא ולהדפיס במנות קטנות, לעבד בסקריפט מקומי, "
                         "ולהביא להקשר רק את מה שצריך.")
      
      
      try:
          main()
      except SystemExit:
          raise
      except Exception:
          sys.exit(0)
      

      חשוב לדעת: ה-hook מצמצם את הסיכון, לא מבטל אותו. גם קובץ קטן יכול להפעיל את הסינון אם התוכן שלו רגיש. את הגרסה הכללית שבהודעה (בלי החלק שלי לקריאה מספרייה מסוימת) בדקתי עכשיו עם קלטים מדומים: קובץ גדול נחסם, קריאה עם limit עוברת, פלט גדול מקבל תזכורת, וכתיבה גדולה נחסמת.

      התיקון: סקריפט שמזהה צ'אט חסום ומחליף את הקטע המרעיל

      Claude Code שומר כל צ'אט כקובץ מקומי (~/.claude/projects/<פרויקט>/<מזהה>.jsonl), והקובץ הזה לא עובר דרך נטפרי. הסקריפט:

      1. מזהה צ'אט שהשורה האחרונה בו היא API Error: 418.
      2. הולך אחורה אל תוצאות הכלי האחרונות (גם כמה במקביל), ומחליף אותן בהערה קצרה. כך ההיסטוריה כבר לא מפעילה את הסינון.
      3. לפני שהוא משנה, הוא שומר גיבוי מלא (.bak-before-scrub) ואת הטקסט שהוסר (.removed.txt), וכותב בצורה בטוחה (קובץ זמני ואז החלפה).
      4. הגנות: לא נוגע בצ'אט שכתבו בו ב-45 השניות האחרונות, רק בצ'אטים שהשתנו בשעתיים האחרונות, ועד 3 תיקונים בכל הרצה, כדי לא "להעיר" צ'אטים ישנים. הוא שומר את תאריך השינוי המקורי של הקובץ. אם החסימה היא לא אחרי תוצאת כלי, הוא רק רושם ביומן "דרוש טיפול ידני".
      5. הוא כותב ליומן ~/.claude/logs/netfree_recover.log, ואפשר להריץ אותו עם --dry-run כדי לראות מה היה מתוקן בלי לשנות כלום.

      קודם כל תריצו עם --dry-run:

      python netfree_recover.py --dry-run
      

      להרצה אוטומטית כל 2 דקות (חלון CMD או PowerShell, לא Git Bash), עם pythonw כדי שלא יקפוץ חלון. את הנתיבים להתאים אליכם (where pythonw מראה איפה הוא):

      schtasks /create /tn NetfreeRecover /sc minute /mo 2 /f /tr "\"C:\Users\<שם המשתמש>\AppData\Local\Programs\Python\Python312\pythonw.exe\" -I \"C:\Users\<שם המשתמש>\.claude\scripts\netfree_recover.py\""
      

      לביטול: schtasks /delete /tn NetfreeRecover /f

      הקוד המלא

      (לשמור כ-netfree_recover.py בתיקייה .claude\scripts שבתיקיית המשתמש. פייתון 3, בלי התקנת חבילות.)

      # -*- coding: utf-8 -*-
      """מזהה צ'אט של Claude Code שנחסם בנטפרי (API Error 418) ומתקן אותו.
      
      הרצה חד-פעמית (מתוזמנת כל ~2 דקות). לכל קובץ תמליל אחרון-לאחרונה:
        1. "נחסם" = שורת השיחה האחרונה היא assistant שטקסטה מתחיל ב-API Error: 418.
        2. הולכים אחורה (מדלגים על 418 ועל הודעות משתמש רגילות) עד שורת user עם תוצאות כלי:
           זו הרעל. אם נתקלים קודם בתשובה רגילה של Claude -> רק יומן "דרוש טיפול ידני".
        3. כל תוצאות הכלי באותה קבוצת שורות (גם במקביל, גם תמונות) מוחלפות בהערה,
           ו-toolUseResult מוסר. שאר השורות נשארות כפי שהן (בית-בבית).
        4. גיבוי פעם אחת (.bak-before-scrub), הטקסט שהוסר ל-.removed.txt, כתיבה אטומית.
      
      שימוש: pythonw netfree_recover.py [--dir D] [--dry-run] [--min-age S] [--max-age-hours H] [--log-dir L]
      """
      import argparse
      import glob
      import json
      import os
      import re
      import shutil
      import sys
      import time
      
      HOME = os.path.expanduser('~')
      DEFAULT_DIR = os.path.join(HOME, '.claude', 'projects')
      DEFAULT_LOG_DIR = os.path.join(HOME, '.claude', 'logs')
      NOTE = "[הקטע הוסר מהצ'אט כי נטפרי חסם אותו (418). לא לקרוא שוב את אותו טווח בספר.]"
      ERR_RE = re.compile(r'^\s*API Error:\s*418\b')
      LOCK_STALE_SECONDS = 600
      
      
      def say(msg, enabled):
          if enabled and sys.stdout:
              try:
                  print(msg)
              except Exception:
                  pass
      
      
      class Logger:
          def __init__(self, log_dir, dry_run, echo):
              self.path = os.path.join(log_dir, 'netfree_recover.log')
              self.prefix = '[dry-run] ' if dry_run else ''
              self.echo = echo
      
          def write(self, text):
              line = '%s %s%s' % (time.strftime('%Y-%m-%d %H:%M:%S'), self.prefix, text)
              try:
                  with open(self.path, 'a', encoding='utf-8') as f:
                      f.write(line + '\n')
              except Exception:
                  pass
              say(line, self.echo)
      
      
      # ---------- ניתוח שורות ----------
      
      def parse(line):
          try:
              row = json.loads(line.decode('utf-8'))
          except Exception:
              return None
          return row if isinstance(row, dict) else None
      
      
      def content_of(row):
          msg = row.get('message')
          return msg.get('content') if isinstance(msg, dict) else None
      
      
      def assistant_text(row):
          c = content_of(row)
          if isinstance(c, str):
              return c
          if isinstance(c, list):
              return ' '.join(b.get('text', '') for b in c
                              if isinstance(b, dict) and b.get('type') == 'text')
          return ''
      
      
      def is_418(row):
          return row.get('type') == 'assistant' and (
              bool(ERR_RE.match(assistant_text(row))) or row.get('apiErrorStatus') == 418)
      
      
      def result_blocks(row):
          c = content_of(row)
          if row.get('type') != 'user' or not isinstance(c, list):
              return []
          return [b for b in c if isinstance(b, dict) and b.get('type') == 'tool_result']
      
      
      def block_text(block):
          c = block.get('content')
          if isinstance(c, str):
              return c
          parts = []
          for x in c or []:
              if not isinstance(x, dict):
                  continue
              if x.get('type') == 'text':
                  parts.append(x.get('text', ''))
              elif x.get('type') == 'image':
                  parts.append('[תמונה]')
          return '\n'.join(parts)
      
      
      def already_note(block):
          return block_text(block).strip() == NOTE
      
      
      def analyse(lines):
          """מחזיר (kind, info). kind: ok | skip | manual | already | poison."""
          idx = [i for i, l in enumerate(lines) if l.strip()]
          if not idx:
              return 'ok', None
          last_nonempty = parse(lines[idx[-1]])
          if last_nonempty is None:
              return 'skip', 'שורה אחרונה לא תקינה (אולי באמצע כתיבה)'
          # שורת השיחה האחרונה
          conv = None
          rows = {}
          for i in reversed(idx):
              r = parse(lines[i])
              rows[i] = r
              if r and r.get('type') in ('user', 'assistant'):
                  conv = i
                  break
          if conv is None or not is_418(rows[conv]):
              return 'ok', None
          # הליכה אחורה אל שורת הרעל
          i = conv
          poison = None
          while i >= 0:
              i -= 1
              if i < 0 or not lines[i].strip():
                  continue
              r = parse(lines[i])
              if not r or r.get('type') not in ('user', 'assistant'):
                  continue
              if r['type'] == 'assistant':
                  if is_418(r):
                      continue
                  return 'manual', i
              if result_blocks(r):
                  poison = i
                  break
              # הודעת משתמש רגילה ("נסה שוב") - ממשיכים אחורה
          if poison is None:
              return 'manual', -1
          # קבוצה רציפה של שורות user עם תוצאות כלי (תוצאות מקבילות בשורות נפרדות)
          group = [poison]
          j = poison
          while j > 0:
              j -= 1
              if not lines[j].strip():
                  continue
              r = parse(lines[j])
              if not r or r.get('type') not in ('user', 'assistant'):
                  continue
              if r['type'] == 'user' and result_blocks(r):
                  group.append(j)
                  continue
              break
          group.sort()
          if all(already_note(b) for g in group for b in result_blocks(parse(lines[g]))):
              return 'already', poison
          return 'poison', group
      
      
      # ---------- תיקון ----------
      
      def scrub_lines(lines, group):
          """מחזיר (שורות חדשות, כמה תוצאות, כמה תווים, טקסט שהוסר)."""
          out = list(lines)
          n_results = 0
          n_chars = 0
          removed = []
          for g in group:
              row = parse(lines[g])
              changed = False
              for b in result_blocks(row):
                  if already_note(b):
                      continue
                  text = block_text(b)
                  n_results += 1
                  n_chars += len(text)
                  removed.append('--- שורה %d, tool_use_id=%s ---\n%s' % (g, b.get('tool_use_id'), text))
                  b['content'] = NOTE
                  changed = True
              if 'toolUseResult' in row:
                  del row['toolUseResult']
                  changed = True
              if changed:
                  ending = b'\r\n' if lines[g].endswith(b'\r\n') else (b'\n' if lines[g].endswith(b'\n') else b'')
                  out[g] = json.dumps(row, ensure_ascii=False, separators=(',', ':')).encode('utf-8') + ending
          return out, n_results, n_chars, '\n\n'.join(removed)
      
      
      def stat_sig(path):
          st = os.stat(path)
          return st.st_mtime_ns, st.st_size
      
      
      def process(path, args, log, state):
          name = os.path.basename(path)
          sig0 = stat_sig(path)
          with open(path, 'rb') as f:
              data = f.read()
          if stat_sig(path) != sig0:
              return  # משתנה תוך כדי קריאה
          lines = data.splitlines(keepends=True)
          kind, info = analyse(lines)
          if kind in ('ok', 'skip'):
              state.pop(path, None)
              return
          if kind in ('manual', 'already'):
              signature = '%s:%s' % (kind, info)
              if state.get(path) == signature:
                  return
              if kind == 'already' and state.get(path) == 'fixed:%d:%d' % (info, sig0[1]):
                  return  # זה הקובץ שרק תיקנו ועוד לא נעשה בו ניסיון חוזר
              if kind == 'manual':
                  log.write('%s | נחסם (418) אבל לא אחרי תוצאת כלי (שורה %s) | דרוש טיפול ידני, לא שונה'
                            % (name, info + 1 if info >= 0 else '?'))
              else:
                  log.write('%s | שורה %d כבר הוחלפה בהערה והשגיאה נמשכת | לא שונה'
                            % (name, info + 1))
              if not args.dry_run:
                  state[path] = signature
              return
          # poison
          group = info
          new_lines, n_results, n_chars, removed = scrub_lines(lines, group)
          where = ','.join(str(g + 1) for g in group)
          if args.fixed >= args.max_fixes:
              if args.fixed == args.max_fixes:
                  args.fixed += 1
                  log.write('יותר מ-%d צ\'אטים חסומים בבת אחת, לא נוגע בשאר (כדי לא להעיר צ\'אטים ישנים). '
                            'אפשר להריץ ידנית עם --max-fixes גבוה יותר' % args.max_fixes)
              return
          args.fixed += 1
          if args.dry_run:
              log.write('%s | היה מתוקן: שורות %s | %d תוצאות | %d תווים' % (name, where, n_results, n_chars))
              return
          backup = path + '.bak-before-scrub'
          if not os.path.exists(backup):
              shutil.copy2(path, backup)
          with open(path + '.removed.txt', 'a', encoding='utf-8') as f:
              f.write('===== %s =====\n%s\n\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), removed))
          tmp = path + '.scrub-tmp'
          with open(tmp, 'wb') as f:
              f.write(b''.join(new_lines))
          if stat_sig(path) != sig0:  # מישהו כתב בינתיים
              os.remove(tmp)
              log.write('%s | הקובץ השתנה בזמן העבודה, ננסה ברוץ הבא' % name)
              return
          try:
              os.replace(tmp, path)
              # שומרים את תאריך השינוי המקורי: הצ'אט לא "קופץ" לראש הרשימה כאילו נעשה בו שימוש
              os.utime(path, ns=(sig0[0], sig0[0]))
          except OSError as e:
              try:
                  os.remove(tmp)
              except OSError:
                  pass
              log.write('%s | הקובץ נעול (%s), ננסה ברוץ הבא' % (name, e.__class__.__name__))
              return
          state[path] = 'fixed:%d:%d' % (group[-1], os.path.getsize(path))
          log.write('%s | תוקן: שורות %s | %d תוצאות | %d תווים' % (name, where, n_results, n_chars))
      
      
      # ---------- ראשי ----------
      
      def load_state(path):
          try:
              with open(path, encoding='utf-8') as f:
                  d = json.load(f)
              return d if isinstance(d, dict) else {}
          except Exception:
              return {}
      
      
      def save_state(path, state):
          tmp = path + '.tmp'
          with open(tmp, 'w', encoding='utf-8') as f:
              json.dump(state, f, ensure_ascii=False, indent=1)
          os.replace(tmp, path)
      
      
      def acquire_lock(path):
          try:
              if os.path.exists(path) and time.time() - os.path.getmtime(path) > LOCK_STALE_SECONDS:
                  os.remove(path)
              fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
              os.close(fd)
              return True
          except OSError:
              return False
      
      
      def main(argv=None):
          ap = argparse.ArgumentParser()
          ap.add_argument('--dir', default=DEFAULT_DIR)
          ap.add_argument('--dry-run', action='store_true')
          ap.add_argument('--min-age', type=float, default=45)
          ap.add_argument('--max-age-hours', type=float, default=2)
          ap.add_argument('--max-fixes', type=int, default=3)
          ap.add_argument('--log-dir', default=DEFAULT_LOG_DIR)
          args = ap.parse_args(argv)
          args.fixed = 0
      
          os.makedirs(args.log_dir, exist_ok=True)
          log = Logger(args.log_dir, args.dry_run, echo=args.dry_run)
          lock = os.path.join(args.log_dir, 'netfree_recover.lock')
          if not acquire_lock(lock):
              return 0
          state_path = os.path.join(args.log_dir, 'netfree_recover_state.json')
          state = load_state(state_path)
          try:
              now = time.time()
              # קבצי הצ'אט הראשיים וגם תמלילי סוכני העזר (<צ'אט>/subagents/agent-*.jsonl)
              paths = glob.glob(os.path.join(args.dir, '*', '*.jsonl'))
              paths += glob.glob(os.path.join(args.dir, '*', '*', 'subagents', 'agent-*.jsonl'))
              for path in paths:
                  try:
                      age = now - os.path.getmtime(path)
                      if age < args.min_age or age > args.max_age_hours * 3600:
                          continue
                      process(path, args, log, state)
                  except Exception as e:
                      log.write('%s | שגיאה: %s: %s' % (os.path.basename(path), e.__class__.__name__, e))
              state = {p: v for p, v in state.items() if os.path.exists(p)}
              if not args.dry_run:
                  save_state(state_path, state)
          finally:
              try:
                  os.remove(lock)
              except OSError:
                  pass
          return 0
      
      
      if __name__ == '__main__':
          sys.exit(main())
      

      מגבלות, בכנות

      • נבדק על תמלילים מומצאים (33 בדיקות, כולל כמה תוצאות במקביל ותמונות), והורץ בפועל אצלי על שני קבצי צ'אט אמיתיים (של סוכני עזר). לא נבדק על גרסאות אחרות של Claude Code. הוא תלוי בפורמט קבצי הצ'אט של Claude Code, ואם הפורמט ישתנה הוא עלול להפסיק לעבוד.
      • צ'אט שפתוח כרגע בחלון חי לא תמיד חוזר לפעול גם אחרי התיקון, כי כנראה ההיסטוריה הישנה נשארת בזיכרון שלו. אצלי, אחרי תיקון ידני באותה שיטה, צ'אט אחד חזר לפעול וצ'אט אחד לא. במקרה כזה פותחים צ'אט חדש (לא "המשך") וממשיכים מנקודת העצירה.
      • הסקריפט משנה את קבצי ההיסטוריה של הצ'אטים. הגיבוי נשמר לידם, אבל האחריות עליכם.
      • Claude "ישכח" מה היה בתוצאה שהוסרה. ההערה שנכתבה במקומה אומרת לו לא לקרוא שוב את אותו טווח.
      • הוא לא מונע חסימות, הוא רק מתקן אחרי. המניעה היא במה שנכנס להקשר.

      אשמח מאוד אם למישהו יש שיטה יעילה יותר להתמודד עם זה, או תיקונים והערות לסקריפט. שימו לב: מה שכתבתי כאן הוא ניקוי של ההיסטוריה המקומית ומניעה של מה שנכנס להקשר, ולא עקיפה או פריצה של הסינון עצמו.

      YAHBDKY מנותק
      YAHBDKY מנותק
      YAHBDK
      כתב נערך לאחרונה על ידי YAHBDK
      #2

      @אוויר

      אני משתמש הרבה בקלוד ולא נתקלתי אולי פעם אחת בשגיאה של נטפרי

      רעיון יפה

      תגובה 1 תגובה אחרונה
      0

      שלום! נראה שהשיחה הזו מעניינת אותך, אבל עדיין אין לך חשבון.

      נמאס לכם לגלול בין אותם הפוסטים בכל ביקור? כשנרשמים לחשבון, תמיד תחזרו בדיוק למקום שבו הייתם קודם, ותוכלו לבחור לקבל התראות על תגובות חדשות (בין אם במייל, ובין אם בהתראת פוש). תוכלו גם לשמור סימניות ולפרגן ב-upvote לפוסטים כדי להביע הערכה לחברי קהילה אחרים.

      בעזרת התרומה שלך, הפוסט הזה יכול להיות אפילו טוב יותר 💗

      הרשמה התחברות

      • התחברות

      • אין לך חשבון עדיין? הרשמה

      • התחברו או הירשמו כדי לחפש.
      • פוסט ראשון
        פוסט אחרון
      0
      • חוקי הפורום
      • פופולרי
      • לא נפתר
      • משתמשים
      • חיפוש גוגל בפורום
      • צור קשר