"""
MCP Server for Claudia Memory System

Exposes memory tools via the Model Context Protocol for use by Claude Code.
"""

import asyncio
import json
import logging
import sys
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional

from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
    CallToolResult,
    ListToolsResult,
    TextContent,
    Tool,
    ToolAnnotations,
)

from ..database import get_db
from ..utils import parse_naive
from ..services.consolidate import (
    get_consolidate_service,
    get_predictions,
    run_full_consolidation,
)
from ..services.recall import (
    entity_overview,
    fetch_by_ids,
    find_duplicate_entities,
    find_path,
    get_active_reflections,
    get_dormant_relationships,
    get_hub_entities,
    get_project_network,
    get_recall_service,
    get_reflection_by_id,
    get_reflections,
    recall,
    recall_about,
    recall_episodes,
    recall_since,
    recall_temporal,
    recall_timeline,
    recall_upcoming_deadlines,
    search_entities,
    search_reflections,
    trace_memory,
)
from ..services.ingest import get_ingest_service
from ..services.documents import get_document_service
from ..services.audit import (
    get_entity_audit_history,
    get_memory_audit_history,
)
from ..services.remember import (
    buffer_turn,
    correct_memory,
    delete_entity,
    end_session,
    get_remember_service,
    get_unsummarized_turns,
    invalidate_memory,
    invalidate_relationship,
    merge_entities,
    relate_entities,
    store_reflection,
    update_reflection,
    delete_reflection,
    remember_entity,
    remember_fact,
    remember_message,
)
from ..embeddings import get_embedding_service

logger = logging.getLogger(__name__)


def _coerce_arg(arguments: Dict[str, Any], key: str, expected_type: type = list) -> None:
    """Coerce a tool argument from JSON string to expected type in-place.

    LLMs sometimes serialize array parameters as JSON strings instead of
    native arrays. This transparently parses them back so handler code
    can assume native types.
    """
    value = arguments.get(key)
    if isinstance(value, str):
        try:
            parsed = json.loads(value)
            if isinstance(parsed, expected_type):
                arguments[key] = parsed
            else:
                logger.warning(
                    f"Coercion: '{key}' parsed to {type(parsed).__name__}, "
                    f"expected {expected_type.__name__}"
                )
        except (json.JSONDecodeError, TypeError):
            logger.warning(f"Could not parse '{key}' as JSON: {value[:100]}")


def _coerce_int(arguments: Dict[str, Any], key: str) -> None:
    """Coerce a string or float argument to int in-place.

    LLMs sometimes pass integer arguments as JSON strings (e.g. "days": "7"
    instead of "days": 7). The MCP SDK validates the schema before calling our
    handler, so we accept ["integer", "string"] in schemas and convert here.
    """
    value = arguments.get(key)
    if value is None:
        return
    if isinstance(value, (str, float)) and not isinstance(value, bool):
        try:
            arguments[key] = int(value)
        except (ValueError, TypeError):
            logger.warning(f"Could not coerce '{key}' to int: {value!r}")


# ── Handler registry ──

_TOOL_HANDLERS: Dict[str, Any] = {}


def _handler(*names: str):
    """Register an async function as handler for one or more tool names."""
    def decorator(fn):
        for n in names:
            _TOOL_HANDLERS[n] = fn
        return fn
    return decorator


def _require(arguments: dict, key: str, tool_name: str):
    """Get a required parameter, raising ValueError with a clear message if missing."""
    value = arguments.get(key)
    if value is None:
        raise ValueError(f"Required parameter '{key}' missing for {tool_name}")
    return value


# ── Parameter-name aliases (v1.58.0 PR E) ──
#
# The memory MCP tools historically used different parameter conventions
# (entity vs source/target/relationship vs query). The aliases below let
# callers use a consistent variant while every existing caller continues
# to work unchanged. Normalization happens here at the MCP boundary;
# service-layer signatures in claudia_memory/services/ are untouched.
#
# Rules:
#   1. Purely additive. The canonical name continues to work as before.
#   2. If both the canonical name and an alias are provided in the same
#      call, the canonical name wins (the alias is left in place and is
#      not consulted by the handler).
#   3. Otherwise, the first matching alias is renamed to the canonical
#      key and the alias key is removed from the arguments dict so the
#      handler only ever sees the canonical name.

_PARAM_ALIASES: Dict[str, Dict[str, List[str]]] = {
    "memory_about": {
        "entity": ["entity_name", "name"],
    },
    "memory_relate": {
        "source": ["source_entity"],
        "target": ["target_entity"],
        "relationship": ["relationship_type"],
    },
    "memory_recall": {
        "query": ["q", "search"],
    },
}


def _normalize_params(arguments: dict, canonical: str, aliases: List[str]) -> dict:
    """Resolve alias parameter names to the canonical name.

    If the canonical name is already present in `arguments`, it wins and
    `arguments` is returned unchanged. Otherwise, the first alias from
    `aliases` that is present is renamed to the canonical key, and the
    alias key is removed. If no alias matches either, `arguments` is
    returned unchanged.

    The function never mutates the caller's dict: when a rewrite is
    needed it returns a shallow copy with the alias key replaced.
    """
    if canonical in arguments:
        return arguments
    for alias in aliases:
        if alias in arguments:
            arguments = dict(arguments)
            arguments[canonical] = arguments.pop(alias)
            return arguments
    return arguments


def _apply_parameter_aliases(tool_name: str, arguments: dict) -> dict:
    """Apply all registered aliases for the given tool, if any.

    Tools without an entry in `_PARAM_ALIASES` (the vast majority) get
    their arguments back unchanged. Dot-notation aliases (e.g.
    'memory.about') are routed to the same canonical tool name for the
    purposes of alias lookup.
    """
    # Dot-notation aliases (memory.about, etc.) share the same canonical
    # alias map as their underscore counterparts.
    lookup_name = tool_name.replace(".", "_", 1) if "." in tool_name else tool_name
    aliases_for_tool = _PARAM_ALIASES.get(lookup_name)
    if not aliases_for_tool:
        return arguments
    for canonical, alias_list in aliases_for_tool.items():
        arguments = _normalize_params(arguments, canonical, alias_list)
    return arguments


MAX_RESPONSE_BYTES = 50_000


def _guard_response_size(text: str, tool_name: str) -> str:
    """Truncate response if it exceeds size limit."""
    if len(text.encode('utf-8')) > MAX_RESPONSE_BYTES:
        truncated = text[:MAX_RESPONSE_BYTES].rsplit('\n', 1)[0]
        return truncated + f"\n\n[Response truncated at {MAX_RESPONSE_BYTES // 1000}KB. Use compact=true or lower limit to reduce size.]"
    return text


# ── Extracted tool handlers ──
# Each handler is a module-level async function registered via @_handler.
# Handlers receive: arguments (dict), db, config (unused placeholder), logger, and **ctx.
# They return a CallToolResult directly.


@_handler("memory_temporal", "memory_upcoming", "memory_since", "memory_timeline", "memory_morning_context")
async def _handle_temporal(arguments, db, config, logger, **ctx):
    # Determine operation from merged tool or backward-compat alias
    name = ctx.get("tool_name", "memory_temporal")
    if name == "memory_upcoming":
        op = "upcoming"
    elif name == "memory_since":
        op = "since"
    elif name == "memory_timeline":
        op = "timeline"
    elif name == "memory_morning_context":
        op = "morning"
    else:
        op = arguments.get("operation", "upcoming")

    if op == "upcoming":
        _coerce_int(arguments, "days")
        days = arguments.get("days", 14)
        include_overdue = arguments.get("include_overdue", True)
        results = recall_upcoming_deadlines(days, include_overdue=include_overdue)
        grouped = {"overdue": [], "today": [], "tomorrow": [], "this_week": [], "later": []}
        for r in results:
            urgency = (r.metadata or {}).get("urgency", "later")
            deadline = (r.metadata or {}).get("deadline_at", "")
            grouped.setdefault(urgency, []).append({
                "id": r.id, "content": r.content, "deadline_at": deadline,
                "importance": r.importance, "entities": r.entities[:3],
            })
        grouped = {k: v for k, v in grouped.items() if v}
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(grouped, indent=2))]
        )
    elif op == "since":
        _coerce_int(arguments, "limit")
        since_val = arguments.get("since", "")
        if not since_val:
            return CallToolResult(
                content=[TextContent(type="text", text=json.dumps({"error": "'since' is required for operation='since'"}))],
                isError=True,
            )
        entity = arguments.get("entity")
        limit = arguments.get("limit", 50)
        results = recall_since(since_val, entity_name=entity, limit=limit)
        formatted = [{
            "id": r.id, "content": r.content, "type": r.memory_type,
            "created_at": r.created_at, "importance": r.importance,
            "entities": r.entities[:3],
        } for r in results]
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(formatted, indent=2))]
        )
    elif op == "timeline":
        _coerce_int(arguments, "limit")
        entity = arguments.get("entity", "")
        if not entity:
            return CallToolResult(
                content=[TextContent(type="text", text=json.dumps({"error": "'entity' is required for operation='timeline'"}))],
                isError=True,
            )
        limit = arguments.get("limit", 50)
        results = recall_timeline(entity, limit=limit)
        formatted = [{
            "id": r.id, "content": r.content, "type": r.memory_type,
            "created_at": r.created_at, "importance": r.importance,
            "deadline_at": (r.metadata or {}).get("deadline_at"),
            "has_deadline": (r.metadata or {}).get("has_deadline", False),
        } for r in results]
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(formatted, indent=2))]
        )
    elif op == "morning":
        morning_text = _build_morning_context()
        return CallToolResult(
            content=[TextContent(type="text", text=morning_text)]
        )
    else:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": f"Unknown temporal operation: {op}"}))],
            isError=True,
        )


@_handler("memory_graph", "memory_project_network", "memory_find_path", "memory_network_hubs", "memory_dormant_relationships", "memory_reconnections")
async def _handle_graph(arguments, db, config, logger, **ctx):
    name = ctx.get("tool_name", "memory_graph")
    if name == "memory_project_network":
        op = "network"
    elif name == "memory_find_path":
        op = "path"
    elif name == "memory_network_hubs":
        op = "hubs"
    elif name == "memory_dormant_relationships":
        op = "dormant"
    elif name == "memory_reconnections":
        op = "reconnect"
    else:
        op = arguments.get("operation", "network")

    if op == "network":
        project = arguments.get("project", "")
        if not project:
            return CallToolResult(
                content=[TextContent(type="text", text=json.dumps({"error": "'project' is required for operation='network'"}))],
                isError=True,
            )
        result = get_project_network(project_name=project)
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result, default=str))]
        )
    elif op == "path":
        _coerce_int(arguments, "max_depth")
        entity_a = arguments.get("entity_a", "")
        entity_b = arguments.get("entity_b", "")
        if not entity_a or not entity_b:
            return CallToolResult(
                content=[TextContent(type="text", text=json.dumps({"error": "'entity_a' and 'entity_b' required for operation='path'"}))],
                isError=True,
            )
        result = find_path(
            entity_a=entity_a, entity_b=entity_b,
            max_depth=arguments.get("max_depth", 4),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"path": result, "connected": result is not None}))]
        )
    elif op == "hubs":
        _coerce_int(arguments, "min_connections")
        _coerce_int(arguments, "limit")
        result = get_hub_entities(
            min_connections=arguments.get("min_connections", 5),
            entity_type=arguments.get("entity_type"),
            limit=arguments.get("limit", 20),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"hubs": result, "count": len(result)}))]
        )
    elif op == "dormant":
        _coerce_int(arguments, "days")
        _coerce_int(arguments, "limit")
        result = get_dormant_relationships(
            days=arguments.get("days", 60),
            min_strength=arguments.get("min_strength", 0.3),
            limit=arguments.get("limit", 20),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"dormant": result, "count": len(result)}))]
        )
    elif op == "reconnect":
        _coerce_int(arguments, "limit")
        limit = arguments.get("limit", 10)
        now_str = datetime.utcnow().isoformat()
        rows = get_db().execute(
            """
            SELECT content, priority, metadata
            FROM predictions
            WHERE prediction_type = 'reconnection'
              AND expires_at > ?
            ORDER BY priority DESC
            LIMIT ?
            """,
            (now_str, limit),
            fetch=True,
        ) or []
        suggestions = []
        for row in rows:
            meta = {}
            try:
                meta = json.loads(row["metadata"]) if row["metadata"] else {}
            except (json.JSONDecodeError, TypeError):
                pass
            suggestions.append({
                "person": meta.get("entity_name", "Unknown"),
                "days_since_contact": meta.get("days_since_contact", 0),
                "trend": meta.get("trend", "unknown"),
                "last_topic": meta.get("last_topic", ""),
                "open_commitments": meta.get("open_commitments", []),
                "suggestion": row["content"],
                "priority": row["priority"],
            })
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(suggestions, indent=2))]
        )
    else:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": f"Unknown graph operation: {op}"}))],
            isError=True,
        )


@_handler("memory_entities", "memory_entity", "memory_search_entities", "memory_merge_entities", "memory_delete_entity", "memory_entity_overview")
async def _handle_entities(arguments, db, config, logger, **ctx):
    name = ctx.get("tool_name", "memory_entities")
    if name == "memory_entity":
        op = "create"
    elif name == "memory_search_entities":
        op = "search"
    elif name == "memory_merge_entities":
        op = "merge"
    elif name == "memory_delete_entity":
        op = "delete"
    elif name == "memory_entity_overview":
        op = "overview"
    else:
        op = arguments.get("operation", "search")

    if op == "create":
        _coerce_arg(arguments, "aliases")
        name_val = arguments.get("name", "")
        if not name_val:
            return CallToolResult(
                content=[TextContent(type="text", text=json.dumps({"error": "'name' required for operation='create'"}))],
                isError=True,
            )
        entity_id = remember_entity(
            name=name_val,
            entity_type=arguments.get("type", ""),
            description=arguments.get("description"),
            aliases=arguments.get("aliases"),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"success": True, "entity_id": entity_id}))]
        )
    elif op == "search":
        _coerce_int(arguments, "limit")
        _coerce_arg(arguments, "types")
        query = _require(arguments, "query", name)
        results = search_entities(
            query=query,
            entity_types=arguments.get("types"),
            limit=arguments.get("limit", 10),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({
                "entities": [{
                    "id": e.id, "name": e.name, "type": e.type,
                    "description": e.description, "importance": e.importance,
                    "memory_count": e.memory_count, "relationship_count": e.relationship_count,
                } for e in results]
            }))]
        )
    elif op == "merge":
        _coerce_int(arguments, "source_id")
        _coerce_int(arguments, "target_id")
        result = merge_entities(
            source_id=_require(arguments, "source_id", name),
            target_id=_require(arguments, "target_id", name),
            reason=arguments.get("reason"),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result))]
        )
    elif op == "delete":
        _coerce_int(arguments, "entity_id")
        result = delete_entity(
            entity_id=_require(arguments, "entity_id", name),
            reason=arguments.get("reason"),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result))]
        )
    elif op == "overview":
        _coerce_arg(arguments, "entities", list)
        entities_arg = arguments.get("entities", [])
        if isinstance(entities_arg, str):
            entities_arg = [entities_arg]
        result = entity_overview(
            entity_names=entities_arg,
            include_network=arguments.get("include_network", True),
            include_summaries=arguments.get("include_summaries", True),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result))]
        )
    else:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": f"Unknown entities operation: {op}"}))],
            isError=True,
        )


