import json import os from amc_server.context import EVENTS_DIR class ConversationMixin: def _serve_events(self, session_id): # Sanitize session_id to prevent path traversal safe_id = os.path.basename(session_id) event_file = EVENTS_DIR / f"{safe_id}.jsonl" events = [] if event_file.exists(): try: for line in event_file.read_text().splitlines(): if line.strip(): try: events.append(json.loads(line)) except json.JSONDecodeError: continue except OSError: pass self._send_json(200, {"session_id": safe_id, "events": events}) def _serve_conversation(self, session_id, project_dir, agent="claude"): """Serve conversation history from Claude Code or Codex JSONL file.""" safe_id = os.path.basename(session_id) messages = [] if agent == "codex": messages = self._parse_codex_conversation(safe_id) else: messages = self._parse_claude_conversation(safe_id, project_dir) self._send_json(200, {"session_id": safe_id, "messages": messages}) def _parse_claude_conversation(self, session_id, project_dir): """Parse Claude Code JSONL conversation format.""" messages = [] conv_file = self._get_claude_conversation_file(session_id, project_dir) if conv_file and conv_file.exists(): try: for line in conv_file.read_text().splitlines(): if not line.strip(): continue try: entry = json.loads(line) if not isinstance(entry, dict): continue msg_type = entry.get("type") if msg_type == "user": content = entry.get("message", {}).get("content", "") # Only include actual human messages (strings), not tool results (arrays) if content and isinstance(content, str): messages.append({ "role": "user", "content": content, "timestamp": entry.get("timestamp", ""), }) elif msg_type == "assistant": # Assistant messages have structured content message = entry.get("message", {}) if not isinstance(message, dict): continue raw_content = message.get("content", []) if not isinstance(raw_content, list): continue text_parts = [] tool_calls = [] thinking_parts = [] for part in raw_content: if isinstance(part, dict): ptype = part.get("type") if ptype == "text": text_parts.append(part.get("text", "")) elif ptype == "tool_use": tool_calls.append({ "name": part.get("name", "unknown"), "input": part.get("input", {}), }) elif ptype == "thinking": thinking_parts.append(part.get("thinking", "")) elif isinstance(part, str): text_parts.append(part) if text_parts or tool_calls or thinking_parts: msg = { "role": "assistant", "content": "\n".join(text_parts) if text_parts else "", "timestamp": entry.get("timestamp", ""), } if tool_calls: msg["tool_calls"] = tool_calls if thinking_parts: msg["thinking"] = "\n\n".join(thinking_parts) messages.append(msg) except json.JSONDecodeError: continue except OSError: pass return messages def _parse_codex_conversation(self, session_id): """Parse Codex JSONL conversation format.""" messages = [] conv_file = self._find_codex_transcript_file(session_id) if conv_file and conv_file.exists(): try: for line in conv_file.read_text().splitlines(): if not line.strip(): continue try: entry = json.loads(line) if not isinstance(entry, dict): continue # Codex format: type="response_item", payload.type="message" if entry.get("type") != "response_item": continue payload = entry.get("payload", {}) if not isinstance(payload, dict): continue if payload.get("type") != "message": continue role = payload.get("role", "") content_parts = payload.get("content", []) if not isinstance(content_parts, list): continue # Skip developer role (system context/permissions) if role == "developer": continue # Extract text from content array text_parts = [] for part in content_parts: if isinstance(part, dict): # Codex uses "input_text" for user, "output_text" for assistant text = part.get("text", "") if text: # Skip injected context (AGENTS.md, environment, permissions) skip_prefixes = ( "", "", "", "# AGENTS.md instructions", ) if any(text.startswith(p) for p in skip_prefixes): continue text_parts.append(text) if text_parts and role in ("user", "assistant"): messages.append({ "role": role, "content": "\n".join(text_parts), "timestamp": entry.get("timestamp", ""), }) except json.JSONDecodeError: continue except OSError: pass return messages