Protokoll #35

ID35
Zeitstempel2025-12-20 15:16:45.676028
Clientroot
IP145.224.96.190
Modellclaude-sonnet-4-20250514
Statuscompleted
Tokens9,186 (Input: 2,310, Output: 6,876)
Dauer9651 ms
Request-Zeit2025-12-20 15:16:45.676028
Response-Zeit2025-12-20 15:16:55.326743

Request

{
    "event": "PreToolUse",
    "tool_name": "Edit",
    "tool_input": {
        "file_path": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/log_to_db.py",
        "old_string": "def get_assistant_response_from_session(session_id: str) -> Optional[str]:\n    \"\"\"Liest die letzte Assistant-Response aus der Session-JSONL\"\"\"\n    try:\n        # Claude Code speichert Sessions unter ~\/.claude\/projects\/\n        claude_dir = Path.home() \/ '.claude' \/ 'projects'\n\n        # Finde das richtige Projektverzeichnis (basierend auf cwd)\n        for project_dir in claude_dir.iterdir():\n            if not project_dir.is_dir():\n                continue\n\n            session_file = project_dir \/ f\"{session_id}.jsonl\"\n            if session_file.exists():\n                # Letzte Zeilen der JSONL lesen (rückwärts für Performance)\n                lines = session_file.read_text().strip().split('\\n')\n\n                # Suche letzte Assistant-Message (von hinten)\n                for line in reversed(lines[-50:]):  # Nur letzte 50 Zeilen\n                    try:\n                        entry = json.loads(line)\n                        if entry.get('type') == 'assistant':\n                            message = entry.get('message', {})\n                            # Extrahiere Text-Content aus der Message\n                            content_parts = message.get('content', [])\n                            text_parts = []\n                            for part in content_parts:\n                                if isinstance(part, dict) and part.get('type') == 'text':\n                                    text_parts.append(part.get('text', ''))\n                                elif isinstance(part, str):\n                                    text_parts.append(part)\n\n                            if text_parts:\n                                return sanitize_data('\\n'.join(text_parts))\n                    except json.JSONDecodeError:\n                        continue\n\n        return None\n\n    except Exception as e:\n        print(f\"Session read error: {e}\", file=sys.stderr)\n        return None\n\n\ndef close_pending_user_prompts(session_id: str, assistant_response: str) -> None:\n    \"\"\"Schließt offene UserPromptSubmit-Einträge mit der Assistant-Response\"\"\"\n    try:\n        connection = pymysql.connect(**DB_CONFIG)\n\n        with connection.cursor() as cursor:\n            current_time = datetime.datetime.now()\n            tokens_output = estimate_tokens(assistant_response)\n\n            response_json = json.dumps({\n                'assistant_response': assistant_response\n            }, ensure_ascii=False)\n\n            # Finde und update alle pending UserPromptSubmit-Einträge dieser Session\n            # (basierend auf client_name und timestamp der letzten Stunde)\n            sql = \"\"\"\n                UPDATE protokoll\n                SET response = %s,\n                    response_timestamp = %s,\n                    tokens_output = %s,\n                    tokens_total = tokens_input + %s,\n                    duration_ms = ROUND(TIMESTAMPDIFF(MICROSECOND, request_timestamp, %s) \/ 1000, 3),\n                    status = 'completed'\n                WHERE status = 'pending'\n                  AND JSON_EXTRACT(request, '$.event') = 'UserPromptSubmit'\n                  AND timestamp > DATE_SUB(%s, INTERVAL 1 HOUR)\n            \"\"\"\n\n            cursor.execute(sql, (\n                response_json,\n                current_time,\n                tokens_output,\n                tokens_output,\n                current_time,\n                current_time\n            ))\n\n            connection.commit()\n\n    except Exception as e:\n        print(f\"Close pending prompts error: {e}\", file=sys.stderr)\n    finally:\n        if 'connection' in locals():\n            connection.close()",
        "new_string": "def get_conversation_pairs_from_session(session_id: str) -> list:\n    \"\"\"Liest User-Prompt → Assistant-Response Paare aus der Session-JSONL\"\"\"\n    pairs = []\n    try:\n        claude_dir = Path.home() \/ '.claude' \/ 'projects'\n\n        for project_dir in claude_dir.iterdir():\n            if not project_dir.is_dir():\n                continue\n\n            session_file = project_dir \/ f\"{session_id}.jsonl\"\n            if session_file.exists():\n                lines = session_file.read_text().strip().split('\\n')\n\n                current_user_prompt = None\n                for line in lines:\n                    try:\n                        entry = json.loads(line)\n                        entry_type = entry.get('type', '')\n\n                        # User-Message gefunden\n                        if entry_type == 'human':\n                            message = entry.get('message', {})\n                            content = message.get('content', '')\n                            if isinstance(content, str) and content.strip():\n                                current_user_prompt = content.strip()\n\n                        # Assistant-Message gefunden - pair mit letztem User-Prompt\n                        elif entry_type == 'assistant' and current_user_prompt:\n                            message = entry.get('message', {})\n                            content_parts = message.get('content', [])\n                            text_parts = []\n                            for part in content_parts:\n                                if isinstance(part, dict) and part.get('type') == 'text':\n                                    text_parts.append(part.get('text', ''))\n                                elif isinstance(part, str):\n                                    text_parts.append(part)\n\n                            if text_parts:\n                                assistant_response = '\\n'.join(text_parts)\n                                pairs.append({\n                                    'prompt': current_user_prompt,\n                                    'response': sanitize_data(assistant_response)\n                                })\n                                current_user_prompt = None  # Reset für nächstes Paar\n\n                    except json.JSONDecodeError:\n                        continue\n\n                break  # Session gefunden, aufhören zu suchen\n\n        return pairs\n\n    except Exception as e:\n        print(f\"Session read error: {e}\", file=sys.stderr)\n        return []\n\n\ndef close_pending_user_prompts(session_id: str) -> None:\n    \"\"\"Schließt offene UserPromptSubmit-Einträge mit ihren zugehörigen Responses\"\"\"\n    try:\n        # Hole alle Conversation-Paare aus der Session\n        pairs = get_conversation_pairs_from_session(session_id)\n        if not pairs:\n            return\n\n        connection = pymysql.connect(**DB_CONFIG)\n\n        with connection.cursor() as cursor:\n            # Hole alle pending UserPromptSubmit-Einträge der letzten Stunde\n            cursor.execute(\"\"\"\n                SELECT id, JSON_UNQUOTE(JSON_EXTRACT(request, '$.prompt')) as prompt\n                FROM protokoll\n                WHERE status = 'pending'\n                  AND JSON_EXTRACT(request, '$.event') = 'UserPromptSubmit'\n                  AND timestamp > DATE_SUB(NOW(), INTERVAL 1 HOUR)\n                ORDER BY id ASC\n            \"\"\")\n            pending_prompts = cursor.fetchall()\n\n            current_time = datetime.datetime.now()\n\n            # Matche jeden pending Prompt mit seinem Response\n            for db_id, db_prompt in pending_prompts:\n                if not db_prompt:\n                    continue\n\n                # Finde das passende Paar (Prompt-Match)\n                matching_response = None\n                for pair in pairs:\n                    # Vergleiche die Prompts (normalisiert)\n                    if pair['prompt'].strip() == db_prompt.strip():\n                        matching_response = pair['response']\n                        break\n\n                if matching_response:\n                    tokens_output = estimate_tokens(matching_response)\n                    response_json = json.dumps({\n                        'assistant_response': matching_response\n                    }, ensure_ascii=False)\n\n                    cursor.execute(\"\"\"\n                        UPDATE protokoll\n                        SET response = %s,\n                            response_timestamp = %s,\n                            tokens_output = %s,\n                            tokens_total = tokens_input + %s,\n                            duration_ms = ROUND(TIMESTAMPDIFF(MICROSECOND, request_timestamp, %s) \/ 1000, 3),\n                            status = 'completed'\n                        WHERE id = %s\n                    \"\"\", (\n                        response_json,\n                        current_time,\n                        tokens_output,\n                        tokens_output,\n                        current_time,\n                        db_id\n                    ))\n\n            connection.commit()\n\n    except Exception as e:\n        print(f\"Close pending prompts error: {e}\", file=sys.stderr)\n    finally:\n        if 'connection' in locals():\n            connection.close()"
    }
}