@_handler("memory_vault", "memory_sync_vault", "memory_vault_status", "memory_generate_canvas", "memory_import_vault_edits")
async def _handle_vault(arguments, db, config, logger, **ctx):
    name = ctx.get("tool_name", "memory_vault")
    if name == "memory_sync_vault":
        op = "sync"
    elif name == "memory_vault_status":
        op = "status"
    elif name == "memory_generate_canvas":
        op = "canvas"
    elif name == "memory_import_vault_edits":
        op = "import"
    else:
        op = arguments.get("operation", "status")

    if op == "sync":
        from ..config import _project_id
        from ..services.vault_sync import run_vault_sync
        full = arguments.get("full", False)
        result = run_vault_sync(project_id=_project_id, full=full)
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"success": True, **result}))]
        )
    elif op == "status":
        from ..config import _project_id
        from ..services.vault_sync import get_vault_sync_service
        svc = get_vault_sync_service(_project_id)
        status = svc.get_status()
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(status, indent=2))]
        )
    elif op == "canvas":
        from ..config import _project_id
        from ..services.vault_sync import get_vault_path
        from ..services.canvas_generator import CanvasGenerator
        vault_path = get_vault_path(_project_id)
        gen = CanvasGenerator(vault_path)
        canvas_type = arguments.get("canvas_type", "all")
        if canvas_type == "all":
            result = gen.generate_all()
        elif canvas_type == "relationship_map":
            path = gen.generate_relationship_map()
            result = {"relationship_map": {"path": str(path), "status": "ok"}}
        elif canvas_type == "morning_brief":
            path = gen.generate_morning_brief()
            result = {"morning_brief": {"path": str(path), "status": "ok"}}
        elif canvas_type == "project_board":
            project_name = arguments.get("project_name")
            if not project_name:
                return CallToolResult(
                    content=[TextContent(type="text", text=json.dumps(
                        {"error": "project_name is required for project_board"}
                    ))],
                    isError=True,
                )
            path = gen.generate_project_board(project_name)
            if path:
                result = {"project_board": {"path": str(path), "status": "ok"}}
            else:
                result = {"project_board": {"status": "not_found", "error": f"Project '{project_name}' not found"}}
        else:
            return CallToolResult(
                content=[TextContent(type="text", text=json.dumps(
                    {"error": f"Unknown canvas type: {canvas_type}"}
                ))],
                isError=True,
            )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result, indent=2))]
        )
    elif op == "import":
        from ..config import _project_id
        from ..services.vault_sync import get_vault_sync_service
        vault_svc = get_vault_sync_service(_project_id)
        result = vault_svc.import_all_edits()
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result, indent=2))]
        )
    else:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": f"Unknown vault operation: {op}"}))],
            isError=True,
        )


@_handler("memory_modify", "memory_correct", "memory_invalidate", "memory_invalidate_relationship")
async def _handle_modify(arguments, db, config, logger, **ctx):
    name = ctx.get("tool_name", "memory_modify")
    if name == "memory_correct":
        op = "correct"
    elif name == "memory_invalidate":
        op = "invalidate"
    elif name == "memory_invalidate_relationship":
        op = "invalidate_relationship"
    else:
        op = arguments.get("operation", "correct")

    if op == "correct":
        _coerce_int(arguments, "memory_id")
        result = correct_memory(
            memory_id=_require(arguments, "memory_id", name),
            correction=_require(arguments, "correction", name),
            reason=arguments.get("reason"),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result))]
        )
    elif op == "invalidate":
        _coerce_int(arguments, "memory_id")
        result = invalidate_memory(
            memory_id=_require(arguments, "memory_id", name),
            reason=arguments.get("reason"),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result))]
        )
    elif op == "invalidate_relationship":
        result = invalidate_relationship(
            source=_require(arguments, "source", name),
            target=_require(arguments, "target", name),
            relationship=_require(arguments, "relationship", name),
            reason=arguments.get("reason"),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result))]
        )
    else:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": f"Unknown modify operation: {op}"}))],
            isError=True,
        )


@_handler("memory_session", "memory_buffer_turn", "memory_session_context", "memory_unsummarized")
async def _handle_session(arguments, db, config, logger, **ctx):
    name = ctx.get("tool_name", "memory_session")
    if name == "memory_buffer_turn":
        op = "buffer"
    elif name == "memory_session_context":
        op = "context"
    elif name == "memory_unsummarized":
        op = "unsummarized"
    else:
        op = arguments.get("operation", "context")

    if op == "buffer":
        _coerce_int(arguments, "episode_id")
        result = buffer_turn(
            user_content=arguments.get("user_content"),
            assistant_content=arguments.get("assistant_content"),
            episode_id=arguments.get("episode_id"),
            source=arguments.get("source"),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result))]
        )
    elif op == "context":
        budget = arguments.get("token_budget", "normal")
        context_text = _build_session_context(budget)
        return CallToolResult(
            content=[TextContent(type="text", text=context_text)]
        )
    elif op == "unsummarized":
        results = get_unsummarized_turns()
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({
                "unsummarized_sessions": results,
                "count": len(results),
            }))]
        )
    else:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": f"Unknown session operation: {op}"}))],
            isError=True,
        )


@_handler("memory_document", "memory_file", "memory_documents", "memory_purge")
async def _handle_document(arguments, db, config, logger, **ctx):
    name = ctx.get("tool_name", "memory_document")
    if name == "memory_file":
        op = "store"
    elif name == "memory_documents":
        op = "search"
    elif name == "memory_purge":
        op = "purge"
    else:
        op = arguments.get("operation", "search")

    if op == "store":
        _coerce_arg(arguments, "about")
        _coerce_arg(arguments, "memory_ids")
        doc_svc = get_document_service()
        result = doc_svc.file_document_from_text(
            content=_require(arguments, "content", name),
            filename=_require(arguments, "filename", name),
            source_type=arguments.get("source_type", "capture"),
            summary=arguments.get("summary"),
            about_entities=arguments.get("about"),
            memory_ids=arguments.get("memory_ids"),
            source_ref=arguments.get("source_ref"),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result))]
        )
    elif op == "search":
        _coerce_int(arguments, "limit")
        doc_svc = get_document_service()
        results = doc_svc.search_documents(
            query=arguments.get("query"),
            source_type=arguments.get("source_type"),
            entity_name=arguments.get("entity"),
            limit=arguments.get("limit", 20),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"documents": results, "count": len(results)}))]
        )
    elif op == "purge":
        doc_svc = get_document_service()
        result = doc_svc.purge_document(document_id=_require(arguments, "document_id", name))
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result))]
        )
    else:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": f"Unknown document operation: {op}"}))],
            isError=True,
        )


@_handler("memory_provenance", "memory_trace", "memory_audit_history")
async def _handle_provenance(arguments, db, config, logger, **ctx):
    name = ctx.get("tool_name", "memory_provenance")
    if name == "memory_trace":
        op = "trace"
    elif name == "memory_audit_history":
        op = "audit"
    else:
        op = arguments.get("operation", "trace")

    if op == "trace":
        _coerce_int(arguments, "memory_id")
        result = trace_memory(memory_id=_require(arguments, "memory_id", name))
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result))]
        )
    elif op == "audit":
        _coerce_int(arguments, "entity_id")
        _coerce_int(arguments, "memory_id")
        _coerce_int(arguments, "limit")
        entity_id = arguments.get("entity_id")
        memory_id = arguments.get("memory_id")
        limit = arguments.get("limit", 20)
        if entity_id:
            history = get_entity_audit_history(entity_id)
        elif memory_id:
            history = get_memory_audit_history(memory_id)
        else:
            return CallToolResult(
                content=[TextContent(type="text", text=json.dumps({"error": "Either entity_id or memory_id is required"}))],
                isError=True,
            )
        history = history[:limit]
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({
                "entity_id": entity_id, "memory_id": memory_id,
                "history": history, "count": len(history),
            }))]
        )
    elif op == "verify_chain":
        from ..services.remember import _compute_chain_hash
        rows = db.execute(
            "SELECT id, content, metadata, hash, prev_hash FROM memories ORDER BY id ASC",
            fetch=True,
        ) or []
        chain_length = 0
        is_valid = True
        first_break_at = None
        for row in rows:
            if row["hash"] is None:
                continue
            chain_length += 1
            meta = json.loads(row["metadata"]) if row["metadata"] else None
            expected = _compute_chain_hash(row["content"], meta, row["prev_hash"])
            if expected != row["hash"]:
                is_valid = False
                if first_break_at is None:
                    first_break_at = row["id"]
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({
                "chain_length": chain_length,
                "is_valid": is_valid,
                "first_break_at": first_break_at,
            }))]
        )
    else:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": f"Unknown provenance operation: {op}"}))],
            isError=True,
        )


@_handler("memory_remember")
async def _handle_remember(arguments, db, config, logger, **ctx):
    _coerce_arg(arguments, "about")
    memory_id = remember_fact(
        content=_require(arguments, "content", "memory_remember"),
        memory_type=arguments.get("type", "fact"),
        about_entities=arguments.get("about"),
        importance=arguments.get("importance", 1.0),
        source=arguments.get("source"),
        source_context=arguments.get("source_context"),
        source_channel=arguments.get("source_channel"),
        critical=arguments.get("critical", False),
        fact_id=arguments.get("fact_id"),
    )
    # Save source material to disk if provided
    if memory_id and arguments.get("source_material"):
        svc = get_remember_service()
        svc.save_source_material(
            memory_id,
            arguments["source_material"],
            metadata={
                "source": arguments.get("source"),
                "source_context": arguments.get("source_context"),
            },
        )
    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=json.dumps({"success": True, "memory_id": memory_id}),
            )
        ]
    )


@_handler("memory_recall")
async def _handle_recall(arguments, db, config, logger, **ctx):
    _coerce_arg(arguments, "types")
    _coerce_arg(arguments, "ids")
    _coerce_int(arguments, "limit")
    # Direct fetch by IDs (skip search)
    if "ids" in arguments and arguments["ids"]:
        results = fetch_by_ids(arguments["ids"])
        response_text = json.dumps(
            {
                "results": [
                    {
                        "id": r.id,
                        "content": r.content,
                        "type": r.type,
                        "score": r.score,
                        "importance": r.importance,
                        "entities": r.entities,
                        "created_at": r.created_at,
                        "source": r.source,
                        "source_id": r.source_id,
                        "source_context": r.source_context,
                        "source_channel": r.source_channel,
                    }
                    for r in results
                ]
            }
        )
        response_text = _guard_response_size(response_text, "memory_recall")
        return CallToolResult(
            content=[TextContent(type="text", text=response_text)]
        )

    # Search mode
    query = arguments.get("query", "")
    if not query:
        return CallToolResult(
            content=[
                TextContent(
                    type="text",
                    text=json.dumps({"error": "Either 'query' or 'ids' is required"}),
                )
            ],
            isError=True,
        )

    results = recall(
        query=query,
        limit=arguments.get("limit", 10),
        memory_types=arguments.get("types"),
        about_entity=arguments.get("about"),
        include_archived=arguments.get("include_archived", False),
    )

    compact = arguments.get("compact", False)
    if compact:
        response_text = json.dumps(
            {
                "results": [
                    {
                        "id": r.id,
                        "snippet": r.content[:80] + ("..." if len(r.content) > 80 else ""),
                        "type": r.type,
                        "score": round(r.score, 3),
                        "entities": r.entities[:3],
                    }
                    for r in results
                ]
            }
        )
        response_text = _guard_response_size(response_text, "memory_recall")
        return CallToolResult(
            content=[TextContent(type="text", text=response_text)]
        )

    response_text = json.dumps(
        {
            "results": [
                {
                    "id": r.id,
                    "content": r.content,
                    "type": r.type,
                    "score": r.score,
                    "importance": r.importance,
                    "entities": r.entities,
                    "created_at": r.created_at,
                    "source": r.source,
                    "source_id": r.source_id,
                    "source_context": r.source_context,
                    "source_channel": r.source_channel,
                }
                for r in results
            ]
        }
    )
    response_text = _guard_response_size(response_text, "memory_recall")
    return CallToolResult(
        content=[TextContent(type="text", text=response_text)]
    )


@_handler("memory_about")
async def _handle_about(arguments, db, config, logger, **ctx):
    _coerce_int(arguments, "limit")
    result = recall_about(
        entity_name=_require(arguments, "entity", "memory_about"),
        limit=arguments.get("limit", 20),
        include_historical=arguments.get("include_historical", False),
    )

    # Convert RecallResult objects to dicts
    if result.get("memories"):
        result["memories"] = [
            {
                "id": m.id,
                "content": m.content,
                "type": m.type,
                "importance": m.importance,
                "created_at": m.created_at,
                "source": m.source,
                "source_id": m.source_id,
                "source_context": m.source_context,
            }
            for m in result["memories"]
        ]

    response_text = json.dumps(result)
    response_text = _guard_response_size(response_text, "memory_about")
    return CallToolResult(
        content=[TextContent(type="text", text=response_text)]
    )


@_handler("memory_relate")
async def _handle_relate(arguments, db, config, logger, **ctx):
    relationship_id = relate_entities(
        source=_require(arguments, "source", "memory_relate"),
        target=_require(arguments, "target", "memory_relate"),
        relationship=_require(arguments, "relationship", "memory_relate"),
        strength=arguments.get("strength", 1.0),
        valid_at=arguments.get("valid_at"),
        supersedes=arguments.get("supersedes", False),
        origin_type=arguments.get("origin_type", "extracted"),
        direction=arguments.get("direction", "bidirectional"),
    )
    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=json.dumps({"success": True, "relationship_id": relationship_id}),
            )
        ]
    )


@_handler("memory_consolidate")
async def _handle_consolidate(arguments, db, config, logger, **ctx):
    result = run_full_consolidation()
    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=json.dumps(result),
            )
        ]
    )


