Protokoll #35
| ID | 35 |
|---|---|
| Zeitstempel | 2025-12-20 15:16:45.676028 |
| Client | root |
| IP | 145.224.96.190 |
| Modell | claude-sonnet-4-20250514 |
| Status | completed |
| Tokens | 9,186 (Input: 2,310, Output: 6,876) |
| Dauer | 9651 ms |
| Request-Zeit | 2025-12-20 15:16:45.676028 |
| Response-Zeit | 2025-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
}
}