Response

{
    "tool_response": {
        "filePath": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/log_to_db.py",
        "oldString": "def get_assistant_response_from_session(session_id: str) -> Optional[str]:\n    \"\"\"Liest die letzte Assistant-Response aus der Session-JSONL\"\"\"\n    try:\n        # Claude Code speichert Sessions unter ~\/.claude\/projects\/\n        claude_dir = Path.home() \/ '.claude' \/ 'projects'\n\n        # Finde das richtige Projektverzeichnis (basierend auf cwd)\n        for project_dir in claude_dir.iterdir():\n            if not project_dir.is_dir():\n                continue\n\n            session_file = project_dir \/ f\"{session_id}.jsonl\"\n            if session_file.exists():\n                # Letzte Zeilen der JSONL lesen (rückwärts für Performance)\n                lines = session_file.read_text().strip().split('\\n')\n\n                # Suche letzte Assistant-Message (von hinten)\n                for line in reversed(lines[-50:]):  # Nur letzte 50 Zeilen\n                    try:\n                        entry = json.loads(line)\n                        if entry.get('type') == 'assistant':\n                            message = entry.get('message', {})\n                            # Extrahiere Text-Content aus der Message\n                            content_parts = message.get('content', [])\n                            text_parts = []\n                            for part in content_parts:\n                                if isinstance(part, dict) and part.get('type') == 'text':\n                                    text_parts.append(part.get('text', ''))\n                                elif isinstance(part, str):\n                                    text_parts.append(part)\n\n                            if text_parts:\n                                return sanitize_data('\\n'.join(text_parts))\n                    except json.JSONDecodeError:\n                        continue\n\n        return None\n\n    except Exception as e:\n        print(f\"Session read error: {e}\", file=sys.stderr)\n        return None\n\n\ndef close_pending_user_prompts(session_id: str, assistant_response: str) -> None:\n    \"\"\"Schließt offene UserPromptSubmit-Einträge mit der Assistant-Response\"\"\"\n    try:\n        connection = pymysql.connect(**DB_CONFIG)\n\n        with connection.cursor() as cursor:\n            current_time = datetime.datetime.now()\n            tokens_output = estimate_tokens(assistant_response)\n\n            response_json = json.dumps({\n                'assistant_response': assistant_response\n            }, ensure_ascii=False)\n\n            # Finde und update alle pending UserPromptSubmit-Einträge dieser Session\n            # (basierend auf client_name und timestamp der letzten Stunde)\n            sql = \"\"\"\n                UPDATE protokoll\n                SET response = %s,\n                    response_timestamp = %s,\n                    tokens_output = %s,\n                    tokens_total = tokens_input + %s,\n                    duration_ms = ROUND(TIMESTAMPDIFF(MICROSECOND, request_timestamp, %s) \/ 1000, 3),\n                    status = 'completed'\n                WHERE status = 'pending'\n                  AND JSON_EXTRACT(request, '$.event') = 'UserPromptSubmit'\n                  AND timestamp > DATE_SUB(%s, INTERVAL 1 HOUR)\n            \"\"\"\n\n            cursor.execute(sql, (\n                response_json,\n                current_time,\n                tokens_output,\n                tokens_output,\n                current_time,\n                current_time\n            ))\n\n            connection.commit()\n\n    except Exception as e:\n        print(f\"Close pending prompts error: {e}\", file=sys.stderr)\n    finally:\n        if 'connection' in locals():\n            connection.close()",
        "newString": "def get_conversation_pairs_from_session(session_id: str) -> list:\n    \"\"\"Liest User-Prompt → Assistant-Response Paare aus der Session-JSONL\"\"\"\n    pairs = []\n    try:\n        claude_dir = Path.home() \/ '.claude' \/ 'projects'\n\n        for project_dir in claude_dir.iterdir():\n            if not project_dir.is_dir():\n                continue\n\n            session_file = project_dir \/ f\"{session_id}.jsonl\"\n            if session_file.exists():\n                lines = session_file.read_text().strip().split('\\n')\n\n                current_user_prompt = None\n                for line in lines:\n                    try:\n                        entry = json.loads(line)\n                        entry_type = entry.get('type', '')\n\n                        # User-Message gefunden\n                        if entry_type == 'human':\n                            message = entry.get('message', {})\n                            content = message.get('content', '')\n                            if isinstance(content, str) and content.strip():\n                                current_user_prompt = content.strip()\n\n                        # Assistant-Message gefunden - pair mit letztem User-Prompt\n                        elif entry_type == 'assistant' and current_user_prompt:\n                            message = entry.get('message', {})\n                            content_parts = message.get('content', [])\n                            text_parts = []\n                            for part in content_parts:\n                                if isinstance(part, dict) and part.get('type') == 'text':\n                                    text_parts.append(part.get('text', ''))\n                                elif isinstance(part, str):\n                                    text_parts.append(part)\n\n                            if text_parts:\n                                assistant_response = '\\n'.join(text_parts)\n                                pairs.append({\n                                    'prompt': current_user_prompt,\n                                    'response': sanitize_data(assistant_response)\n                                })\n                                current_user_prompt = None  # Reset für nächstes Paar\n\n                    except json.JSONDecodeError:\n                        continue\n\n                break  # Session gefunden, aufhören zu suchen\n\n        return pairs\n\n    except Exception as e:\n        print(f\"Session read error: {e}\", file=sys.stderr)\n        return []\n\n\ndef close_pending_user_prompts(session_id: str) -> None:\n    \"\"\"Schließt offene UserPromptSubmit-Einträge mit ihren zugehörigen Responses\"\"\"\n    try:\n        # Hole alle Conversation-Paare aus der Session\n        pairs = get_conversation_pairs_from_session(session_id)\n        if not pairs:\n            return\n\n        connection = pymysql.connect(**DB_CONFIG)\n\n        with connection.cursor() as cursor:\n            # Hole alle pending UserPromptSubmit-Einträge der letzten Stunde\n            cursor.execute(\"\"\"\n                SELECT id, JSON_UNQUOTE(JSON_EXTRACT(request, '$.prompt')) as prompt\n                FROM protokoll\n                WHERE status = 'pending'\n                  AND JSON_EXTRACT(request, '$.event') = 'UserPromptSubmit'\n                  AND timestamp > DATE_SUB(NOW(), INTERVAL 1 HOUR)\n                ORDER BY id ASC\n            \"\"\")\n            pending_prompts = cursor.fetchall()\n\n            current_time = datetime.datetime.now()\n\n            # Matche jeden pending Prompt mit seinem Response\n            for db_id, db_prompt in pending_prompts:\n                if not db_prompt:\n                    continue\n\n                # Finde das passende Paar (Prompt-Match)\n                matching_response = None\n                for pair in pairs:\n                    # Vergleiche die Prompts (normalisiert)\n                    if pair['prompt'].strip() == db_prompt.strip():\n                        matching_response = pair['response']\n                        break\n\n                if matching_response:\n                    tokens_output = estimate_tokens(matching_response)\n                    response_json = json.dumps({\n                        'assistant_response': matching_response\n                    }, ensure_ascii=False)\n\n                    cursor.execute(\"\"\"\n                        UPDATE protokoll\n                        SET response = %s,\n                            response_timestamp = %s,\n                            tokens_output = %s,\n                            tokens_total = tokens_input + %s,\n                            duration_ms = ROUND(TIMESTAMPDIFF(MICROSECOND, request_timestamp, %s) \/ 1000, 3),\n                            status = 'completed'\n                        WHERE id = %s\n                    \"\"\", (\n                        response_json,\n                        current_time,\n                        tokens_output,\n                        tokens_output,\n                        current_time,\n                        db_id\n                    ))\n\n            connection.commit()\n\n    except Exception as e:\n        print(f\"Close pending prompts error: {e}\", file=sys.stderr)\n    finally:\n        if 'connection' in locals():\n            connection.close()",
        "originalFile": "#!\/usr\/bin\/env python3\n\"\"\"\nClaude Code Hook Logger v2 für KI-Protokoll-Datenbank\nVerbesserte Version mit Request\/Response-Pairing und Duration-Tracking\n\"\"\"\n\nimport json\nimport os\nimport sys\nimport re\nimport datetime\nimport hashlib\nimport pymysql\nimport tempfile\nfrom pathlib import Path\nfrom typing import Dict, Any, Optional\n\n# .env aus Hook-Verzeichnis laden\nfrom dotenv import load_dotenv\nload_dotenv(Path(__file__).parent \/ '.env')\n\n# Konfiguration aus Environment-Variablen\nDB_CONFIG = {\n    'host': os.environ.get('CLAUDE_DB_HOST', 'localhost'),\n    'port': int(os.environ.get('CLAUDE_DB_PORT', '3306')),\n    'user': os.environ.get('CLAUDE_DB_USER', 'claude_code'),\n    'password': os.environ.get('CLAUDE_DB_PASSWORD', ''),\n    'database': os.environ.get('CLAUDE_DB_NAME', 'ki_protokoll'),\n    'charset': 'utf8mb4'\n}\n\n# Session-Tracking im temporären Verzeichnis\nTEMP_DIR = Path(tempfile.gettempdir()) \/ \"claude_hooks\"\nTEMP_DIR.mkdir(exist_ok=True)\n\n# Sicherheitseinstellungen\nMAX_FIELD_LENGTH = 10000\nSENSITIVE_KEY_PATTERNS = re.compile(r\"(?i)(password|pass|secret|token|apikey|api_key|authorization|auth|bearer|credential)\")\nSENSITIVE_VALUE_PATTERNS = [\n    re.compile(r\"(?i)\\bAKIA[0-9A-Z]{16}\\b\"),\n    re.compile(r\"(?i)\\b(?:sk|rk|pk)[0-9A-Za-z]{20,}\\b\"),\n    re.compile(r\"(?i)\\beyJ[a-zA-Z0-9-]{10,}\\.[a-zA-Z0-9_-]{10,}\\.[a-zA-Z0-9_-]{10,}\\b\")\n]\n\ndef get_client_ip() -> str:\n    \"\"\"Ermittelt die Client-IP-Adresse\"\"\"\n    ssh_client = os.environ.get('SSH_CLIENT', '')\n    if ssh_client:\n        return ssh_client.split()[0]\n    ssh_connection = os.environ.get('SSH_CONNECTION', '')\n    if ssh_connection:\n        return ssh_connection.split()[0]\n    return '127.0.0.1'\n\ndef sanitize_data(obj: Any) -> Any:\n    \"\"\"Entfernt oder maskiert sensible Daten\"\"\"\n    if isinstance(obj, dict):\n        result = {}\n        for key, value in obj.items():\n            if SENSITIVE_KEY_PATTERNS.search(str(key)):\n                result[key] = '[REDACTED]'\n            else:\n                result[key] = sanitize_data(value)\n        return result\n    elif isinstance(obj, list):\n        return [sanitize_data(item) for item in obj]\n    elif isinstance(obj, str):\n        for pattern in SENSITIVE_VALUE_PATTERNS:\n            if pattern.search(obj):\n                return '[REDACTED]'\n        if len(obj) > MAX_FIELD_LENGTH:\n            hash_value = hashlib.sha256(obj.encode('utf-8', errors='ignore')).hexdigest()[:16]\n            return obj[:MAX_FIELD_LENGTH] + f'... [TRUNCATED-{hash_value}]'\n        return obj\n    return obj\n\ndef estimate_tokens(text: str) -> int:\n    \"\"\"Grobe Token-Schätzung (4 Zeichen = 1 Token)\"\"\"\n    if not text:\n        return 0\n    return max(1, len(text) \/\/ 4)\n\ndef get_session_tracking_key(data: Dict[str, Any]) -> str:\n    \"\"\"Erstellt einen eindeutigen Key für Session-Tracking\"\"\"\n    session_id = data.get('session_id', '')\n    event_name = data.get('hook_event_name', '')\n    tool_name = data.get('tool_name', '')\n\n    # Für Tool-Events: tool_name-spezifischer Key\n    if event_name in ['PreToolUse', 'PostToolUse'] and tool_name:\n        return f\"{session_id}_{tool_name}_{event_name}\"\n\n    # Für andere Events: event-spezifischer Key\n    return f\"{session_id}_{event_name}\"\n\ndef save_pending_request(data: Dict[str, Any], db_id: int) -> None:\n    \"\"\"Speichert pending Request für spätere Response-Zuordnung\"\"\"\n    try:\n        key = get_session_tracking_key(data)\n        tracking_file = TEMP_DIR \/ f\"{key}.json\"\n\n        tracking_data = {\n            'db_id': db_id,\n            'timestamp': datetime.datetime.now().isoformat(),\n            'event': data.get('hook_event_name'),\n            'tool_name': data.get('tool_name', ''),\n            'session_id': data.get('session_id', '')\n        }\n\n        with open(tracking_file, 'w') as f:\n            json.dump(tracking_data, f)\n\n    except Exception as e:\n        print(f\"Session tracking save error: {e}\", file=sys.stderr)\n\ndef find_matching_request(data: Dict[str, Any]) -> Optional[int]:\n    \"\"\"Findet matching Request für Response-Event\"\"\"\n    try:\n        event_name = data.get('hook_event_name', '')\n\n        # PostToolUse sucht nach PreToolUse\n        if event_name == 'PostToolUse':\n            search_data = dict(data)\n            search_data['hook_event_name'] = 'PreToolUse'\n            key = get_session_tracking_key(search_data)\n        else:\n            return None\n\n        tracking_file = TEMP_DIR \/ f\"{key}.json\"\n\n        if tracking_file.exists():\n            with open(tracking_file, 'r') as f:\n                tracking_data = json.load(f)\n\n            # Cleanup\n            tracking_file.unlink()\n            return tracking_data['db_id']\n\n    except Exception as e:\n        print(f\"Session tracking find error: {e}\", file=sys.stderr)\n\n    return None\n\ndef update_request_with_response(db_id: int, response_data: str) -> None:\n    \"\"\"Updated existing Request mit Response-Daten und berechnet Duration\"\"\"\n    try:\n        connection = pymysql.connect(**DB_CONFIG)\n\n        with connection.cursor() as cursor:\n            current_time = datetime.datetime.now()\n\n            # Erst Response und Timestamp setzen\n            tokens_output = estimate_tokens(response_data)\n\n            sql = \"\"\"\n                UPDATE protokoll\n                SET response = %s,\n                    response_timestamp = %s,\n                    tokens_output = %s,\n                    tokens_total = tokens_input + %s,\n                    status = 'completed'\n                WHERE id = %s\n            \"\"\"\n\n            cursor.execute(sql, (\n                response_data,\n                current_time,\n                tokens_output,\n                tokens_output,\n                db_id\n            ))\n\n            # Dann Duration aus Timestamps berechnen und setzen (mit Microsekunden-Präzision)\n            duration_sql = \"\"\"\n                UPDATE protokoll\n                SET duration_ms = ROUND(TIMESTAMPDIFF(MICROSECOND, request_timestamp, response_timestamp) \/ 1000, 3)\n                WHERE id = %s\n            \"\"\"\n\n            cursor.execute(duration_sql, (db_id,))\n\n            connection.commit()\n\n    except Exception as e:\n        print(f\"Database update error: {e}\", file=sys.stderr)\n    finally:\n        if 'connection' in locals():\n            connection.close()\n\n\ndef get_assistant_response_from_session(session_id: str) -> Optional[str]:\n    \"\"\"Liest die letzte Assistant-Response aus der Session-JSONL\"\"\"\n    try:\n        # Claude Code speichert Sessions unter ~\/.claude\/projects\/\n        claude_dir = Path.home() \/ '.claude' \/ 'projects'\n\n        # Finde das richtige Projektverzeichnis (basierend auf cwd)\n        for project_dir in claude_dir.iterdir():\n            if not project_dir.is_dir():\n                continue\n\n            session_file = project_dir \/ f\"{session_id}.jsonl\"\n            if session_file.exists():\n                # Letzte Zeilen der JSONL lesen (rückwärts für Performance)\n                lines = session_file.read_text().strip().split('\\n')\n\n                # Suche letzte Assistant-Message (von hinten)\n                for line in reversed(lines[-50:]):  # Nur letzte 50 Zeilen\n                    try:\n                        entry = json.loads(line)\n                        if entry.get('type') == 'assistant':\n                            message = entry.get('message', {})\n                            # Extrahiere Text-Content aus der Message\n                            content_parts = message.get('content', [])\n                            text_parts = []\n                            for part in content_parts:\n                                if isinstance(part, dict) and part.get('type') == 'text':\n                                    text_parts.append(part.get('text', ''))\n                                elif isinstance(part, str):\n                                    text_parts.append(part)\n\n                            if text_parts:\n                                return sanitize_data('\\n'.join(text_parts))\n                    except json.JSONDecodeError:\n                        continue\n\n        return None\n\n    except Exception as e:\n        print(f\"Session read error: {e}\", file=sys.stderr)\n        return None\n\n\ndef close_pending_user_prompts(session_id: str, assistant_response: str) -> None:\n    \"\"\"Schließt offene UserPromptSubmit-Einträge mit der Assistant-Response\"\"\"\n    try:\n        connection = pymysql.connect(**DB_CONFIG)\n\n        with connection.cursor() as cursor:\n            current_time = datetime.datetime.now()\n            tokens_output = estimate_tokens(assistant_response)\n\n            response_json = json.dumps({\n                'assistant_response': assistant_response\n            }, ensure_ascii=False)\n\n            # Finde und update alle pending UserPromptSubmit-Einträge dieser Session\n            # (basierend auf client_name und timestamp der letzten Stunde)\n            sql = \"\"\"\n                UPDATE protokoll\n                SET response = %s,\n                    response_timestamp = %s,\n                    tokens_output = %s,\n                    tokens_total = tokens_input + %s,\n                    duration_ms = ROUND(TIMESTAMPDIFF(MICROSECOND, request_timestamp, %s) \/ 1000, 3),\n                    status = 'completed'\n                WHERE status = 'pending'\n                  AND JSON_EXTRACT(request, '$.event') = 'UserPromptSubmit'\n                  AND timestamp > DATE_SUB(%s, INTERVAL 1 HOUR)\n            \"\"\"\n\n            cursor.execute(sql, (\n                response_json,\n                current_time,\n                tokens_output,\n                tokens_output,\n                current_time,\n                current_time\n            ))\n\n            connection.commit()\n\n    except Exception as e:\n        print(f\"Close pending prompts error: {e}\", file=sys.stderr)\n    finally:\n        if 'connection' in locals():\n            connection.close()\n\n# Entfernt - Duration wird jetzt direkt in SQL berechnet\n\ndef log_to_database(data: Dict[str, Any]) -> Optional[int]:\n    \"\"\"Schreibt Protokoll-Eintra... [TRUNCATED-d068e9453f4144d8]",
        "structuredPatch": [
            {
                "oldStart": 188,
                "oldLines": 29,
                "newStart": 188,
                "newLines": 36,
                "lines": [
                    "             connection.close()",
                    " ",
                    " ",
                    "-def get_assistant_response_from_session(session_id: str) -> Optional[str]:",
                    "-    \"\"\"Liest die letzte Assistant-Response aus der Session-JSONL\"\"\"",
                    "+def get_conversation_pairs_from_session(session_id: str) -> list:",
                    "+    \"\"\"Liest User-Prompt → Assistant-Response Paare aus der Session-JSONL\"\"\"",
                    "+    pairs = []",
                    "     try:",
                    "-        # Claude Code speichert Sessions unter ~\/.claude\/projects\/",
                    "         claude_dir = Path.home() \/ '.claude' \/ 'projects'",
                    " ",
                    "-        # Finde das richtige Projektverzeichnis (basierend auf cwd)",
                    "         for project_dir in claude_dir.iterdir():",
                    "             if not project_dir.is_dir():",
                    "                 continue",
                    " ",
                    "             session_file = project_dir \/ f\"{session_id}.jsonl\"",
                    "             if session_file.exists():",
                    "-                # Letzte Zeilen der JSONL lesen (rückwärts für Performance)",
                    "                 lines = session_file.read_text().strip().split('\\n')",
                    " ",
                    "-                # Suche letzte Assistant-Message (von hinten)",
                    "-                for line in reversed(lines[-50:]):  # Nur letzte 50 Zeilen",
                    "+                current_user_prompt = None",
                    "+                for line in lines:",
                    "                     try:",
                    "                         entry = json.loads(line)",
                    "-                        if entry.get('type') == 'assistant':",
                    "+                        entry_type = entry.get('type', '')",
                    "+",
                    "+                        # User-Message gefunden",
                    "+                        if entry_type == 'human':",
                    "                             message = entry.get('message', {})",
                    "-                            # Extrahiere Text-Content aus der Message",
                    "+                            content = message.get('content', '')",
                    "+                            if isinstance(content, str) and content.strip():",
                    "+                                current_user_prompt = content.strip()",
                    "+",
                    "+                        # Assistant-Message gefunden - pair mit letztem User-Prompt",
                    "+                        elif entry_type == 'assistant' and current_user_prompt:",
                    "+                            message = entry.get('message', {})",
                    "                             content_parts = message.get('content', [])",
                    "                             text_parts = []",
                    "                             for part in content_parts:"
                ]
            },
            {
                "oldStart": 220,
                "oldLines": 54,
                "newStart": 227,
                "newLines": 86,
                "lines": [
                    "                                     text_parts.append(part)",
                    " ",
                    "                             if text_parts:",
                    "-                                return sanitize_data('\\n'.join(text_parts))",
                    "+                                assistant_response = '\\n'.join(text_parts)",
                    "+                                pairs.append({",
                    "+                                    'prompt': current_user_prompt,",
                    "+                                    'response': sanitize_data(assistant_response)",
                    "+                                })",
                    "+                                current_user_prompt = None  # Reset für nächstes Paar",
                    "+",
                    "                     except json.JSONDecodeError:",
                    "                         continue",
                    " ",
                    "-        return None",
                    "+                break  # Session gefunden, aufhören zu suchen",
                    " ",
                    "+        return pairs",
                    "+",
                    "     except Exception as e:",
                    "         print(f\"Session read error: {e}\", file=sys.stderr)",
                    "-        return None",
                    "+        return []",
                    " ",
                    " ",
                    "-def close_pending_user_prompts(session_id: str, assistant_response: str) -> None:",
                    "-    \"\"\"Schließt offene UserPromptSubmit-Einträge mit der Assistant-Response\"\"\"",
                    "+def close_pending_user_prompts(session_id: str) -> None:",
                    "+    \"\"\"Schließt offene UserPromptSubmit-Einträge mit ihren zugehörigen Responses\"\"\"",
                    "     try:",
                    "+        # Hole alle Conversation-Paare aus der Session",
                    "+        pairs = get_conversation_pairs_from_session(session_id)",
                    "+        if not pairs:",
                    "+            return",
                    "+",
                    "         connection = pymysql.connect(**DB_CONFIG)",
                    " ",
                    "         with connection.cursor() as cursor:",
                    "+            # Hole alle pending UserPromptSubmit-Einträge der letzten Stunde",
                    "+            cursor.execute(\"\"\"",
                    "+                SELECT id, JSON_UNQUOTE(JSON_EXTRACT(request, '$.prompt')) as prompt",
                    "+                FROM protokoll",
                    "+                WHERE status = 'pending'",
                    "+                  AND JSON_EXTRACT(request, '$.event') = 'UserPromptSubmit'",
                    "+                  AND timestamp > DATE_SUB(NOW(), INTERVAL 1 HOUR)",
                    "+                ORDER BY id ASC",
                    "+            \"\"\")",
                    "+            pending_prompts = cursor.fetchall()",
                    "+",
                    "             current_time = datetime.datetime.now()",
                    "-            tokens_output = estimate_tokens(assistant_response)",
                    " ",
                    "-            response_json = json.dumps({",
                    "-                'assistant_response': assistant_response",
                    "-            }, ensure_ascii=False)",
                    "+            # Matche jeden pending Prompt mit seinem Response",
                    "+            for db_id, db_prompt in pending_prompts:",
                    "+                if not db_prompt:",
                    "+                    continue",
                    " ",
                    "-            # Finde und update alle pending UserPromptSubmit-Einträge dieser Session",
                    "-            # (basierend auf client_name und timestamp der letzten Stunde)",
                    "-            sql = \"\"\"",
                    "-                UPDATE protokoll",
                    "-                SET response = %s,",
                    "-                    response_timestamp = %s,",
                    "-                    tokens_output = %s,",
                    "-                    tokens_total = tokens_input + %s,",
                    "-                    duration_ms = ROUND(TIMESTAMPDIFF(MICROSECOND, request_timestamp, %s) \/ 1000, 3),",
                    "-                    status = 'completed'",
                    "-                WHERE status = 'pending'",
                    "-                  AND JSON_EXTRACT(request, '$.event') = 'UserPromptSubmit'",
                    "-                  AND timestamp > DATE_SUB(%s, INTERVAL 1 HOUR)",
                    "-            \"\"\"",
                    "+                # Finde das passende Paar (Prompt-Match)",
                    "+                matching_response = None",
                    "+                for pair in pairs:",
                    "+                    # Vergleiche die Prompts (normalisiert)",
                    "+                    if pair['prompt'].strip() == db_prompt.strip():",
                    "+                        matching_response = pair['response']",
                    "+                        break",
                    " ",
                    "-            cursor.execute(sql, (",
                    "-                response_json,",
                    "-                current_time,",
                    "-                tokens_output,",
                    "-                tokens_output,",
                    "-                current_time,",
                    "-                current_time",
                    "-            ))",
                    "+                if matching_response:",
                    "+                    tokens_output = estimate_tokens(matching_response)",
                    "+                    response_json = json.dumps({",
                    "+                        'assistant_response': matching_response",
                    "+                    }, ensure_ascii=False)",
                    " ",
                    "+                    cursor.execute(\"\"\"",
                    "+                        UPDATE protokoll",
                    "+                        SET response = %s,",
                    "+                            response_timestamp = %s,",
                    "+                            tokens_output = %s,",
                    "+                            tokens_total = tokens_input + %s,",
                    "+                            duration_ms = ROUND(TIMESTAMPDIFF(MICROSECOND, request_timestamp, %s) \/ 1000, 3),",
                    "+                            status = 'completed'",
                    "+                        WHERE id = %s",
                    "+                    \"\"\", (",
                    "+                        response_json,",
                    "+                        current_time,",
                    "+                        tokens_output,",
                    "+                        tokens_output,",
                    "+                        current_time,",
                    "+                        db_id",
                    "+                    ))",
                    "+",
                    "             connection.commit()",
                    " ",
                    "     except Exception as e:"
                ]
            }
        ],
        "userModified": false,
        "replaceAll": false
    }
}
← Vorheriger Zur Liste Nächster →