@_handler("memory_end_session")
async def _handle_end_session(arguments, db, config, logger, **ctx):
    _coerce_int(arguments, "episode_id")
    # Coerce all array fields (LLMs may send JSON strings)
    for field in ("facts", "commitments", "entities", "relationships", "key_topics", "reflections"):
        _coerce_arg(arguments, field)

    # Handle missing or invalid episode_id: auto-create
    episode_id = arguments.get("episode_id")
    svc = get_remember_service()
    if episode_id is None:
        episode_id = svc.db.insert("episodes", {
            "started_at": datetime.utcnow().isoformat(),
            "source": "claude_code",
        })
        logger.info(f"Auto-created episode {episode_id} (no episode_id provided)")
    else:
        episode = svc.db.get_one("episodes", where="id = ?", where_params=(episode_id,))
        if not episode:
            new_id = svc.db.insert("episodes", {
                "started_at": datetime.utcnow().isoformat(),
                "source": arguments.get("source", "claude_code"),
            })
            logger.info(f"Auto-created episode {new_id} (requested {episode_id} did not exist)")
            episode_id = new_id

    result = end_session(
        episode_id=episode_id,
        narrative=_require(arguments, "narrative", "memory_end_session"),
        facts=arguments.get("facts"),
        commitments=arguments.get("commitments"),
        entities=arguments.get("entities"),
        relationships=arguments.get("relationships"),
        key_topics=arguments.get("key_topics"),
    )

    # Process reflections if provided
    reflections_input = arguments.get("reflections", [])
    reflections_stored = 0
    for ref in reflections_input:
        ref_id = store_reflection(
            content=ref["content"],
            reflection_type=ref.get("type", "observation"),
            episode_id=episode_id,
            about_entity=ref.get("about"),
            importance=ref.get("importance", 0.7),
        )
        if ref_id:
            reflections_stored += 1
    result["reflections_stored"] = reflections_stored

    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=json.dumps(result),
            )
        ]
    )


@_handler("memory_reflections")
async def _handle_reflections(arguments, db, config, logger, **ctx):
    _coerce_int(arguments, "limit")
    _coerce_int(arguments, "reflection_id")
    _coerce_arg(arguments, "types")
    action = arguments.get("action", "get")
    limit = arguments.get("limit", 10)
    types = arguments.get("types")
    about = arguments.get("about")
    query = arguments.get("query")

    if action == "delete":
        reflection_id = arguments.get("reflection_id")
        if not reflection_id:
            return CallToolResult(
                content=[TextContent(type="text", text=json.dumps({"error": "reflection_id required for delete"}))]
            )
        success = delete_reflection(reflection_id)
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"deleted": success, "reflection_id": reflection_id}))]
        )

    elif action == "update":
        reflection_id = arguments.get("reflection_id")
        new_content = arguments.get("content")
        if not reflection_id:
            return CallToolResult(
                content=[TextContent(type="text", text=json.dumps({"error": "reflection_id required for update"}))]
            )
        success = update_reflection(reflection_id, content=new_content)
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"updated": success, "reflection_id": reflection_id}))]
        )

    elif action == "search" and query:
        results = search_reflections(query, limit=limit, reflection_types=types)
        return CallToolResult(
            content=[
                TextContent(
                    type="text",
                    text=json.dumps({
                        "reflections": [
                            {
                                "id": r.id,
                                "content": r.content,
                                "type": r.reflection_type,
                                "importance": r.importance,
                                "confidence": r.confidence,
                                "about_entity": r.about_entity,
                                "first_observed": r.first_observed_at,
                                "last_confirmed": r.last_confirmed_at,
                                "times_confirmed": r.aggregation_count,
                                "score": r.score,
                            }
                            for r in results
                        ],
                        "count": len(results),
                    }),
                )
            ]
        )

    else:  # action == "get" (default)
        results = get_reflections(
            limit=limit,
            reflection_types=types,
            about_entity=about,
        )
        return CallToolResult(
            content=[
                TextContent(
                    type="text",
                    text=json.dumps({
                        "reflections": [
                            {
                                "id": r.id,
                                "content": r.content,
                                "type": r.reflection_type,
                                "importance": r.importance,
                                "confidence": r.confidence,
                                "about_entity": r.about_entity,
                                "first_observed": r.first_observed_at,
                                "last_confirmed": r.last_confirmed_at,
                                "times_confirmed": r.aggregation_count,
                            }
                            for r in results
                        ],
                        "count": len(results),
                    }),
                )
            ]
        )


@_handler("memory_batch")
async def _handle_batch(arguments, db, config, logger, **ctx):
    _coerce_arg(arguments, "operations")
    operations = arguments.get("operations", [])

    # --- Pass 1: Collect all texts that need embeddings ---
    embed_tasks = []  # list of (index, text) for parallel embedding
    for i, op in enumerate(operations):
        op_type = op.get("op")
        if op_type == "remember":
            embed_tasks.append((i, op["content"]))
        elif op_type == "entity":
            # Only new entities need embeddings; collect optimistically
            embed_text = f"{op['name']}. {op.get('description') or ''}"
            embed_tasks.append((i, embed_text))

    # --- Parallel embedding pass ---
    embeddings_map = {}  # index -> embedding
    if embed_tasks:
        try:
            emb_svc = get_embedding_service()
            texts = [text for _, text in embed_tasks]
            all_embeddings = await emb_svc.embed_batch(texts)
            for (idx, _), emb in zip(embed_tasks, all_embeddings):
                if emb is not None:
                    embeddings_map[idx] = emb
        except Exception as e:
            logger.warning(f"Batch parallel embedding failed, falling back to per-op: {e}")
            # embeddings_map stays empty; remember_fact/entity will embed individually

    # --- Pass 2: Execute operations with pre-computed embeddings ---
    results = []
    for i, op in enumerate(operations):
        op_type = op.get("op")
        op_result = {"index": i, "op": op_type}
        try:
            if op_type == "entity":
                entity_id = remember_entity(
                    name=op["name"],
                    entity_type=op.get("type", ""),
                    description=op.get("description"),
                    aliases=op.get("aliases"),
                    _precomputed_embedding=embeddings_map.get(i),
                )
                op_result["success"] = True
                op_result["entity_id"] = entity_id
            elif op_type == "remember":
                memory_id = remember_fact(
                    content=op["content"],
                    memory_type=op.get("type", "fact"),
                    about_entities=op.get("about"),
                    importance=op.get("importance", 1.0),
                    source=op.get("source"),
                    source_context=op.get("source_context"),
                    source_channel=op.get("source_channel"),
                    _precomputed_embedding=embeddings_map.get(i),
                )
                op_result["success"] = True
                op_result["memory_id"] = memory_id
                # Save source material to disk if provided
                if memory_id and op.get("source_material"):
                    svc = get_remember_service()
                    svc.save_source_material(
                        memory_id,
                        op["source_material"],
                        metadata={
                            "source": op.get("source"),
                            "source_context": op.get("source_context"),
                        },
                    )
            elif op_type == "relate":
                relationship_id = relate_entities(
                    source=op["source"],
                    target=op["target"],
                    relationship=op["relationship"],
                    strength=op.get("strength", 1.0),
                    supersedes=op.get("supersedes", False),
                    valid_at=op.get("valid_at"),
                    direction=op.get("direction", "bidirectional"),
                    origin_type=op.get("origin_type", "extracted"),
                )
                op_result["success"] = True
                op_result["relationship_id"] = relationship_id
            else:
                op_result["success"] = False
                op_result["error"] = f"Unknown operation: {op_type}"
        except Exception as e:
            logger.warning(f"Batch operation {i} ({op_type}) failed: {e}")
            op_result["success"] = False
            op_result["error"] = str(e)
        results.append(op_result)

    succeeded = sum(1 for r in results if r.get("success"))
    failed = len(results) - succeeded
    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=json.dumps({
                    "success": failed == 0,
                    "total": len(results),
                    "succeeded": succeeded,
                    "failed": failed,
                    "results": results,
                }),
            )
        ]
    )


@_handler("cognitive.ingest")
async def _handle_ingest(arguments, db, config, logger, **ctx):
    svc = get_ingest_service()
    result = await svc.ingest(
        text=_require(arguments, "text", "cognitive.ingest"),
        source_type=arguments.get("source_type", "general"),
        context=arguments.get("context"),
    )
    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=json.dumps(result),
            )
        ]
    )


@_handler("memory_multi_recall")
async def _handle_multi_recall(arguments, db, config, logger, **ctx):
    """Execute multiple recall queries in a single call and return deduplicated results.

    This is the compound equivalent of calling memory_recall N times sequentially.
    Saves N-1 model round trips and deduplicates overlapping results server-side.
    """
    _coerce_arg(arguments, "queries")
    queries = arguments.get("queries", [])
    if not queries:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": "queries array is required"}))],
            isError=True,
        )

    global_limit = arguments.get("limit", 10)
    compact = arguments.get("compact", False)

    seen_ids = set()
    all_sections = []

    for q in queries:
        # Each query can be a string or an object with query/limit/types/about
        if isinstance(q, str):
            query_text = q
            q_limit = global_limit
            q_types = None
            q_about = None
            q_label = q
        else:
            query_text = q.get("query", "")
            q_limit = q.get("limit", global_limit)
            q_types = q.get("types")
            q_about = q.get("about")
            q_label = q.get("label", query_text)

        if not query_text:
            all_sections.append({"label": q_label, "results": [], "error": "empty query"})
            continue

        try:
            results = recall(
                query=query_text,
                limit=q_limit,
                memory_types=q_types,
                about_entity=q_about,
            )
            section_results = []
            for r in results:
                if r.id in seen_ids:
                    continue
                seen_ids.add(r.id)
                if compact:
                    section_results.append({
                        "id": r.id,
                        "snippet": r.content[:80],
                        "type": r.type,
                        "score": round(r.score, 3),
                        "entities": (r.entities or [])[:3],
                    })
                else:
                    section_results.append({
                        "id": r.id,
                        "content": r.content,
                        "type": r.type,
                        "importance": r.importance,
                        "score": round(r.score, 3),
                        "entities": r.entities or [],
                        "created_at": r.created_at,
                        "source": r.source,
                        "source_context": r.source_context,
                    })
            all_sections.append({"label": q_label, "count": len(section_results), "results": section_results})
        except Exception as e:
            logger.warning(f"multi_recall query '{q_label}' failed: {e}")
            all_sections.append({"label": q_label, "results": [], "error": str(e)})

    response = {
        "queries": len(queries),
        "total_unique": len(seen_ids),
        "sections": all_sections,
    }
    text = json.dumps(response)
    return CallToolResult(
        content=[TextContent(type="text", text=_guard_response_size(text, "memory_multi_recall"))]
    )


@_handler("memory_deep_context")
async def _handle_deep_context(arguments, db, config, logger, **ctx):
    """Server-side deep context assembly. Replaces 6-8 sequential MCP calls with one.

    Executes: entity lookup + semantic recall + connected entity pulls +
    temporal sweep + episode search. Deduplicates by memory ID across all steps.
    Returns a structured JSON object ready for synthesis.
    """
    target = _require(arguments, "target", "memory_deep_context")
    entity_limit = arguments.get("entity_limit", 50)
    recall_limit = arguments.get("recall_limit", 50)
    connected_limit = arguments.get("connected_limit", 10)
    max_connections = arguments.get("max_connections", 3)
    temporal_limit = arguments.get("temporal_limit", 30)
    episode_limit = arguments.get("episode_limit", 20)

    seen_ids = set()
    result = {
        "target": target,
        "entity": None,
        "memories": [],
        "relationships": [],
        "connected_entities": [],
        "temporal": [],
        "episodes": [],
        "stats": {},
    }

    def _dedup_results(recall_results):
        """Filter out already-seen memory IDs and return new ones."""
        new = []
        for r in recall_results:
            if r.id not in seen_ids:
                seen_ids.add(r.id)
                new.append({
                    "id": r.id,
                    "content": r.content,
                    "type": r.type,
                    "importance": r.importance,
                    "score": round(r.score, 3) if r.score else 0,
                    "entities": r.entities or [],
                    "created_at": r.created_at,
                    "source": r.source,
                    "source_context": r.source_context,
                })
        return new

    # Step 1: Entity core (recall_about)
    try:
        about_data = recall_about(target, limit=entity_limit)
        if about_data.get("entity"):
            entity_info = about_data["entity"]
            result["entity"] = {
                "name": entity_info.get("name"),
                "type": entity_info.get("type"),
                "description": entity_info.get("description"),
                "importance": entity_info.get("importance"),
                "created_at": entity_info.get("created_at"),
                "updated_at": entity_info.get("updated_at"),
            }
            result["relationships"] = about_data.get("relationships", [])
            # Process memories from about
            about_memories = about_data.get("memories", [])
            for m in about_memories:
                if hasattr(m, "id") and m.id not in seen_ids:
                    seen_ids.add(m.id)
                    result["memories"].append({
                        "id": m.id,
                        "content": m.content,
                        "type": m.type,
                        "importance": m.importance,
                        "score": round(m.score, 3) if m.score else 0,
                        "entities": m.entities or [],
                        "created_at": m.created_at,
                        "source": getattr(m, "source", None),
                        "source_context": getattr(m, "source_context", None),
                    })
    except Exception as e:
        logger.warning(f"deep_context Step 1 (about) failed for '{target}': {e}")

    # Step 2: Broad semantic recall
    try:
        broad_results = recall(query=target, limit=recall_limit)
        new_memories = _dedup_results(broad_results)
        result["memories"].extend(new_memories)
    except Exception as e:
        logger.warning(f"deep_context Step 2 (semantic recall) failed: {e}")

    # Step 3: Connected entities (top N by relationship strength)
    try:
        rels = result.get("relationships", [])
        # Sort by strength descending, take top N
        sorted_rels = sorted(rels, key=lambda r: r.get("strength", 0) if isinstance(r, dict) else 0, reverse=True)
        connected_names = []
        for rel in sorted_rels[:max_connections]:
            if isinstance(rel, dict):
                # Get the "other" entity name
                source_name = rel.get("source_name") or rel.get("source", "")
                target_name = rel.get("target_name") or rel.get("target", "")
                from ..extraction.entity_extractor import get_extractor as _get_ext
                target_canonical = _get_ext().canonical_name(target)
                other = target_name if _get_ext().canonical_name(source_name) == target_canonical else source_name
                if other and other not in connected_names:
                    connected_names.append(other)

        for conn_name in connected_names:
            try:
                conn_about = recall_about(conn_name, limit=connected_limit)
                conn_entry = {
                    "name": conn_name,
                    "entity": None,
                    "memories": [],
                }
                if conn_about.get("entity"):
                    ei = conn_about["entity"]
                    conn_entry["entity"] = {
                        "name": ei.get("name"),
                        "type": ei.get("type"),
                        "description": ei.get("description"),
                    }
                conn_memories = conn_about.get("memories", [])
                for m in conn_memories:
                    if hasattr(m, "id") and m.id not in seen_ids:
                        seen_ids.add(m.id)
                        conn_entry["memories"].append({
                            "id": m.id,
                            "content": m.content,
                            "type": m.type,
                            "importance": m.importance,
                            "created_at": m.created_at,
                        })
                result["connected_entities"].append(conn_entry)
            except Exception as e:
                logger.warning(f"deep_context connected entity '{conn_name}' failed: {e}")
    except Exception as e:
        logger.warning(f"deep_context Step 3 (connections) failed: {e}")

    # Step 4: Temporal sweep (observations, learnings, commitments)
    try:
        temporal_results = recall(
            query=target,
            limit=temporal_limit,
            memory_types=["observation", "learning", "commitment"],
        )
        result["temporal"] = _dedup_results(temporal_results)
    except Exception as e:
        logger.warning(f"deep_context Step 4 (temporal) failed: {e}")

    # Step 5: Episode context
    try:
        episodes = recall_episodes(query=f"session with {target}", limit=episode_limit)
        result["episodes"] = [
            {
                "id": ep.get("id"),
                "narrative": ep.get("narrative", "")[:500],
                "started_at": ep.get("started_at"),
                "turn_count": ep.get("turn_count"),
            }
            for ep in episodes
        ]
    except Exception as e:
        logger.warning(f"deep_context Step 5 (episodes) failed: {e}")

    # Stats
    result["stats"] = {
        "total_unique_memories": len(seen_ids),
        "entity_found": result["entity"] is not None,
        "relationships_count": len(result.get("relationships", [])),
        "connected_entities_pulled": len(result.get("connected_entities", [])),
        "temporal_items": len(result.get("temporal", [])),
        "episodes_found": len(result.get("episodes", [])),
    }

    text = json.dumps(result)
    return CallToolResult(
        content=[TextContent(type="text", text=_guard_response_size(text, "memory_deep_context"))]
    )


@_handler("memory_briefing")
async def _handle_briefing(arguments, db, config, logger, **ctx):
    briefing_text = _build_briefing()
    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=briefing_text,
            )
        ]
    )


@_handler("memory_summary")
async def _handle_summary(arguments, db, config, logger, **ctx):
    _coerce_int(arguments, "top_facts_limit")
    entity_names = arguments.get("entities", [])
    top_facts_limit = arguments.get("top_facts_limit", 5)
    db = get_db()
    summaries = []

    for entity_name in entity_names:
        from ..extraction.entity_extractor import get_extractor
        canonical = get_extractor().canonical_name(entity_name)

        entity = db.get_one(
            "entities",
            where="canonical_name = ? AND deleted_at IS NULL",
            where_params=(canonical,),
        )
        if not entity:
            summaries.append({"name": entity_name, "found": False})
            continue

        eid = entity["id"]

        # Memory count (excluding invalidated)
        mem_count_rows = db.execute(
            """
            SELECT COUNT(*) as cnt FROM memories m
            JOIN memory_entities me ON m.id = me.memory_id
            WHERE me.entity_id = ? AND m.invalidated_at IS NULL
            """,
            (eid,),
            fetch=True,
        ) or []
        memory_count = mem_count_rows[0]["cnt"] if mem_count_rows else 0

        # Relationship count
        rel_count_rows = db.execute(
            """
            SELECT COUNT(*) as cnt FROM relationships
            WHERE (source_entity_id = ? OR target_entity_id = ?)
            AND invalid_at IS NULL
            """,
            (eid, eid),
            fetch=True,
        ) or []
        relationship_count = rel_count_rows[0]["cnt"] if rel_count_rows else 0

        # Last mentioned
        last_rows = db.execute(
            """
            SELECT MAX(m.created_at) as last_mentioned FROM memories m
            JOIN memory_entities me ON m.id = me.memory_id
            WHERE me.entity_id = ? AND m.invalidated_at IS NULL
            """,
            (eid,),
            fetch=True,
        ) or []
        last_mentioned = last_rows[0]["last_mentioned"] if last_rows else None

        # Top facts
        fact_rows = db.execute(
            """
            SELECT m.content, m.type, m.importance FROM memories m
            JOIN memory_entities me ON m.id = me.memory_id
            WHERE me.entity_id = ? AND m.invalidated_at IS NULL
            ORDER BY m.importance DESC, m.created_at DESC
            LIMIT ?
            """,
            (eid, top_facts_limit),
            fetch=True,
        ) or []

        summaries.append({
            "name": entity["name"],
            "type": entity["type"],
            "importance": entity["importance"],
            "found": True,
            "memory_count": memory_count,
            "relationship_count": relationship_count,
            "last_mentioned": last_mentioned,
            "top_facts": [
                {"content": r["content"], "type": r["type"], "importance": r["importance"]}
                for r in fact_rows
            ],
        })

    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=json.dumps({"summaries": summaries}, indent=2),
            )
        ]
    )


@_handler("memory_system_health")
async def _handle_system_health(arguments, db, config, logger, **ctx):
    import urllib.request, urllib.error
    report = None
    try:
        with urllib.request.urlopen("http://localhost:3848/status", timeout=2) as resp:
            report = json.loads(resp.read().decode())
    except (urllib.error.URLError, OSError):
        from ..daemon.health import build_status_report
        report = build_status_report()
    embedding_svc = get_embedding_service()
    if hasattr(embedding_svc, '_model_mismatch') and embedding_svc._model_mismatch:
        if "components" not in report:
            report["components"] = {}
        report["components"]["embedding_model_mismatch"] = True
    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=json.dumps(report, indent=2),
            )
        ]
    )


@_handler("memory_backup")
async def _handle_backup(arguments, db, config, logger, **ctx):
    import urllib.request, urllib.error
    result = None
    try:
        req = urllib.request.Request(
            "http://localhost:3848/backup", method="POST", data=b""
        )
        with urllib.request.urlopen(req, timeout=10) as resp:
            result = json.loads(resp.read().decode())
    except (urllib.error.URLError, OSError):
        # Daemon not running — trigger backup directly
        backup_path = get_db().backup()
        result = {"status": "ok", "path": str(backup_path)}
    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=json.dumps(result, indent=2),
            )
        ]
    )


@_handler("memory_project_health")
async def _handle_project_health(arguments, db, config, logger, **ctx):
    _coerce_int(arguments, "days_ahead")
    from ..services.recall import project_relationship_health
    entity = _require(arguments, "entity", "memory_project_health")
    days_ahead = arguments.get("days_ahead", 30)
    result = project_relationship_health(entity, days_ahead)
    return CallToolResult(
        content=[TextContent(type="text", text=json.dumps(result, indent=2))]
    )


@_handler("memory_lifecycle")
async def _handle_lifecycle(arguments, db, config, logger, **ctx):
    op = _require(arguments, "operation", "memory_lifecycle")

    if op == "set":
        fact_id = _require(arguments, "fact_id", "memory_lifecycle")
        tier = _require(arguments, "tier", "memory_lifecycle")
        reason = arguments.get("reason", "manual")
        if tier == "sacred":
            db.execute(
                "UPDATE memories SET lifecycle_tier = 'sacred', sacred_reason = ?, updated_at = datetime('now') WHERE fact_id = ?",
                (reason, fact_id),
            )
        elif tier == "archived":
            db.execute(
                "UPDATE memories SET lifecycle_tier = 'archived', archived_at = datetime('now'), updated_at = datetime('now') WHERE fact_id = ?",
                (fact_id,),
            )
        else:
            db.execute(
                "UPDATE memories SET lifecycle_tier = ?, updated_at = datetime('now') WHERE fact_id = ?",
                (tier, fact_id),
            )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"success": True, "fact_id": fact_id, "tier": tier}))]
        )

    elif op == "protect":
        entity_name = _require(arguments, "entity", "memory_lifecycle")
        reason = arguments.get("reason", "user-designated")
        canonical = entity_name.strip().lower()
        row = db.execute(
            "SELECT id FROM entities WHERE canonical_name = ? AND deleted_at IS NULL",
            (canonical,),
            fetch=True,
        )
        if not row:
            return CallToolResult(
                content=[TextContent(type="text", text=json.dumps({"error": f"Entity not found: {entity_name}"}))],
                isError=True,
            )
        result = set_close_circle(row[0]["id"], reason=reason)
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(result))]
        )

    elif op == "archive":
        fact_id = _require(arguments, "fact_id", "memory_lifecycle")
        db.execute(
            "UPDATE memories SET lifecycle_tier = 'archived', archived_at = datetime('now'), updated_at = datetime('now') WHERE fact_id = ?",
            (fact_id,),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"success": True, "fact_id": fact_id, "tier": "archived"}))]
        )

    elif op == "restore":
        fact_id = _require(arguments, "fact_id", "memory_lifecycle")
        db.execute(
            "UPDATE memories SET lifecycle_tier = 'active', archived_at = NULL, updated_at = datetime('now') WHERE fact_id = ?",
            (fact_id,),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"success": True, "fact_id": fact_id, "tier": "active"}))]
        )

    elif op == "status":
        stats = {}
        for tier in ("sacred", "active", "cooling", "archived"):
            row = db.execute(
                "SELECT COUNT(*) as cnt FROM memories WHERE lifecycle_tier = ? AND invalidated_at IS NULL",
                (tier,),
                fetch=True,
            )
            stats[tier] = row[0]["cnt"] if row else 0
        null_row = db.execute(
            "SELECT COUNT(*) as cnt FROM memories WHERE lifecycle_tier IS NULL AND invalidated_at IS NULL",
            fetch=True,
        )
        stats["legacy_active"] = null_row[0]["cnt"] if null_row else 0
        cc_row = db.execute(
            "SELECT COUNT(*) as cnt FROM entities WHERE close_circle = 1 AND deleted_at IS NULL",
            fetch=True,
        )
        stats["close_circle_entities"] = cc_row[0]["cnt"] if cc_row else 0
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(stats, indent=2))]
        )

    else:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": f"Unknown lifecycle operation: {op}"}))],
            isError=True,
        )


@_handler("memory_context")
async def _handle_context(arguments, db, config, logger, **ctx):
    from ..services.context_builder import build_context
    _coerce_int(arguments, "token_budget")
    result = build_context(
        query=_require(arguments, "query", "memory_context"),
        token_budget=arguments.get("token_budget", 8000),
        include_sacred=arguments.get("include_sacred", True),
        entity=arguments.get("entity"),
    )
    return CallToolResult(
        content=[TextContent(type="text", text=json.dumps(result, default=str))]
    )


@_handler("memory_checkpoint")
async def _handle_checkpoint(arguments, db, config, logger, **ctx):
    op = _require(arguments, "operation", "memory_checkpoint")

    if op == "save":
        name = arguments.get("name", "manual")
        backup_path = db.backup(label=name)
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"success": True, "path": str(backup_path), "label": name}))]
        )

    elif op == "list":
        import glob as glob_mod
        from pathlib import Path
        pattern = f"{db.db_path}.backup-*.db"
        backups = sorted(glob_mod.glob(pattern), key=lambda p: Path(p).stat().st_mtime, reverse=True)
        items = []
        for b in backups[:20]:
            p = Path(b)
            items.append({
                "path": str(p),
                "name": p.name,
                "size_mb": round(p.stat().st_size / 1024 / 1024, 2),
                "modified": datetime.fromtimestamp(p.stat().st_mtime).isoformat(),
            })
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps(items, indent=2))]
        )

    elif op == "load":
        name = arguments.get("name", "")
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({
                "warning": "Checkpoint restore requires daemon restart.",
                "instruction": "Stop daemon, copy backup over DB, restart.",
                "backup_pattern": f"{db.db_path}.backup-{name}-*.db",
            }))]
        )

    else:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": f"Unknown checkpoint operation: {op}"}))],
            isError=True,
        )


@_handler("memory_rollback")
async def _handle_rollback(arguments, db, config, logger, **ctx):
    op = _require(arguments, "operation", "memory_rollback")

    if op == "set":
        ts = _require(arguments, "timestamp", "memory_rollback")
        db.execute(
            "INSERT INTO _meta (key, value, updated_at) VALUES ('view_as_of', ?, datetime('now')) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = datetime('now')",
            (ts,),
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"success": True, "view_as_of": ts}))]
        )

    elif op == "clear":
        db.execute(
            "INSERT INTO _meta (key, value, updated_at) VALUES ('view_as_of', NULL, datetime('now')) ON CONFLICT(key) DO UPDATE SET value = NULL, updated_at = datetime('now')",
        )
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"success": True, "view_as_of": None}))]
        )

    elif op == "status":
        row = db.execute("SELECT value FROM _meta WHERE key = 'view_as_of'", fetch=True)
        val = row[0]["value"] if row and row[0]["value"] else None
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"view_as_of": val, "mode": "historical" if val else "current"}))]
        )

    else:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({"error": f"Unknown rollback operation: {op}"}))],
            isError=True,
        )


# ── Backward compatibility: accept dot-notation names during transition ──
# Users who haven't updated skills/hooks may still call memory.recall instead
# of memory_recall. Register aliases so both work. list_tools() only advertises
# underscore names; these aliases only affect call_tool() dispatch.
# TODO: Remove after v1.58.0 (2 release cycles)
_DOT_ALIASES = {k.replace("_", ".", 1): v for k, v in _TOOL_HANDLERS.items()
                if k.startswith("memory_")}
_TOOL_HANDLERS.update(_DOT_ALIASES)

# Initialize the MCP server
server = Server("claudia-memory")


@server.list_tools()
async def list_tools() -> ListToolsResult:
    """List all available memory tools"""
    tools = [
        Tool(
            name="memory_remember",
            title="Store a Memory",
            description="Store information in Claudia's memory. Use for facts, preferences, observations, or learnings about people, projects, or the user.",
            annotations=ToolAnnotations(destructiveHint=False),
            inputSchema={
                "type": "object",
                "properties": {
                    "content": {
                        "type": "string",
                        "description": "The information to remember (fact, preference, observation, etc.)",
                    },
                    "type": {
                        "type": "string",
                        "enum": ["fact", "preference", "observation", "learning", "commitment"],
                        "description": "Type of memory",
                        "default": "fact",
                    },
                    "about": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Entity names this memory relates to (people, projects, etc.)",
                    },
                    "importance": {
                        "type": "number",
                        "description": "Importance score from 0.0 to 1.0",
                        "default": 1.0,
                    },
                    "source": {
                        "type": "string",
                        "description": "Source type: email, transcript, document, conversation, user_input",
                    },
                    "source_context": {
                        "type": "string",
                        "description": "One-line breadcrumb describing origin (e.g., 'Email from Jim Ferry re: Forum V+, 2025-01-28')",
                    },
                    "source_material": {
                        "type": "string",
                        "description": "Full raw text of the source (email body, transcript, etc.). Saved to disk, not stored in DB.",
                    },
                    "source_channel": {
                        "type": "string",
                        "description": "Origin channel: claude_code, telegram, slack",
                    },
                    "critical": {
                        "type": "boolean",
                        "description": "Mark this memory as sacred (never decays, always included in context). Use for critical personal facts.",
                    },
                    "fact_id": {
                        "type": "string",
                        "description": "Explicit UUID for this memory. Auto-generated if not provided.",
                    },
                },
                "required": ["content"],
            },
        ),
        Tool(
            name="memory_recall",
            title="Search Memory",
            description=(
                "Search Claudia's memory for relevant information. Uses hybrid vector + full-text "
                "similarity. Use compact=true for lightweight browsing (snippets), then fetch full "
                "content with ids=[...] for the interesting results."
            ),
            annotations=ToolAnnotations(readOnlyHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "What to search for (required unless ids is provided)",
                    },
                    "limit": {
                        "type": "string",
                        "description": "Maximum number of results",
                        "default": 10,
                    },
                    "types": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Filter by memory types (fact, preference, observation, learning, commitment)",
                    },
                    "about": {
                        "type": "string",
                        "description": "Filter to memories about a specific entity",
                    },
                    "compact": {
                        "type": "boolean",
                        "description": "Return compact results: {id, snippet (80 chars), type, score, entities (max 3)}",
                        "default": False,
                    },
                    "ids": {
                        "type": "array",
                        "items": {"type": "integer"},
                        "description": "Fetch specific memories by ID (skips search). Use after a compact search to get full content.",
                    },
                    "include_archived": {
                        "type": "boolean",
                        "description": "Include archived memories in results (default: false)",
                    },
                },
            },
        ),
        Tool(
            name="memory_about",
            title="Get Entity Context",
            description="Get all context about a specific person, project, or entity. Returns memories, relationships, and metadata.",
            annotations=ToolAnnotations(readOnlyHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "entity": {
                        "type": "string",
                        "description": "Name of the person, project, or entity",
                    },
                    "limit": {
                        "type": "string",
                        "description": "Maximum number of memories to return",
                        "default": 20,
                    },
                    "include_historical": {
                        "type": "boolean",
                        "description": "Include superseded/historical relationships (shows valid_at/invalid_at timestamps)",
                        "default": False,
                    },
                },
                "required": ["entity"],
            },
        ),
        Tool(
            name="memory_relate",
            title="Create Relationship",
            description="Create or strengthen a relationship between two entities (people, projects, etc.)",
            annotations=ToolAnnotations(destructiveHint=False),
            inputSchema={
                "type": "object",
                "properties": {
                    "source": {
                        "type": "string",
                        "description": "Source entity name",
                    },
                    "target": {
                        "type": "string",
                        "description": "Target entity name",
                    },
                    "relationship": {
                        "type": "string",
                        "description": "Type of relationship (works_with, manages, client_of, etc.)",
                    },
                    "strength": {
                        "type": "number",
                        "description": "Relationship strength from 0.0 to 1.0",
                        "default": 1.0,
                    },
                    "valid_at": {
                        "type": "string",
                        "description": "When this relationship became true (ISO date string). Defaults to now.",
                    },
                    "supersedes": {
                        "type": "boolean",
                        "description": "If true, invalidate existing relationship of same type between same entities and create new one",
                        "default": False,
                    },
                    "origin_type": {
                        "type": "string",
                        "description": "How this was learned: user_stated, extracted, inferred, corrected",
                        "default": "extracted",
                    },
                    "direction": {
                        "type": "string",
                        "description": "Relationship direction: forward, backward, or bidirectional",
                        "default": "bidirectional",
                    },
                },
                "required": ["source", "target", "relationship"],
            },
        ),
        Tool(
            name="memory_consolidate",
            title="Run Consolidation",
            description="Manually trigger memory consolidation (decay, merging, pattern detection). Usually runs automatically at 3 AM.",
            annotations=ToolAnnotations(destructiveHint=False),
            inputSchema={
                "type": "object",
                "properties": {},
            },
        ),
        Tool(
            name="memory_end_session",
            title="End Session",
            description=(
                "Finalize a session with a narrative summary and structured extractions. "
                "Call at session end. The narrative should ENHANCE stored information -- capture "
                "tone, emotional context, unresolved threads, reasons behind decisions, half-formed "
                "ideas, and anything that doesn't fit structured categories. The structured fields "
                "(facts, commitments, entities, relationships) are stored alongside the narrative, "
                "not replaced by it. Both are searchable in future sessions."
            ),
            annotations=ToolAnnotations(destructiveHint=False),
            inputSchema={
                "type": "object",
                "properties": {
                    "episode_id": {
                        "type": "string",
                        "description": "Episode ID from buffer_turn calls during the session",
                    },
                    "narrative": {
                        "type": "string",
                        "description": (
                            "Free-form narrative summary of the session. Capture the texture: "
                            "what was discussed, what felt important, what was unresolved, "
                            "emotional undercurrents, reasons behind decisions, context that "
                            "enriches the structured data. This is NOT a compression -- it adds "
                            "dimensions that structured fields cannot capture."
                        ),
                    },
                    "facts": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "content": {"type": "string"},
                                "type": {
                                    "type": "string",
                                    "enum": ["fact", "preference", "observation", "learning", "pattern"],
                                    "default": "fact",
                                },
                                "about": {
                                    "type": "array",
                                    "items": {"type": "string"},
                                    "description": "Entity names this fact relates to",
                                },
                                "importance": {"type": "number", "default": 1.0},
                                "source": {
                                    "type": "string",
                                    "description": "Override source type (default: session_summary)",
                                },
                                "source_context": {
                                    "type": "string",
                                    "description": "One-line breadcrumb describing origin",
                                },
                                "source_material": {
                                    "type": "string",
                                    "description": "Full raw source text, saved to disk",
                                },
                            },
                            "required": ["content"],
                        },
                        "description": "Structured facts, preferences, observations, learnings extracted from the session",
                    },
                    "commitments": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "content": {"type": "string"},
                                "about": {
                                    "type": "array",
                                    "items": {"type": "string"},
                                },
                                "importance": {"type": "number", "default": 1.0},
                                "source": {
                                    "type": "string",
                                    "description": "Override source type (default: session_summary)",
                                },
                                "source_context": {
                                    "type": "string",
                                    "description": "One-line breadcrumb describing origin",
                                },
                                "source_material": {
                                    "type": "string",
                                    "description": "Full raw source text, saved to disk",
                                },
                            },
                            "required": ["content"],
                        },
                        "description": "Commitments or promises made during the session",
                    },
                    "entities": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "name": {"type": "string"},
                                "type": {
                                    "type": "string",
                                    "enum": ["person", "organization", "project", "concept", "location"],
                                    "default": "person",
                                },
                                "description": {"type": "string"},
                                "aliases": {
                                    "type": "array",
                                    "items": {"type": "string"},
                                },
                            },
                            "required": ["name"],
                        },
                        "description": "New or updated entities mentioned during the session",
                    },
                    "relationships": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "source": {"type": "string"},
                                "target": {"type": "string"},
                                "relationship": {"type": "string"},
                                "strength": {"type": "number", "default": 1.0},
                            },
                            "required": ["source", "target", "relationship"],
                        },
                        "description": "Relationships between entities observed during the session",
                    },
                    "key_topics": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Main topics discussed in the session",
                    },
                    "reflections": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "content": {"type": "string"},
                                "type": {
                                    "type": "string",
                                    "enum": ["observation", "pattern", "learning", "question"],
                                    "default": "observation",
                                    "description": (
                                        "observation: User behavior/preference noticed. "
                                        "pattern: Recurring theme across sessions. "
                                        "learning: How to work better with this user. "
                                        "question: Worth revisiting later."
                                    ),
                                },
                                "about": {
                                    "type": "string",
                                    "description": "Entity name this reflection is about (optional)",
                                },
                                "importance": {
                                    "type": "number",
                                    "default": 0.7,
                                    "description": "Importance 0-1 (default 0.7, higher than regular memories)",
                                },
                            },
                            "required": ["content", "type"],
                        },
                        "description": (
                            "Persistent reflections from /meditate. These are user-approved "
                            "observations, patterns, learnings, and questions that decay very slowly "
                            "and inform future sessions. Generate 1-3 reflections per session capturing "
                            "cross-session patterns or communication insights."
                        ),
                    },
                },
                "required": ["narrative"],
            },
        ),
        Tool(
            name="memory_reflections",
            title="Manage Reflections",
            description=(
                "Get or search persistent reflections (observations, patterns, learnings, questions) "
                "from past /meditate sessions. Reflections are user-approved insights that decay "
                "very slowly and inform future interactions. Use this to retrieve cross-session "
                "patterns about user preferences and communication styles."
            ),
            annotations=ToolAnnotations(destructiveHint=False),
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Semantic search query (optional). If omitted, returns recent high-importance reflections.",
                    },
                    "types": {
                        "type": "array",
                        "items": {
                            "type": "string",
                            "enum": ["observation", "pattern", "learning", "question"],
                        },
                        "description": "Filter by reflection types (optional)",
                    },
                    "about": {
                        "type": "string",
                        "description": "Filter to reflections about a specific entity (optional)",
                    },
                    "limit": {
                        "type": "string",
                        "default": 10,
                        "description": "Maximum results to return",
                    },
                    "action": {
                        "type": "string",
                        "enum": ["get", "search", "update", "delete"],
                        "default": "get",
                        "description": "Action: get (list), search (semantic), update, delete",
                    },
                    "reflection_id": {
                        "type": "string",
                        "description": "Reflection ID (required for update/delete actions)",
                    },
                    "content": {
                        "type": "string",
                        "description": "New content (for update action)",
                    },
                },
            },
        ),
        Tool(
            name="memory_batch",
            title="Batch Memory Operations",
            description=(
                "Execute multiple memory operations in a single call. Use this for mid-session "
                "entity creation when processing a new person, meeting transcript, or topic that "
                "requires entity creation, multiple memories, and relationships. Much more efficient "
                "than calling memory_entity, memory_remember, and memory_relate separately. "
                "For end-of-session summaries, use memory_end_session instead."
            ),
            annotations=ToolAnnotations(destructiveHint=False),
            inputSchema={
                "type": "object",
                "properties": {
                    "operations": {
                        "type": "array",
                        "description": "Array of operations to execute in order",
                        "items": {
                            "type": "object",
                            "properties": {
                                "op": {
                                    "type": "string",
                                    "enum": ["entity", "remember", "relate"],
                                    "description": "Operation type",
                                },
                                "name": {
                                    "type": "string",
                                    "description": "Entity name (for 'entity' op)",
                                },
                                "type": {
                                    "type": "string",
                                    "description": "Entity type or memory type",
                                },
                                "description": {
                                    "type": "string",
                                    "description": "Entity description (for 'entity' op)",
                                },
                                "aliases": {
                                    "type": "array",
                                    "items": {"type": "string"},
                                    "description": "Entity aliases (for 'entity' op)",
                                },
                                "content": {
                                    "type": "string",
                                    "description": "Memory content (for 'remember' op)",
                                },
                                "about": {
                                    "type": "array",
                                    "items": {"type": "string"},
                                    "description": "Entity names this relates to (for 'remember' op)",
                                },
                                "importance": {
                                    "type": "number",
                                    "description": "Importance score 0.0-1.0 (for 'remember' op)",
                                },
                                "source": {
                                    "type": "string",
                                    "description": "Source entity (for 'relate' op) or source type (for 'remember' op)",
                                },
                                "source_context": {
                                    "type": "string",
                                    "description": "One-line breadcrumb (for 'remember' op)",
                                },
                                "source_material": {
                                    "type": "string",
                                    "description": "Full raw source text, saved to disk (for 'remember' op)",
                                },
                                "source_channel": {
                                    "type": "string",
                                    "description": "Origin channel: claude_code, telegram, slack (for 'remember' op)",
                                },
                                "target": {
                                    "type": "string",
                                    "description": "Target entity (for 'relate' op)",
                                },
                                "relationship": {
                                    "type": "string",
                                    "description": "Relationship type (for 'relate' op)",
                                },
                                "strength": {
                                    "type": "number",
                                    "description": "Relationship strength 0.0-1.0 (for 'relate' op)",
                                },
                                "origin_type": {
                                    "type": "string",
                                    "description": "How this was learned: user_stated, extracted, inferred (for 'relate' op)",
                                },
                                "supersedes": {
                                    "type": "boolean",
                                    "description": "Invalidate existing relationship of same type (for 'relate' op)",
                                    "default": False,
                                },
                                "valid_at": {
                                    "type": "string",
                                    "description": "When this relationship became true (for 'relate' op)",
                                },
                                "direction": {
                                    "type": "string",
                                    "description": "Relationship direction (for 'relate' op)",
                                },
                            },
                            "required": ["op"],
                        },
                    },
                },
                "required": ["operations"],
            },
        ),
        Tool(
            name="memory_multi_recall",
            title="Multi-Query Recall",
            description=(
                "Execute multiple recall queries in a single call. Returns deduplicated results "
                "grouped by query. Use instead of calling memory_recall repeatedly when you need "
                "to search across several dimensions (e.g., different topics, entity types, or "
                "time-sensitive items). Saves round trips and deduplicates overlapping results "
                "server-side. Each query can specify its own limit, types filter, and entity filter."
            ),
            annotations=ToolAnnotations(readOnlyHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "queries": {
                        "type": "array",
                        "description": (
                            "Array of queries. Each can be a string (simple query) or an object "
                            "with {query, limit, types, about, label} for fine-grained control."
                        ),
                        "items": {},
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Default limit per query (overridden by per-query limit)",
                        "default": 10,
                    },
                    "compact": {
                        "type": "boolean",
                        "description": "Return compact results (id, snippet, type, score, top 3 entities)",
                        "default": False,
                    },
                },
                "required": ["queries"],
            },
        ),
        Tool(
            name="memory_deep_context",
            title="Deep Context Assembly",
            description=(
                "Full-context deep analysis in a single call. Executes the complete deep-context "
                "pipeline server-side: entity lookup, broad semantic recall, connected entity pulls, "
                "temporal sweep (observations/learnings/commitments), and episode search. "
                "Deduplicates by memory ID across all steps. Returns structured JSON ready for "
                "synthesis. Use for meeting prep, relationship deep dives, or strategic analysis. "
                "Replaces 6-8 sequential memory_about/memory_recall calls with one compound call."
            ),
            annotations=ToolAnnotations(readOnlyHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "target": {
                        "type": "string",
                        "description": "Entity name or topic to deep-dive on",
                    },
                    "entity_limit": {
                        "type": "integer",
                        "description": "Max memories from entity lookup (Step 1)",
                        "default": 50,
                    },
                    "recall_limit": {
                        "type": "integer",
                        "description": "Max memories from broad semantic search (Step 2)",
                        "default": 50,
                    },
                    "connected_limit": {
                        "type": "integer",
                        "description": "Max memories per connected entity (Step 3)",
                        "default": 10,
                    },
                    "max_connections": {
                        "type": "integer",
                        "description": "How many connected entities to pull (Step 3)",
                        "default": 3,
                    },
                    "temporal_limit": {
                        "type": "integer",
                        "description": "Max temporal items (Step 4)",
                        "default": 30,
                    },
                    "episode_limit": {
                        "type": "integer",
                        "description": "Max episodes to search (Step 5)",
                        "default": 20,
                    },
                },
                "required": ["target"],
            },
        ),
        Tool(
            name="memory_briefing",
            title="Compact Briefing",
            description=(
                "Compact session briefing (~500 tokens). Returns aggregate counts and highlights: "
                "active commitments, cooling relationships, unread messages, top prediction, "
                "recent activity. Call at session start instead of loading full context. "
                "Use memory_recall or memory_about to drill into specifics during conversation."
            ),
            annotations=ToolAnnotations(readOnlyHint=True),
            inputSchema={
                "type": "object",
                "properties": {},
            },
        ),
        Tool(
            name="cognitive.ingest",
            title="Extract Entities from Text",
            description=(
                "Extract structured data from raw text using a local language model. "
                "Use this when the user pastes a meeting transcript, email, document, or "
                "any large block of text that needs entity extraction, fact identification, "
                "and commitment detection. Returns structured JSON with entities, facts, "
                "commitments, action items, and relationships. If no local language model "
                "is available, returns the raw text so Claude can process it directly."
            ),
            annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "text": {
                        "type": "string",
                        "description": "The raw text to extract structured data from",
                    },
                    "source_type": {
                        "type": "string",
                        "enum": ["meeting", "email", "document", "general"],
                        "description": "Type of source text (affects extraction schema)",
                        "default": "general",
                    },
                    "context": {
                        "type": "string",
                        "description": (
                            "Optional context about the text "
                            "(e.g., 'Call between user and their investor Sarah')"
                        ),
                    },
                },
                "required": ["text"],
            },
        ),
        Tool(
            name="memory_summary",
            title="Entity Summaries",
            description=(
                "Get a lightweight summary for one or more entities. Returns name, type, "
                "importance, memory count, relationship count, last mentioned date, and "
                "top facts. Cheaper than memory_about for quick overviews."
            ),
            annotations=ToolAnnotations(readOnlyHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "entities": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "List of entity names to summarize",
                    },
                    "top_facts_limit": {
                        "type": "string",
                        "description": "Maximum top facts per entity (default 5)",
                        "default": 5,
                    },
                },
                "required": ["entities"],
            },
        ),
        Tool(
            name="memory_system_health",
            title="System Health Check",
            description=(
                "Get comprehensive system health: schema version, component status, "
                "scheduled job list, and memory/entity counts. Use this to diagnose "
                "issues or verify the memory system is working correctly."
            ),
            annotations=ToolAnnotations(readOnlyHint=True),
            inputSchema={
                "type": "object",
                "properties": {},
            },
        ),
        Tool(
            name="memory_backup",
            title="Trigger Database Backup",
            description=(
                "Trigger an immediate backup of the memory database. Returns the path "
                "of the newly created backup file. Backups use a timestamp suffix and "
                "older backups are pruned automatically per the retention policy."
            ),
            inputSchema={
                "type": "object",
                "properties": {},
            },
        ),
        Tool(
            name="memory_project_health",
            title="Project Health Check",
            description=(
                "Project when a relationship will go dormant based on contact velocity. "
                "Returns projected dormant date, recommended contact date, risk level, "
                "and open commitments. Ask 'if I don't contact [person], when will they go dormant?' "
                "or 'which relationships need attention in the next 2 weeks?'"
            ),
            annotations=ToolAnnotations(readOnlyHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "entity": {
                        "type": "string",
                        "description": "Name of the person to project health for",
                    },
                    "days_ahead": {
                        "type": "string",
                        "description": "How far ahead to project (default 30)",
                        "default": 30,
                    },
                },
                "required": ["entity"],
            },
        ),
        # ── Merged tools (8 composite tools with operation parameter) ──
        Tool(
            name="memory_temporal",
            title="Temporal Queries",
            description=(
                "Time-based memory queries: upcoming deadlines, recent changes, entity timelines, "
                "and curated morning digests. Use operation='upcoming' for deadlines, 'since' for "
                "recent changes, 'timeline' for entity history, 'morning' for the full morning digest."
            ),
            annotations=ToolAnnotations(readOnlyHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["upcoming", "since", "timeline", "morning"],
                        "description": "Which temporal query to run",
                    },
                    "days": {
                        "type": "string",
                        "description": "Look ahead window in days (for upcoming, default 14)",
                        "default": 14,
                    },
                    "include_overdue": {
                        "type": "boolean",
                        "description": "Include overdue items (for upcoming, default true)",
                        "default": True,
                    },
                    "since": {
                        "type": "string",
                        "description": "ISO datetime string (for since operation)",
                    },
                    "entity": {
                        "type": "string",
                        "description": "Entity name (for timeline, or optional filter for since)",
                    },
                    "limit": {
                        "type": "string",
                        "description": "Maximum results",
                        "default": 50,
                    },
                },
                "required": ["operation"],
            },
        ),
        Tool(
            name="memory_graph",
            title="Relationship Graph",
            description=(
                "Explore the entity relationship graph: project networks, connection paths, "
                "hub entities, dormant relationships, and reconnection suggestions. "
                "Use operation='network' for project stakeholders, 'path' for connection chains, "
                "'hubs' for key connectors, 'dormant' for neglected relationships, "
                "'reconnect' for actionable reconnection suggestions."
            ),
            annotations=ToolAnnotations(readOnlyHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["network", "path", "hubs", "dormant", "reconnect"],
                        "description": "Which graph query to run",
                    },
                    "project": {
                        "type": "string",
                        "description": "Project name (for network operation)",
                    },
                    "entity_a": {
                        "type": "string",
                        "description": "First entity (for path operation)",
                    },
                    "entity_b": {
                        "type": "string",
                        "description": "Second entity (for path operation)",
                    },
                    "max_depth": {
                        "type": "string",
                        "description": "Maximum hops to search (for path, default 4)",
                        "default": 4,
                    },
                    "min_connections": {
                        "type": "string",
                        "description": "Minimum relationships to be a hub (for hubs, default 5)",
                        "default": 5,
                    },
                    "entity_type": {
                        "type": "string",
                        "enum": ["person", "organization", "project"],
                        "description": "Filter by entity type (for hubs)",
                    },
                    "days": {
                        "type": "string",
                        "description": "Days without activity (for dormant, default 60)",
                        "default": 60,
                    },
                    "min_strength": {
                        "type": "number",
                        "description": "Minimum relationship strength (for dormant, default 0.3)",
                        "default": 0.3,
                    },
                    "limit": {
                        "type": "string",
                        "description": "Maximum results to return",
                        "default": 20,
                    },
                },
                "required": ["operation"],
            },
        ),
        Tool(
            name="memory_entities",
            title="Entity Management",
            description=(
                "Create, search, merge, delete, or overview entities (people, projects, orgs). "
                "Use operation='create' to add/update, 'search' to find, 'merge' to deduplicate, "
                "'delete' to soft-delete, 'overview' for community-style summaries with relationship maps."
            ),
            annotations=ToolAnnotations(destructiveHint=False),
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["create", "search", "merge", "delete", "overview"],
                        "description": "Which entity operation to perform",
                    },
                    "name": {
                        "type": "string",
                        "description": "Entity name (for create)",
                    },
                    "type": {
                        "type": "string",
                        "enum": ["person", "organization", "project", "concept", "location"],
                        "description": "Entity type (for create, default person)",
                        "default": "person",
                    },
                    "description": {
                        "type": "string",
                        "description": "Entity description (for create)",
                    },
                    "aliases": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Alternative names (for create)",
                    },
                    "query": {
                        "type": "string",
                        "description": "Search query (for search)",
                    },
                    "types": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Filter by entity types (for search)",
                    },
                    "source_id": {
                        "type": "string",
                        "description": "Entity ID to merge FROM (for merge)",
                    },
                    "target_id": {
                        "type": "string",
                        "description": "Entity ID to merge INTO (for merge)",
                    },
                    "entity_id": {
                        "type": "string",
                        "description": "Entity ID (for delete)",
                    },
                    "reason": {
                        "type": "string",
                        "description": "Reason for merge or delete",
                    },
                    "entities": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Entity names for overview",
                    },
                    "include_network": {
                        "type": "boolean",
                        "description": "Include 1-hop connections in overview (default true)",
                        "default": True,
                    },
                    "include_summaries": {
                        "type": "boolean",
                        "description": "Include cached summaries in overview (default true)",
                        "default": True,
                    },
                    "limit": {
                        "type": "string",
                        "description": "Maximum results (for search, default 10)",
                        "default": 10,
                    },
                },
                "required": ["operation"],
            },
        ),
        Tool(
            name="memory_vault",
            title="Obsidian Vault",
            description=(
                "Manage the Obsidian vault integration: sync memory to vault, check status, "
                "generate canvas dashboards, or import user edits back. "
                "Use operation='sync' to export, 'status' to check, 'canvas' to generate dashboards, "
                "'import' to pull in user edits from the vault."
            ),
            annotations=ToolAnnotations(destructiveHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["sync", "status", "canvas", "import"],
                        "description": "Which vault operation to perform",
                    },
                    "full": {
                        "type": "boolean",
                        "description": "Full rebuild for sync (default false = incremental)",
                        "default": False,
                    },
                    "canvas_type": {
                        "type": "string",
                        "enum": ["relationship_map", "morning_brief", "project_board", "all"],
                        "description": "Type of canvas to generate (for canvas operation)",
                    },
                    "project_name": {
                        "type": "string",
                        "description": "Project name (for canvas project_board type)",
                    },
                },
                "required": ["operation"],
            },
        ),
        Tool(
            name="memory_modify",
            title="Modify Memories & Relationships",
            description=(
                "Correct, invalidate, or end relationships. "
                "Use operation='correct' to fix a memory's content (preserves history), "
                "'invalidate' to mark a memory as no longer true, "
                "'invalidate_relationship' to end a relationship between entities."
            ),
            annotations=ToolAnnotations(destructiveHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["correct", "invalidate", "invalidate_relationship"],
                        "description": "Which modification to perform",
                    },
                    "memory_id": {
                        "type": "string",
                        "description": "Memory ID (for correct, invalidate)",
                    },
                    "correction": {
                        "type": "string",
                        "description": "Corrected content (for correct)",
                    },
                    "reason": {
                        "type": "string",
                        "description": "Reason for the change",
                    },
                    "source": {
                        "type": "string",
                        "description": "Source entity name (for invalidate_relationship)",
                    },
                    "target": {
                        "type": "string",
                        "description": "Target entity name (for invalidate_relationship)",
                    },
                    "relationship": {
                        "type": "string",
                        "description": "Relationship type (for invalidate_relationship)",
                    },
                },
                "required": ["operation"],
            },
        ),
        Tool(
            name="memory_session",
            title="Session Lifecycle",
            description=(
                "Session management: buffer turns, load context, or check unsummarized sessions. "
                "Use operation='buffer' to record a conversation turn, 'context' to load session "
                "start context, 'unsummarized' to find orphaned sessions needing retroactive summaries."
            ),
            annotations=ToolAnnotations(destructiveHint=False),
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["buffer", "context", "unsummarized"],
                        "description": "Which session operation to perform",
                    },
                    "user_content": {
                        "type": "string",
                        "description": "What the user said (for buffer)",
                    },
                    "assistant_content": {
                        "type": "string",
                        "description": "What the assistant said (for buffer)",
                    },
                    "episode_id": {
                        "type": "string",
                        "description": "Episode ID from previous buffer call (for buffer)",
                    },
                    "source": {
                        "type": "string",
                        "description": "Origin channel (for buffer): claude_code, telegram, slack",
                    },
                    "token_budget": {
                        "type": "string",
                        "enum": ["brief", "normal", "full"],
                        "description": "How much context to load (for context, default normal)",
                        "default": "normal",
                    },
                },
                "required": ["operation"],
            },
        ),
        Tool(
            name="memory_document",
            title="Document Storage",
            description=(
                "Store or search documents (transcripts, emails, files). "
                "Use operation='store' to save a document with entity links and provenance, "
                "'search' to find documents by entity, source type, or text query."
            ),
            annotations=ToolAnnotations(destructiveHint=False),
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["store", "search"],
                        "description": "Which document operation to perform",
                    },
                    "content": {
                        "type": "string",
                        "description": "Raw text content (for store)",
                    },
                    "filename": {
                        "type": "string",
                        "description": "Display filename (for store)",
                    },
                    "source_type": {
                        "type": "string",
                        "enum": ["gmail", "transcript", "upload", "capture", "session"],
                        "description": "Document source type",
                        "default": "capture",
                    },
                    "summary": {
                        "type": "string",
                        "description": "Brief summary (for store)",
                    },
                    "about": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Entity names this document relates to",
                    },
                    "memory_ids": {
                        "type": "array",
                        "items": {"type": "integer"},
                        "description": "Memory IDs to link (for store)",
                    },
                    "source_ref": {
                        "type": "string",
                        "description": "External reference (for store)",
                    },
                    "query": {
                        "type": "string",
                        "description": "Search text (for search)",
                    },
                    "entity": {
                        "type": "string",
                        "description": "Filter by entity (for search)",
                    },
                    "limit": {
                        "type": "string",
                        "description": "Maximum results (for search, default 20)",
                        "default": 20,
                    },
                },
                "required": ["operation"],
            },
        ),
        Tool(
            name="memory_provenance",
            title="Provenance & Audit",
            description=(
                "Trace memory origins, get full audit trails, or verify hash chain integrity. "
                "Use operation='trace' to reconstruct a memory's full provenance chain, "
                "'audit' to get the audit history for an entity or memory, "
                "'verify_chain' to check SHA-256 hash chain integrity across all memories."
            ),
            annotations=ToolAnnotations(readOnlyHint=True),
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["trace", "audit", "verify_chain"],
                        "description": "Which provenance operation to perform",
                    },
                    "memory_id": {
                        "type": "string",
                        "description": "Memory ID (for trace, or audit by memory)",
                    },
                    "entity_id": {
                        "type": "string",
                        "description": "Entity ID (for audit by entity)",
                    },
                    "limit": {
                        "type": "string",
                        "description": "Maximum audit entries (default 20)",
                        "default": 20,
                    },
                },
                "required": ["operation"],
            },
        ),
        # ── Lifecycle / Sacred memory tools ──
        Tool(
            name="memory_lifecycle",
            title="Memory Lifecycle",
            description="Manage memory lifecycle tiers (sacred/active/cooling/archived) and entity close-circle status.",
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["set", "protect", "archive", "restore", "status"],
                        "description": (
                            "set: change tier by fact_id. protect: mark entity close-circle. "
                            "archive/restore: archive or restore memory. status: show lifecycle stats."
                        ),
                    },
                    "fact_id": {
                        "type": "string",
                        "description": "Memory fact_id UUID. Required for set/archive/restore.",
                    },
                    "entity": {
                        "type": "string",
                        "description": "Entity name. Required for protect.",
                    },
                    "tier": {
                        "type": "string",
                        "enum": ["sacred", "active", "cooling", "archived"],
                        "description": "Target tier. Required for set.",
                    },
                    "reason": {
                        "type": "string",
                        "description": "Reason for the operation.",
                    },
                },
                "required": ["operation"],
            },
        ),
        Tool(
            name="memory_context",
            title="Build Context Window",
            description="Build a token-budgeted context window with sacred facts always included. Uses the Context Relevance Engine (CRE).",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "What context to build for",
                    },
                    "token_budget": {
                        "type": "string",
                        "description": "Maximum tokens (default: 8000)",
                    },
                    "include_sacred": {
                        "type": "boolean",
                        "description": "Include sacred facts (default: true)",
                    },
                    "entity": {
                        "type": "string",
                        "description": "Scope sacred facts to this entity",
                    },
                },
                "required": ["query"],
            },
        ),
        Tool(
            name="memory_checkpoint",
            title="Database Checkpoints",
            description="Save, list, or restore database checkpoints (labeled backups).",
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["save", "list", "load"],
                        "description": "save: create checkpoint. list: show available. load: returns restore instructions.",
                    },
                    "name": {
                        "type": "string",
                        "description": "Checkpoint name/label. Required for save/load.",
                    },
                },
                "required": ["operation"],
            },
        ),
        Tool(
            name="memory_rollback",
            title="Temporal Rollback",
            description="Set a temporal view filter so recall only returns memories created before a given timestamp. Non-destructive.",
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["set", "clear", "status"],
                        "description": "set: set view_as_of timestamp. clear: reset to current. status: show current.",
                    },
                    "timestamp": {
                        "type": "string",
                        "description": "ISO timestamp for view_as_of. Required for set.",
                    },
                },
                "required": ["operation"],
            },
        ),
    ]
    return ListToolsResult(tools=tools)


@server.call_tool()
async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
    """Handle tool calls via dispatch registry."""
    db = get_db()
    try:
        # Normalize parameter-name aliases at the MCP boundary so handlers
        # only ever see canonical parameter names. Purely additive: tools
        # without registered aliases are unchanged. See _PARAM_ALIASES.
        arguments = _apply_parameter_aliases(name, arguments)
        with db.transaction():
            handler = _TOOL_HANDLERS.get(name)
            if handler:
                return await handler(
                    arguments, db=db, config=None, logger=logger, tool_name=name,
                )
            else:
                return CallToolResult(
                    content=[
                        TextContent(
                            type="text",
                            text=json.dumps({"error": f"Unknown tool: {name}"}),
                        )
                    ],
                    isError=True,
                )

    except Exception as e:
        logger.exception(f"Error in tool {name}")
        return CallToolResult(
            content=[
                TextContent(
                    type="text",
                    text=json.dumps({"error": str(e)}),
                )
            ],
            isError=True,
        )


def _build_briefing() -> str:
    """
    Build a compact session briefing (~500 tokens).

    Returns aggregate counts and one-line highlights instead of full data.
    Designed to replace "load all markdown files at session start" with a
    single lightweight call.
    """
    from datetime import datetime, timedelta

    db = get_db()
    lines = []
    lines.append("# Session Briefing\n")

    # 0. Pending database consolidation alert
    try:
        unified_row = db.execute(
            "SELECT value FROM _meta WHERE key = 'unified_db'", fetch=True
        )
        is_unified = unified_row and unified_row[0]["value"] == "true" if unified_row else False

        if is_unified:
            # Check if consolidation just happened (within last 5 minutes)
            try:
                ts_row = db.execute(
                    "SELECT updated_at FROM _meta WHERE key = 'unified_db'", fetch=True
                )
                if ts_row and ts_row[0]["updated_at"]:
                    from datetime import timedelta as _td
                    consolidated_at = parse_naive(ts_row[0]["updated_at"])
                    if (datetime.utcnow() - consolidated_at) < _td(minutes=5):
                        # Just consolidated, include stats
                        mem_row = db.execute("SELECT COUNT(*) as c FROM memories", fetch=True)
                        ent_row = db.execute("SELECT COUNT(*) as c FROM entities WHERE deleted_at IS NULL", fetch=True)
                        rel_row = db.execute("SELECT COUNT(*) as c FROM relationships", fetch=True)
                        mem_c = mem_row[0]["c"] if mem_row else 0
                        ent_c = ent_row[0]["c"] if ent_row else 0
                        rel_c = rel_row[0]["c"] if rel_row else 0
                        lines.append(
                            f"**✅ DATABASE CONSOLIDATED:** All memories unified into claudia.db. "
                            f"Current state: {mem_c:,} memories, {ent_c:,} entities, {rel_c:,} relationships. "
                            f"Include these stats in your greeting and explain that backups now run automatically "
                            f"(daily at 2:30 AM, weekly on Sundays, kept in ~/.claudia/backups/)."
                        )
            except Exception:
                pass

        if not is_unified:
            import re
            from pathlib import Path
            memory_dir = Path(get_config().db_path).parent
            if memory_dir.exists():
                hash_pattern = re.compile(r"^[0-9a-f]{12}\.db$")
                hash_dbs = [f for f in memory_dir.iterdir() if hash_pattern.match(f.name)]
                if hash_dbs:
                    # Quick count of total memories across hash DBs
                    import sqlite3 as _sql3
                    total_mem = 0
                    dbs_with_data = 0
                    for hdb in hash_dbs:
                        try:
                            conn = _sql3.connect(f"file:{hdb}?mode=ro", uri=True, timeout=2)
                            row = conn.execute("SELECT COUNT(*) FROM memories").fetchone()
                            cnt = row[0] if row else 0
                            conn.close()
                            if cnt > 0:
                                total_mem += cnt
                                dbs_with_data += 1
                        except Exception:
                            pass
                    if dbs_with_data > 0:
                        lines.append(
                            f"**⚠️ PENDING CONSOLIDATION:** {dbs_with_data} legacy database{'s' if dbs_with_data != 1 else ''} "
                            f"with {total_mem:,} memories waiting to be merged into claudia.db. "
                            f"Tell the user about this and ask if they want to consolidate now. "
                            f"To trigger: restart Claude Code (the daemon auto-merges on startup), "
                            f"or run `python -m claudia_memory --merge-databases` from terminal. "
                            f"Show them the counts so they know what data is at stake."
                        )
    except Exception as e:
        logger.debug(f"Briefing consolidation check failed: {e}")

    # 1. Active commitments count + stale count
    try:
        total_row = db.execute(
            "SELECT COUNT(*) as cnt FROM memories WHERE type = 'commitment' AND importance > 0.1 AND invalidated_at IS NULL",
            fetch=True,
        )
        total_commitments = total_row[0]["cnt"] if total_row else 0

        stale_cutoff = (datetime.utcnow() - timedelta(days=7)).isoformat()
        stale_row = db.execute(
            "SELECT COUNT(*) as cnt FROM memories WHERE type = 'commitment' AND importance > 0.1 AND invalidated_at IS NULL AND created_at < ?",
            (stale_cutoff,),
            fetch=True,
        )
        stale_commitments = stale_row[0]["cnt"] if stale_row else 0

        if total_commitments > 0:
            stale_note = f" ({stale_commitments} older than 7d)" if stale_commitments else ""
            lines.append(f"**Commitments:** {total_commitments} active{stale_note}")
    except Exception as e:
        logger.debug(f"Briefing commitments failed: {e}")

    # 2. Cooling relationships (30d+ no mention)
    try:
        cooling_cutoff = (datetime.utcnow() - timedelta(days=30)).isoformat()
        cooling_row = db.execute(
            """
            SELECT COUNT(*) as cnt FROM entities
            WHERE type = 'person' AND importance > 0.3
              AND deleted_at IS NULL
              AND updated_at < ?
            """,
            (cooling_cutoff,),
            fetch=True,
        )
        cooling_count = cooling_row[0]["cnt"] if cooling_row else 0
        if cooling_count > 0:
            lines.append(f"**Cooling relationships:** {cooling_count} people not mentioned in 30+ days")
    except Exception as e:
        logger.debug(f"Briefing cooling failed: {e}")

    # 3. Unread gateway messages
    try:
        unread_row = db.execute(
            "SELECT COUNT(*) as cnt FROM episodes WHERE source IN ('telegram', 'slack') AND ingested_at IS NULL",
            fetch=True,
        )
        unread_count = unread_row[0]["cnt"] if unread_row else 0
        if unread_count > 0:
            lines.append(f"**Unread messages:** {unread_count} from gateway")
    except Exception as e:
        logger.debug(f"Briefing unread failed: {e}")

    # 4. Top prediction (1 line)
    try:
        pred_row = db.execute(
            """
            SELECT content, prediction_type FROM predictions
            WHERE expires_at > datetime('now') AND is_shown = 0
            ORDER BY priority DESC
            LIMIT 1
            """,
            fetch=True,
        )
        if pred_row:
            p = pred_row[0]
            lines.append(f"**Top prediction:** [{p['prediction_type']}] {p['content'][:100]}")
    except Exception as e:
        logger.debug(f"Briefing prediction failed: {e}")

    # 4b. Active reflections count + top 1
    try:
        reflections = get_active_reflections(limit=3, min_importance=0.6)
        if reflections:
            top_r = reflections[0]
            rtype = top_r.reflection_type or "observation"
            lines.append(f"**Active reflections:** {len(reflections)} ({rtype}: {top_r.content[:80]}...)")
    except Exception as e:
        logger.debug(f"Briefing reflections failed: {e}")

    # 5. Recent activity count (24h)
    try:
        recent_cutoff = (datetime.utcnow() - timedelta(hours=24)).isoformat()
        recent_row = db.execute(
            "SELECT COUNT(*) as cnt FROM memories WHERE created_at > ?",
            (recent_cutoff,),
            fetch=True,
        )
        recent_count = recent_row[0]["cnt"] if recent_row else 0
        lines.append(f"**Recent activity:** {recent_count} memories in last 24h")
    except Exception as e:
        logger.debug(f"Briefing recent failed: {e}")

    # 6. Embedding health check
    try:
        mem_total = db.execute("SELECT COUNT(*) as c FROM memories WHERE invalidated_at IS NULL", fetch=True)
        emb_total = db.execute("SELECT COUNT(*) as c FROM memory_embeddings", fetch=True)
        mem_c = mem_total[0]["c"] if mem_total else 0
        emb_c = emb_total[0]["c"] if emb_total else 0
        if mem_c > 0:
            coverage = (emb_c / mem_c) * 100
            if coverage < 90:
                gap = mem_c - emb_c
                # Check _meta for backfill status to give accurate guidance
                status_hint = "Start Ollama and restart daemon to generate embeddings."
                try:
                    repair_row = db.execute(
                        "SELECT value FROM _meta WHERE key = 'indexes_repaired'",
                        fetch=True,
                    )
                    if repair_row and repair_row[0]["value"]:
                        repair_info = repair_row[0]["value"]
                        if "backfill started" in repair_info:
                            status_hint = "Backfill was started on this run. Check daemon logs for progress."
                        elif coverage > 0:
                            status_hint = "Partial embeddings exist. Restart daemon to trigger backfill for the rest."
                except Exception:
                    pass
                lines.append(
                    f"**Embedding coverage:** {emb_c}/{mem_c} ({coverage:.0f}%). "
                    f"{gap} memories lack vector embeddings. "
                    f"{status_hint} "
                    f"Recall uses keyword fallback for unembedded memories."
                )
    except Exception as e:
        logger.debug(f"Briefing embedding health failed: {e}")

    if len(lines) <= 1:
        lines.append("No context available yet. This appears to be a fresh workspace.")

    return "\n".join(lines)


def _build_telegram_inbox(limit: int = 10, mark_read: bool = True) -> str:
    """
    Fetch unread gateway episodes and recent gateway-sourced memories.
    Marks returned episodes as ingested (read) if mark_read is True.

    Returns formatted text block with conversation summaries and notes.
    """
    db = get_db()
    sections = []
    episode_ids = []

    # 1. Get unread episodes from gateway channels
    try:
        unread_episodes = db.execute(
            """
            SELECT id, session_id, narrative, started_at, turn_count, source
            FROM episodes
            WHERE source IN ('telegram', 'slack')
              AND ingested_at IS NULL
            ORDER BY started_at DESC
            LIMIT ?
            """,
            (limit,),
            fetch=True,
        ) or []

        if unread_episodes:
            sections.append(f"## Telegram Inbox ({len(unread_episodes)} unread)\n")
            for ep in unread_episodes:
                episode_ids.append(ep["id"])
                source = ep["source"] or "gateway"
                started = ep["started_at"] or "unknown"
                turn_count = ep["turn_count"] or 0

                sections.append(f"### {source.title()} conversation ({started[:16]}, {turn_count} turns)")

                # Fetch turns for this episode
                turns = db.execute(
                    """
                    SELECT user_content, assistant_content, turn_number
                    FROM turn_buffer
                    WHERE episode_id = ?
                    ORDER BY turn_number ASC
                    """,
                    (ep["id"],),
                    fetch=True,
                ) or []

                if turns:
                    for t in turns:
                        if t["user_content"]:
                            sections.append(f"  **User:** {t['user_content'][:200]}")
                        if t["assistant_content"]:
                            sections.append(f"  **Claudia:** {t['assistant_content'][:200]}")
                else:
                    # Fall back to narrative if turns were already archived
                    narrative = ep["narrative"]
                    if narrative:
                        preview = narrative[:300] + "..." if len(narrative) > 300 else narrative
                        sections.append(f"  {preview}")
                    else:
                        sections.append(f"  (no content available)")

                sections.append("")
    except Exception as e:
        logger.debug(f"Could not fetch unread episodes: {e}")

    # 2. Get recent gateway-sourced memories (48h)
    try:
        recall_svc = get_recall_service()
        telegram_memories = recall_svc.get_recent_memories(
            limit=limit,
            hours=48,
            source_filter="telegram",
        )
        if telegram_memories:
            sections.append(f"## Recent Telegram Memories ({len(telegram_memories)})\n")
            for m in telegram_memories:
                entities_str = ", ".join(m.entities[:3]) if m.entities else ""
                prefix = f"[{m.type}]"
                if entities_str:
                    prefix += f" [{entities_str}]"
                sections.append(f"- {prefix} {m.content}")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not fetch telegram memories: {e}")

    # 3. Mark episodes as ingested
    if mark_read and episode_ids:
        try:
            placeholders = ", ".join(["?" for _ in episode_ids])
            db.execute(
                f"UPDATE episodes SET ingested_at = datetime('now') WHERE id IN ({placeholders})",
                tuple(episode_ids),
            )
            logger.debug(f"Marked {len(episode_ids)} episodes as ingested")
        except Exception as e:
            logger.warning(f"Could not mark episodes as ingested: {e}")

    if not sections:
        return "No new messages from Telegram or Slack."

    return "\n".join(sections)


def _build_session_context(token_budget: str = "normal") -> str:
    """
    Assemble a pre-formatted session context block for session start.

    Token budget tiers control how much data is returned:
    - brief:  5 memories, 3 predictions, 2 episodes, 3 commitments, 3 reflections
    - normal: 10 memories, 5 predictions, 3 episodes, 5 commitments, 5 reflections
    - full:   20 memories, 10 predictions, 5 episodes, 10 commitments, 8 reflections
    """
    budgets = {
        "brief":  {"memories": 5,  "predictions": 3,  "episodes": 2, "commitments": 3, "reflections": 3},
        "normal": {"memories": 10, "predictions": 5,  "episodes": 3, "commitments": 5, "reflections": 5},
        "full":   {"memories": 20, "predictions": 10, "episodes": 5, "commitments": 10, "reflections": 8},
    }
    limits = budgets.get(token_budget, budgets["normal"])

    sections = []
    sections.append("# Session Context\n")

    # 1. Unsummarized sessions
    try:
        unsummarized = get_unsummarized_turns()
        if unsummarized:
            sections.append(f"## Unsummarized Sessions ({len(unsummarized)})\n")
            sections.append("**Action needed:** Generate retroactive summaries using `memory_end_session` for each.\n")
            for session in unsummarized:
                ep_id = session.get("episode_id", "?")
                turn_count = session.get("turn_count", 0)
                started = session.get("started_at", "unknown")
                sections.append(f"- Episode {ep_id}: {turn_count} turns (started {started})")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not fetch unsummarized sessions: {e}")

    # 2. Telegram/Slack Inbox (unread gateway messages)
    try:
        inbox_text = _build_telegram_inbox(limit=limits.get("memories", 10), mark_read=True)
        if inbox_text and "No new messages" not in inbox_text:
            sections.append(inbox_text)
    except Exception as e:
        logger.debug(f"Could not fetch telegram inbox: {e}")

    # 3. Recent memories (48h)
    try:
        recall_svc = get_recall_service()
        recent = recall_svc.get_recent_memories(
            limit=limits["memories"],
            hours=48,
        )
        if recent:
            sections.append(f"## Recent Context (48h) — {len(recent)} memories\n")
            for m in recent:
                entities_str = ", ".join(m.entities[:3]) if m.entities else ""
                prefix = f"[{m.type}]"
                if entities_str:
                    prefix += f" [{entities_str}]"
                sections.append(f"- {prefix} {m.content}")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not fetch recent memories: {e}")

    # 3. Active predictions
    try:
        predictions = get_predictions(limit=limits["predictions"])
        if predictions:
            sections.append(f"## Predictions & Insights\n")
            for p in predictions:
                ptype = p.get("prediction_type", "insight")
                content = p.get("content", "")
                sections.append(f"- **{ptype}**: {content}")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not fetch predictions: {e}")

    # 3b. Active reflections (learnings about working with user)
    try:
        reflections = get_active_reflections(
            limit=limits.get("reflections", 5),
            min_importance=0.6,
        )
        if reflections:
            sections.append(f"## Active Reflections ({len(reflections)})\n")
            sections.append("*Apply silently unless user asks. Observations inform style, learnings modify approach.*\n")
            for r in reflections:
                rtype = r.reflection_type or "observation"
                about = f" [{r.about_entity}]" if r.about_entity else ""
                count = f" (confirmed {r.aggregation_count}x)" if r.aggregation_count > 1 else ""
                sections.append(f"- **{rtype}**{about}: {r.content}{count}")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not fetch reflections: {e}")

    # 4. Active commitments (7 days)
    try:
        commitments = recall_svc.get_recent_memories(
            limit=limits["commitments"],
            memory_types=["commitment"],
            hours=168,  # 7 days
        )
        if commitments:
            sections.append(f"## Active Commitments (7d)\n")
            for c in commitments:
                entities_str = ", ".join(c.entities[:3]) if c.entities else ""
                prefix = f"[{entities_str}]" if entities_str else ""
                sections.append(f"- {prefix} {c.content} (created {c.created_at[:10]})")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not fetch commitments: {e}")

    # 5. Recent episode narratives
    try:
        db = get_db()
        episode_rows = db.execute(
            """
            SELECT id, session_id, narrative, started_at, key_topics
            FROM episodes
            WHERE is_summarized = 1
            ORDER BY started_at DESC
            LIMIT ?
            """,
            (limits["episodes"],),
            fetch=True,
        ) or []
        if episode_rows:
            sections.append(f"## Recent Sessions\n")
            for ep in episode_rows:
                narrative = ep["narrative"] or ""
                preview = narrative[:150] + "..." if len(narrative) > 150 else narrative
                topics = json.loads(ep["key_topics"]) if ep["key_topics"] else []
                topic_str = ", ".join(topics[:4]) if topics else "no topics"
                sections.append(f"- **Session {ep['id']}** ({ep['started_at'][:10]}) [{topic_str}]")
                if preview:
                    sections.append(f"  {preview}")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not fetch episodes: {e}")

    if len(sections) <= 1:
        sections.append("No context available yet. This appears to be a fresh workspace.\n")

    return "\n".join(sections)


def _build_morning_context() -> str:
    """
    Build a curated morning digest with stale commitments, cooling relationships,
    cross-entity connections, predictions, and recent activity.
    """
    from datetime import datetime, timedelta

    sections = []
    sections.append("# Morning Context Digest\n")

    consolidate_svc = get_consolidate_service()
    recall_svc = get_recall_service()
    db = get_db()

    # 1. Stale commitments (importance > 0.3, created > 3 days ago)
    try:
        cutoff = (datetime.utcnow() - timedelta(days=3)).isoformat()
        stale = db.execute(
            """
            SELECT m.id, m.content, m.importance, m.created_at,
                   GROUP_CONCAT(e.name) as entity_names
            FROM memories m
            LEFT JOIN memory_entities me ON m.id = me.memory_id
            LEFT JOIN entities e ON me.entity_id = e.id
            WHERE m.type = 'commitment' AND m.importance > 0.3 AND m.created_at < ?
            GROUP BY m.id
            ORDER BY m.created_at ASC
            LIMIT 10
            """,
            (cutoff,),
            fetch=True,
        ) or []

        if stale:
            sections.append(f"## Stale Commitments ({len(stale)})\n")
            for c in stale:
                days_old = (datetime.utcnow() - parse_naive(c["created_at"])).days
                entities = c["entity_names"] or ""
                prefix = f"[{entities}] " if entities else ""
                sections.append(f"- {prefix}{c['content'][:100]} ({days_old}d old, importance: {c['importance']:.1f})")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not fetch stale commitments: {e}")

    # 2. Cooling relationships
    try:
        cooling = consolidate_svc._detect_cooling_relationships()
        if cooling:
            sections.append(f"## Cooling Relationships ({len(cooling)})\n")
            for p in cooling:
                sections.append(f"- {p.description} (confidence: {p.confidence:.1f})")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not detect cooling relationships: {e}")

    # 3. Cross-entity connections
    try:
        cross = consolidate_svc._detect_cross_entity_patterns()
        if cross:
            sections.append(f"## Potential Connections ({len(cross)})\n")
            for p in cross:
                sections.append(f"- {p.description} (confidence: {p.confidence:.1f})")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not detect cross-entity patterns: {e}")

    # 3.5 Relationship Health Dashboard
    try:
        # Dormant relationships (30/60/90 day buckets)
        dormant_30 = get_dormant_relationships(days=30, min_strength=0.4, limit=5)
        dormant_60 = get_dormant_relationships(days=60, min_strength=0.3, limit=5)
        dormant_90 = get_dormant_relationships(days=90, min_strength=0.2, limit=5)

        # Deduplicate (90 includes 60 includes 30)
        seen_ids = set()
        buckets = {"30d": [], "60d": [], "90d": []}
        for rel in dormant_30:
            buckets["30d"].append(rel)
            seen_ids.add(rel["relationship_id"])
        for rel in dormant_60:
            if rel["relationship_id"] not in seen_ids:
                buckets["60d"].append(rel)
                seen_ids.add(rel["relationship_id"])
        for rel in dormant_90:
            if rel["relationship_id"] not in seen_ids:
                buckets["90d"].append(rel)

        total_dormant = len(buckets["30d"]) + len(buckets["60d"]) + len(buckets["90d"])
        if total_dormant > 0:
            sections.append(f"## Relationship Health ({total_dormant} need attention)\n")
            if buckets["30d"]:
                sections.append("**30+ days dormant (consider reaching out):**")
                for rel in buckets["30d"][:3]:
                    sections.append(f"- {rel['source']['name']} ↔ {rel['target']['name']} ({rel['days_dormant']}d)")
            if buckets["60d"]:
                sections.append("\n**60+ days dormant (relationship cooling):**")
                for rel in buckets["60d"][:3]:
                    sections.append(f"- {rel['source']['name']} ↔ {rel['target']['name']} ({rel['days_dormant']}d)")
            if buckets["90d"]:
                sections.append("\n**90+ days dormant (at risk):**")
                for rel in buckets["90d"][:3]:
                    sections.append(f"- {rel['source']['name']} ↔ {rel['target']['name']} ({rel['days_dormant']}d)")
            sections.append("")

        # Introduction opportunities
        intro_patterns = consolidate_svc._detect_introduction_opportunities()
        if intro_patterns:
            sections.append(f"## Introduction Opportunities ({len(intro_patterns)})\n")
            for p in intro_patterns[:5]:
                sections.append(f"- {p.description}")
            sections.append("")

        # Forming clusters
        cluster_patterns = consolidate_svc._detect_cluster_forming()
        if cluster_patterns:
            sections.append(f"## Forming Groups ({len(cluster_patterns)})\n")
            for p in cluster_patterns[:3]:
                sections.append(f"- {p.description}")
            sections.append("")

    except Exception as e:
        logger.debug(f"Could not build relationship health: {e}")

    # 4. Active predictions
    try:
        predictions = get_predictions(limit=10)
        if predictions:
            sections.append(f"## Predictions & Insights\n")
            for p in predictions:
                ptype = p.get("prediction_type", "insight")
                sections.append(f"- **{ptype}**: {p.get('content', '')}")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not fetch predictions: {e}")

    # 5. Recent activity (72h)
    try:
        recent = recall_svc.get_recent_memories(limit=15, hours=72)
        if recent:
            sections.append(f"## Recent Activity (72h) - {len(recent)} memories\n")
            for m in recent:
                entities_str = ", ".join(m.entities[:3]) if m.entities else ""
                prefix = f"[{m.type}]"
                if entities_str:
                    prefix += f" [{entities_str}]"
                sections.append(f"- {prefix} {m.content[:100]}")
            sections.append("")
    except Exception as e:
        logger.debug(f"Could not fetch recent activity: {e}")

    if len(sections) <= 1:
        sections.append("No data available yet. Start by telling me about your work and the people you interact with.\n")

    return "\n".join(sections)


def _write_startup_manifest(db_path: str, stdin_info: str, tool_count: int = 0) -> None:
    """Write a session manifest so diagnostics can verify the daemon started.

    The manifest at ~/.claudia/daemon-session.json proves the daemon reached
    the MCP stdio loop. If this file is missing, the daemon never started.
    If present with a stale PID, the daemon started but died.
    """
    import os
    from datetime import datetime as _dt
    from pathlib import Path as _Path

    manifest_path = _Path.home() / ".claudia" / "daemon-session.json"
    manifest_path.parent.mkdir(parents=True, exist_ok=True)
    manifest = {
        "pid": os.getpid(),
        "started_at": _dt.now().isoformat(timespec="seconds"),
        "db_path": db_path,
        "project_dir": os.environ.get("CLAUDIA_WORKSPACE_PATH", ""),
        "stdin_type": stdin_info,
        "tool_count": tool_count,
    }
    try:
        manifest_path.write_text(json.dumps(manifest, indent=2))
        logger.info(f"Startup manifest written to {manifest_path}")
    except Exception as e:
        logger.warning(f"Could not write startup manifest: {e}")


def _cleanup_startup_manifest() -> None:
    """Update the session manifest with exit timestamp on clean shutdown."""
    from datetime import datetime as _dt
    from pathlib import Path as _Path

    manifest_path = _Path.home() / ".claudia" / "daemon-session.json"
    if not manifest_path.exists():
        return
    try:
        data = json.loads(manifest_path.read_text())
        data["exited_at"] = _dt.now().isoformat(timespec="seconds")
        manifest_path.write_text(json.dumps(data, indent=2))
    except Exception:
        pass


async def run_server():
    """Run the MCP server via stdio transport.

    The server stays alive as long as stdin remains open (i.e., the MCP client
    keeps the pipe connected). When stdin closes, the server exits cleanly.
    """
    # Initialize database
    db = get_db()
    db.initialize()

    # Log stdin state for diagnostics (helps debug "exits immediately" issues)
    stdin_info = "unknown"
    try:
        import os
        import stat
        fd = sys.stdin.fileno()
        mode = os.fstat(fd).st_mode
        if stat.S_ISFIFO(mode):
            stdin_info = "pipe"
        elif stat.S_ISREG(mode):
            stdin_info = "file"
        elif stat.S_ISCHR(mode):
            stdin_info = "tty/char-device"
        else:
            stdin_info = f"mode={oct(mode)}"
    except Exception:
        stdin_info = "not available (no fileno)"
    logger.info(f"Starting Claudia Memory MCP server (stdin={stdin_info})")

    try:
        async with stdio_server() as (read_stream, write_stream):
            # Count registered tools for the manifest
            try:
                tools_result = await list_tools()
                tool_count = len(tools_result.tools) if hasattr(tools_result, "tools") else 0
            except Exception:
                tool_count = 0

            # Write startup manifest BEFORE entering the message loop
            _write_startup_manifest(
                db_path=str(db.db_path),
                stdin_info=stdin_info,
                tool_count=tool_count,
            )

            await server.run(read_stream, write_stream, server.create_initialization_options())
        logger.info("MCP server exited normally (stdin closed by client)")
    except Exception:
        logger.exception("MCP server crashed with exception")
    finally:
        _cleanup_startup_manifest()


def main():
    """Entry point for the MCP server"""
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        handlers=[logging.StreamHandler(sys.stderr)],
    )

    asyncio.run(run_server())


if __name__ == "__main__":
    # Quick startup test mode for diagnostics
    if "--test" in sys.argv:
        # Verify we can import all required modules and list tools
        try:
            # Test that server is properly configured
            assert server is not None
            print("MCP server OK")
            sys.exit(0)
        except Exception as e:
            print(f"MCP server ERROR: {e}")
            sys.exit(1)

    main()
