'use strict';
const fs = require('node:fs');
const path = require('node:path');
const { randomUUID: uuidv4 } = require('node:crypto');
const brain = require('./brain');
const { createSessionIngestService } = require('./memory/session-ingest-service');
const ctmSessionContext = require('./memory/ctm-session-context');
const ctmContextClient = require('./memory/ctm-context-client');
const ctmOperationalContext = require('./memory/ctm-operational-context');
const { collectIngestRecords } = require('./sources/base');
const { ensureBuiltinSourceAdapters } = require('./sources/builtin');
const sourceRegistry = require('./sources/registry');
const { RECORD_TYPES } = require('./sources/record-types');
const { wallEAgentMemoryInstructions } = require('./lib/mcp-integration');
const { getLiveCapabilities, summarizeLiveCapabilities } = require('./runtime/live-capabilities');
let _embeddings;
try { _embeddings = require('./embeddings'); } catch { _embeddings = null; }

const PROTOCOL_VERSION = '2025-03-26';

const MCP_RESOURCES = [
  {
    uri: 'walle://status/session-memory',
    name: 'Wall-E session memory status',
    description: 'Operational status for Wall-E brain memory and CTM cached session-context access.',
    mimeType: 'application/json',
  },
  {
    uri: 'walle://profile/compact',
    name: 'Wall-E compact user profile',
    description: 'Compact digital-twin profile: stable preferences, communication style, work patterns, and evidence pointers.',
    mimeType: 'application/json',
  },
];

const MCP_RESOURCE_TEMPLATES = [
  {
    uriTemplate: 'walle-session://ctm/{session_id}/full',
    name: 'CTM session full context',
    description: 'Full cached CTM session context from session_conversations/session_messages. Use for explicit session transfer.',
    mimeType: 'application/json',
  },
  {
    uriTemplate: 'walle-session://ctm/{session_id}/compact',
    name: 'CTM session compact context',
    description: 'Prompt-friendly markdown view of cached CTM session context.',
    mimeType: 'text/markdown',
  },
  {
    uriTemplate: 'walle-context://task/{query}',
    name: 'Task-scoped CTM context pack',
    description: 'Search cached CTM sessions for a task prompt and return a deduplicated context pack.',
    mimeType: 'text/markdown',
  },
];

const MCP_PROMPTS = [
  {
    name: 'walle_memory_routing',
    title: 'Wall-E Memory Routing Policy',
    description: 'Instructions for when to use Wall-E MCP memory, CTM session context, live tools, or public web search.',
    arguments: [
      { name: 'walle_port', description: 'Optional Wall-E port used in the instruction text', required: false },
    ],
  },
  {
    name: 'walle_coding_resume',
    title: 'Resume With Wall-E Session Context',
    description: 'Prompt template for resuming coding work using Wall-E CTM session memory and diary context.',
    arguments: [
      { name: 'task', description: 'Current coding task or user request', required: false },
      { name: 'session_id', description: 'Optional CTM or agent session id to inspect first', required: false },
    ],
  },
];

const ANY_OBJECT_SCHEMA = { type: 'object', additionalProperties: true };

const MEMORY_RESULT_SCHEMA = {
  type: 'object',
  properties: {
    id: { type: 'string' },
    source: { type: 'string' },
    source_id: { type: 'string' },
    memory_type: { type: 'string' },
    content: { type: 'string' },
    timestamp: { type: 'string' },
  },
  additionalProperties: true,
};

const ROUTE_OUTPUT_SCHEMA = {
  type: 'object',
  properties: {
    route: { type: 'string' },
    should_use_walle: { type: 'boolean' },
    primary_tools: { type: 'array', items: { type: 'string' } },
    secondary_tools: { type: 'array', items: { type: 'string' } },
    avoid_tools: { type: 'array', items: { type: 'string' } },
    avoid_until_memory_miss: { type: 'array', items: { type: 'string' } },
    reason: { type: 'string' },
    matched: { type: 'array', items: ANY_OBJECT_SCHEMA },
  },
  required: ['route', 'should_use_walle', 'reason', 'matched'],
  additionalProperties: true,
};

const SEARCH_MEMORIES_OUTPUT_SCHEMA = {
  type: 'object',
  properties: {
    results: { type: 'array', items: MEMORY_RESULT_SCHEMA },
    filters_applied: ANY_OBJECT_SCHEMA,
    vector_search_used: { type: 'boolean' },
  },
  required: ['results', 'filters_applied', 'vector_search_used'],
  additionalProperties: true,
};

const LOOKUP_CONTEXT_OUTPUT_SCHEMA = {
  type: 'object',
  properties: {
    route: ROUTE_OUTPUT_SCHEMA,
    query: { type: 'string' },
    memory_results: { type: 'array', items: MEMORY_RESULT_SCHEMA },
    session_results: { type: 'array', items: ANY_OBJECT_SCHEMA },
    entity_results: { type: 'array', items: ANY_OBJECT_SCHEMA },
    context_pack: { anyOf: [ANY_OBJECT_SCHEMA, { type: 'null' }] },
    sources: ANY_OBJECT_SCHEMA,
  },
  required: ['route', 'query', 'sources'],
  additionalProperties: true,
};

const CONTEXT_HINT_OUTPUT_SCHEMA = {
  type: 'object',
  properties: {
    route: ROUTE_OUTPUT_SCHEMA,
    confidence: { type: 'string' },
    must_use_walle_before_answer: { type: 'boolean' },
    should_inject_context: { type: 'boolean' },
    recommended_tools: { type: 'array', items: { type: 'string' } },
    skipped_reason: { type: 'string' },
    compact_profile: ANY_OBJECT_SCHEMA,
    lookup: { anyOf: [LOOKUP_CONTEXT_OUTPUT_SCHEMA, { type: 'null' }] },
    prompt_context: { type: 'string' },
    sources: ANY_OBJECT_SCHEMA,
  },
  required: ['route', 'confidence', 'must_use_walle_before_answer', 'should_inject_context', 'recommended_tools', 'sources'],
  additionalProperties: true,
};

const USER_PROFILE_OUTPUT_SCHEMA = {
  type: 'object',
  properties: {
    owner: { type: 'string' },
    aspects: ANY_OBJECT_SCHEMA,
    evidence: { type: 'array', items: ANY_OBJECT_SCHEMA },
    updated_at: { type: 'string' },
  },
  required: ['aspects', 'evidence', 'updated_at'],
  additionalProperties: true,
};

const POSTFLIGHT_OUTPUT_SCHEMA = {
  type: 'object',
  properties: {
    status: { type: 'string' },
    missed_walle: { type: 'boolean' },
    should_flag: { type: 'boolean' },
    warnings: { type: 'array', items: { type: 'string' } },
    evidence: ANY_OBJECT_SCHEMA,
    recommendation: { type: 'string' },
  },
  required: ['status', 'missed_walle', 'should_flag', 'warnings', 'evidence', 'recommendation'],
  additionalProperties: true,
};

const TOOL_OUTPUT_SCHEMAS = {
  walle_route_query: ROUTE_OUTPUT_SCHEMA,
  walle_lookup_context: LOOKUP_CONTEXT_OUTPUT_SCHEMA,
  walle_context_hint: CONTEXT_HINT_OUTPUT_SCHEMA,
  walle_user_profile: USER_PROFILE_OUTPUT_SCHEMA,
  walle_postflight_check: POSTFLIGHT_OUTPUT_SCHEMA,
  search_memories: SEARCH_MEMORIES_OUTPUT_SCHEMA,
  remember: {
    type: 'object',
    properties: {
      stored: { type: 'boolean' },
      id: { type: 'string' },
    },
    required: ['stored', 'id'],
    additionalProperties: true,
  },
  walle_memory_status: ANY_OBJECT_SCHEMA,
  walle_live_capabilities: ANY_OBJECT_SCHEMA,
  walle_ctm_context: ANY_OBJECT_SCHEMA,
  walle_ctm_remote_access_status: ANY_OBJECT_SCHEMA,
  walle_search_sessions: ANY_OBJECT_SCHEMA,
  walle_get_session: ANY_OBJECT_SCHEMA,
  walle_context_pack: ANY_OBJECT_SCHEMA,
  walle_diary_write: ANY_OBJECT_SCHEMA,
  walle_diary_read: ANY_OBJECT_SCHEMA,
};

const READ_ONLY_TOOL_ANNOTATION = Object.freeze({
  readOnlyHint: true,
  destructiveHint: false,
  idempotentHint: true,
  openWorldHint: false,
});

const ADDITIVE_WRITE_TOOL_ANNOTATION = Object.freeze({
  readOnlyHint: false,
  destructiveHint: false,
  idempotentHint: false,
  openWorldHint: false,
});

const IDEMPOTENT_WRITE_TOOL_ANNOTATION = Object.freeze({
  readOnlyHint: false,
  destructiveHint: false,
  idempotentHint: true,
  openWorldHint: false,
});

const TOOL_ANNOTATIONS = {
  walle_route_query: READ_ONLY_TOOL_ANNOTATION,
  walle_lookup_context: READ_ONLY_TOOL_ANNOTATION,
  search_memories: READ_ONLY_TOOL_ANNOTATION,
  remember: ADDITIVE_WRITE_TOOL_ANNOTATION,
  list_knowledge: READ_ONLY_TOOL_ANNOTATION,
  list_people: READ_ONLY_TOOL_ANNOTATION,
  get_brief: READ_ONLY_TOOL_ANNOTATION,
  ask_walle: {
    readOnlyHint: false,
    destructiveHint: false,
    idempotentHint: false,
    openWorldHint: true,
  },
  entity_search: READ_ONLY_TOOL_ANNOTATION,
  entity_graph: READ_ONLY_TOOL_ANNOTATION,
  knowledge_timeline: READ_ONLY_TOOL_ANNOTATION,
  knowledge_query: READ_ONLY_TOOL_ANNOTATION,
  dedup_scan: READ_ONLY_TOOL_ANNOTATION,
  export_brain: READ_ONLY_TOOL_ANNOTATION,
  brain_stats: READ_ONLY_TOOL_ANNOTATION,
  walle_memory_status: READ_ONLY_TOOL_ANNOTATION,
  walle_live_capabilities: READ_ONLY_TOOL_ANNOTATION,
  walle_ctm_context: READ_ONLY_TOOL_ANNOTATION,
  walle_ctm_remote_access_status: READ_ONLY_TOOL_ANNOTATION,
  walle_list_sources: READ_ONLY_TOOL_ANNOTATION,
  walle_source_ingest: ADDITIVE_WRITE_TOOL_ANNOTATION,
  walle_search_sessions: READ_ONLY_TOOL_ANNOTATION,
  walle_get_session: READ_ONLY_TOOL_ANNOTATION,
  walle_context_pack: READ_ONLY_TOOL_ANNOTATION,
  walle_diary_write: IDEMPOTENT_WRITE_TOOL_ANNOTATION,
  walle_diary_read: READ_ONLY_TOOL_ANNOTATION,
  walle_reconnect: IDEMPOTENT_WRITE_TOOL_ANNOTATION,
  walle_repair_status: READ_ONLY_TOOL_ANNOTATION,
  walle_rebuild_ctm_message_index: IDEMPOTENT_WRITE_TOOL_ANNOTATION,
  walle_rebuild_source_index: IDEMPOTENT_WRITE_TOOL_ANNOTATION,
};

const MCP_TOOLS = [
  {
    name: 'walle_route_query',
    description: 'Classify whether a user request should use Wall-E memory/session tools, live source tools, or public web search. Use when unsure before choosing web/search for private, remembered, work, or prior-session context.',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'The user request or task to route' },
        context: { type: 'string', description: 'Optional nearby conversation context' },
      },
      required: ['query'],
    },
  },
  {
    name: 'walle_lookup_context',
    description: 'One-stop Wall-E context lookup. Routes the request, then returns the most relevant private memory, entity, and CTM session context when Wall-E should be used. Use before public web search for ambiguous private/work-context requests.',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'The user request or task to look up' },
        context: { type: 'string', description: 'Optional nearby conversation context' },
        limit: { type: 'number', description: 'Max memories/session snippets/entities per source (default 5, max 20)' },
        include_memories: { type: 'boolean', description: 'Include Wall-E memory search results (default true when route needs Wall-E)' },
        include_sessions: { type: 'boolean', description: 'Include CTM coding-session context (default true for session routes)' },
        include_ctm_operational: { type: 'boolean', description: 'Include CTM operational context such as current phone URLs and Dev Tunnel status (default true for CTM operational routes)' },
        include_entities: { type: 'boolean', description: 'Include knowledge-graph entity matches (default true for people/work routes)' },
        entity_name: { type: 'string', description: 'Optional explicit entity/person/project/tool name to look up' },
        force_memory: { type: 'boolean', description: 'Search Wall-E memory even when routing says live/public/direct' },
        prefer_db: { type: 'boolean', description: 'Prefer cached CTM DB for session context (default true)' },
        source: { type: 'string', description: 'Optional memory source filter' },
        memory_type: { type: 'string', description: 'Optional memory_type filter' },
        since: { type: 'string', description: 'Optional inclusive ISO timestamp lower bound for memory results' },
        until: { type: 'string', description: 'Optional inclusive ISO timestamp upper bound for memory results' },
      },
      required: ['query'],
    },
  },
  {
    name: 'walle_context_hint',
    description: 'Cheap context-broker preflight. Routes a user request and returns whether the agent must consult Wall-E, plus compact profile/context that a host can inject before the agent answers.',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'The user request or prompt to preflight' },
        context: { type: 'string', description: 'Optional nearby conversation/session context' },
        mode: { type: 'string', description: 'preflight, evidence, or full (default preflight)' },
        budget: { type: 'number', description: 'Approximate prompt-context character budget (default 1800, max 8000)' },
        include_profile: { type: 'boolean', description: 'Include compact user profile when useful (default true)' },
        include_lookup: { type: 'boolean', description: 'Gather memory/session/entity evidence when route needs Wall-E (default true)' },
        force_memory: { type: 'boolean', description: 'Force Wall-E lookup even if the router is unsure' },
      },
      required: ['query'],
    },
  },
  {
    name: 'walle_user_profile',
    description: 'Return a compact digital-twin profile for the user: preferences, communication style, work patterns, leadership style, and evidence pointers. Use for style/personality/self-analysis and personalization.',
    inputSchema: {
      type: 'object',
      properties: {
        aspects: { type: 'array', items: { type: 'string' }, description: 'Optional aspect filters, e.g. communication_style, work_patterns, preferences' },
        limit: { type: 'number', description: 'Max evidence rows per profile lookup (default 6, max 20)' },
      },
    },
  },
  {
    name: 'walle_postflight_check',
    description: 'Verify after an agent response whether Wall-E memory should have been used and whether the response appears to have missed required private/user/session context.',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Original user request' },
        answer: { type: 'string', description: 'Agent response or final answer to verify' },
        preflight: { type: 'object', description: 'Optional earlier walle_context_hint result' },
        used_walle: { type: 'boolean', description: 'Whether the host knows Wall-E context/tooling was used' },
        used_tools: { type: 'array', items: { type: 'string' }, description: 'Tool names observed in the agent turn' },
      },
      required: ['query'],
    },
  },
  {
    name: 'search_memories',
    description: "Search Wall-E's private/user memory across ingested Slack, email, calendar, files, and work context. Use before public web search for remembered context, prior discussions, decisions, preferences, people, projects, tools, or 'do you know' / 'what did we discuss' questions.",
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Search query (space-separated terms, all must match)' },
        source: { type: 'string', description: 'Optional source filter, e.g. slack, email, calendar, ctm, walle-diary, codex-jsonl' },
        memory_type: { type: 'string', description: 'Optional memory_type filter, e.g. message, email, agent_diary, coding_session_user_message' },
        since: { type: 'string', description: 'Optional inclusive ISO timestamp lower bound' },
        until: { type: 'string', description: 'Optional inclusive ISO timestamp upper bound' },
        limit: { type: 'number', description: 'Max results to return (default 20, max 200)' },
      },
      required: ['query'],
    },
  },
  {
    name: 'remember',
    description: 'Store a fact, preference, or piece of knowledge in Wall-E\'s brain. Uses subject-predicate-object triples (e.g., subject="user", predicate="prefers", object="dark mode").',
    inputSchema: {
      type: 'object',
      properties: {
        subject: { type: 'string', description: 'The entity (e.g., "user", "project-x", "Alice")' },
        predicate: { type: 'string', description: 'The relationship (e.g., "prefers", "uses", "works-on")' },
        object: { type: 'string', description: 'The value (e.g., "dark mode", "TypeScript", "backend team")' },
        category: { type: 'string', description: 'Category: preference, fact, observation, or rule (default: "fact")' },
      },
      required: ['subject', 'predicate', 'object'],
    },
  },
  {
    name: 'list_knowledge',
    description: "Browse Wall-E's stored knowledge base. Filter by subject, category, or get everything.",
    inputSchema: {
      type: 'object',
      properties: {
        subject: { type: 'string', description: 'Filter by subject (e.g., "user", "project-x")' },
        category: { type: 'string', description: 'Filter by category: preference, fact, observation, rule' },
      },
    },
  },
  {
    name: 'list_people',
    description: 'List people that Wall-E knows about, with their relationships and notes.',
    inputSchema: {
      type: 'object',
      properties: {
        relationship: { type: 'string', description: 'Filter by relationship type (e.g., "colleague", "friend", "manager")' },
        limit: { type: 'number', description: 'Max results (default 50)' },
      },
    },
  },
  {
    name: 'get_brief',
    description: "Get Wall-E's daily briefing — a summary of recent activity, pending questions, and important items.",
    inputSchema: {
      type: 'object',
      properties: {},
    },
  },
  {
    name: 'ask_walle',
    description: 'Ask Wall-E a question through the full memory-aware chat pipeline. Use for complex private/work-context questions that need reasoning across stored memories, knowledge, people, projects, and conversation history.',
    inputSchema: {
      type: 'object',
      properties: {
        question: { type: 'string', description: 'The question to ask Wall-E' },
      },
      required: ['question'],
    },
  },
  {
    name: 'entity_search',
    description: 'Search for entities (people, projects, tools, orgs, concepts) in Wall-E\'s private knowledge graph by name or type. Use for user-context lookups before public web search.',
    inputSchema: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'Entity name to search for (fuzzy match supported)' },
        type: { type: 'string', description: 'Filter by type: person, project, tool, org, concept' },
        limit: { type: 'number', description: 'Max results (default 20)' },
      },
    },
  },
  {
    name: 'entity_graph',
    description: 'Explore entity relationships via BFS graph traversal. Returns connected entities and their relationships.',
    inputSchema: {
      type: 'object',
      properties: {
        entity_name: { type: 'string', description: 'Name of the entity to start from' },
        depth: { type: 'number', description: 'BFS traversal depth (default 2, max 4)' },
      },
      required: ['entity_name'],
    },
  },
  {
    name: 'knowledge_timeline',
    description: 'Get the temporal history of a knowledge subject — see how facts changed over time.',
    inputSchema: {
      type: 'object',
      properties: {
        subject: { type: 'string', description: 'The subject to get timeline for' },
      },
      required: ['subject'],
    },
  },
  {
    name: 'knowledge_query',
    description: 'Query knowledge with advanced filtering: temporal (as-of date), category, status.',
    inputSchema: {
      type: 'object',
      properties: {
        subject: { type: 'string', description: 'Filter by subject' },
        category: { type: 'string', description: 'Filter by category' },
        as_of: { type: 'string', description: 'Point-in-time query (ISO date). Returns knowledge valid at that date.' },
        status: { type: 'string', description: 'Filter by status: active, superseded, disputed, retracted (default: active)' },
        limit: { type: 'number', description: 'Max results (default 50)' },
      },
    },
  },
  {
    name: 'dedup_scan',
    description: 'Scan for duplicate memories using embedding similarity. Returns groups of near-duplicates (dry run, no changes made).',
    inputSchema: {
      type: 'object',
      properties: {
        threshold: { type: 'number', description: 'Similarity threshold 0-1 (default 0.85). Higher = stricter matching.' },
        limit: { type: 'number', description: 'Max memories to scan (default 200)' },
      },
    },
  },
  {
    name: 'export_brain',
    description: 'Export Wall-E\'s brain as structured markdown. Returns files organized by knowledge category, memory domain, people, and entities.',
    inputSchema: {
      type: 'object',
      properties: {
        domain: { type: 'string', description: 'Filter by domain: work, personal, social, technical, admin' },
        format: { type: 'string', description: 'Export format (currently only "markdown" supported)' },
      },
    },
  },
  {
    name: 'brain_stats',
    description: 'Get detailed statistics about Wall-E\'s brain: memory counts, entity counts, knowledge counts, embedding coverage, etc.',
    inputSchema: {
      type: 'object',
      properties: {},
    },
  },
  {
    name: 'walle_memory_status',
    description: 'Get coding-session memory status: brain stats, source adapters, diary count, live capability summary, and operational tool availability.',
    inputSchema: {
      type: 'object',
      properties: {},
    },
  },
  {
    name: 'walle_live_capabilities',
    description: 'Report read-only status for Wall-E live source access such as Slack, Gmail/Google Workspace, calendar, and macOS app fallbacks. This does not read mail, Slack messages, or calendar events.',
    inputSchema: {
      type: 'object',
      properties: {},
    },
  },
  {
    name: 'walle_ctm_context',
    description: 'Resolve CTM operational and session-memory context for CTM primary, mobile phone URLs, Dev Tunnel, /m/, and related CTM session-memory questions. Use this instead of generic memory for CTM remote-access answers.',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'The CTM/mobile/session-memory question to resolve' },
        context: { type: 'string', description: 'Optional nearby conversation context' },
        limit: { type: 'number', description: 'Max CTM session-memory snippets to include (default 5, max 10)' },
        force: { type: 'boolean', description: 'Force CTM operational lookup even if the query classifier is unsure' },
        timeout_ms: { type: 'number', description: 'Timeout for CTM API calls (default 6000)' },
        cooldown_ms: { type: 'number', description: 'Cooldown for CTM API retry suppression (default 0 for this exact lookup)' },
      },
      required: ['query'],
    },
  },
  {
    name: 'walle_ctm_remote_access_status',
    description: 'Read current CTM setup/network status and summarize active phone access URLs, including Microsoft Dev Tunnel and Tailscale. Read-only.',
    inputSchema: {
      type: 'object',
      properties: {
        timeout_ms: { type: 'number', description: 'Timeout for the CTM setup/network API call (default 6000)' },
        cooldown_ms: { type: 'number', description: 'Cooldown for CTM API retry suppression (default 0)' },
      },
    },
  },
  {
    name: 'walle_list_sources',
    description: 'List source adapters Wall-E can ingest, including Claude Code, Codex, Gemini, and Wall-E JSONL sessions.',
    inputSchema: {
      type: 'object',
      properties: {
        privacy_class: { type: 'string', description: 'Optional adapter default privacy class filter' },
        incremental: { type: 'boolean', description: 'Optional incremental-support filter' },
      },
    },
  },
  {
    name: 'walle_source_ingest',
    description: 'Ingest a source file through a Wall-E source adapter. Use dry_run first when inspecting an unfamiliar source; set confirm_write=true for non-dry-run ingestion.',
    inputSchema: {
      type: 'object',
      properties: {
        adapter_id: { type: 'string', description: 'Source adapter id, e.g. codex-jsonl or claude-code-jsonl' },
        uri: { type: 'string', description: 'Path to the JSONL source file' },
        source_id: { type: 'string', description: 'Optional stable source/session id' },
        cwd: { type: 'string', description: 'Optional project working directory' },
        privacy_class: { type: 'string', description: 'Optional source privacy class' },
        privacy_floor: { type: 'string', description: 'Optional maximum privacy class allowed to write' },
        dry_run: { type: 'boolean', description: 'Parse and validate without writing memories' },
        confirm_write: { type: 'boolean', description: 'Required when dry_run is false because ingestion writes Wall-E memories' },
        metadata: { type: 'object', description: 'Optional adapter metadata' },
      },
      required: ['adapter_id', 'uri'],
    },
  },
  {
    name: 'walle_search_sessions',
    description: 'Search coding-session history. Tries cached CTM DB session messages first, then ingested Wall-E memories/diary entries. Returns deduplicated attributed snippets plus session pointers.',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Search query' },
        limit: { type: 'number', description: 'Max session snippets (default 10, max 50)' },
        prefer_db: { type: 'boolean', description: 'Try cached CTM DB tables before Wall-E memory (default true)' },
        include_memory: { type: 'boolean', description: 'Also search Wall-E ingested memories and diary entries (default true)' },
      },
      required: ['query'],
    },
  },
  {
    name: 'walle_get_session',
    description: 'Retrieve cached CTM session context by session id/source id, or parse a JSONL session directly when adapter_id and uri are provided. DB tables are preferred for full context transfer.',
    inputSchema: {
      type: 'object',
      properties: {
        session_id: { type: 'string', description: 'CTM session id or agent session id to retrieve from cached DB tables' },
        session_ids: { type: 'array', items: { type: 'string' }, description: 'Several CTM/agent session ids to retrieve together' },
        source_id: { type: 'string', description: 'Stable source/session id to retrieve from memory' },
        adapter_id: { type: 'string', description: 'Optional adapter id for direct JSONL parse' },
        uri: { type: 'string', description: 'Optional JSONL path for direct parse' },
        limit: { type: 'number', description: 'Max records/messages (default 50 for memory, 200 for CTM DB; use 0/full/all for all cached DB messages)' },
        cursor: { type: 'number', description: 'Message offset for cached DB pagination' },
        format: { type: 'string', description: 'Cached DB output format: messages, markdown, or compact' },
        dedupe: { type: 'boolean', description: 'Deduplicate repeated message content (default true)' },
        prefer_db: { type: 'boolean', description: 'Use cached CTM DB tables before Wall-E memory fallback (default true)' },
        include_raw: { type: 'boolean', description: 'Include raw content when available' },
      },
    },
  },
  {
    name: 'walle_context_pack',
    description: 'Build a deduplicated task-scoped context pack from cached CTM session history. Use before answering or starting a coding task when the user references prior attempts, old bugs, branches, decisions, restarts, or past agent work.',
    inputSchema: {
      type: 'object',
      properties: {
        task: { type: 'string', description: 'Current user prompt or task description' },
        query: { type: 'string', description: 'Optional explicit search query; defaults to task' },
        session_ids: { type: 'array', items: { type: 'string' }, description: 'Optional CTM/agent session ids to force include' },
        limit: { type: 'number', description: 'Max sessions to include (default 5, max 50)' },
        token_budget: { type: 'number', description: 'Approximate context-pack token budget (default 12000)' },
        mode: { type: 'string', description: 'auto, compact, or full' },
        include_raw: { type: 'boolean', description: 'Include raw cached messages when mode/full transfer needs exact source payloads' },
      },
    },
  },
  {
    name: 'walle_diary_write',
    description: 'Write an idempotent agent diary entry for a coding session stop, handoff, or compaction boundary.',
    inputSchema: {
      type: 'object',
      properties: {
        agent: { type: 'string', description: 'Agent name or harness' },
        session_id: { type: 'string', description: 'Stable session id' },
        event: { type: 'string', description: 'Diary event, e.g. stop, precompact, handoff' },
        cwd: { type: 'string', description: 'Project working directory' },
        branch: { type: 'string', description: 'Git branch' },
        changed_files: { type: 'array', items: { type: 'string' } },
        commands: { type: 'array', items: { type: 'string' } },
        decisions: { type: 'array', items: { type: 'string' } },
        blockers: { type: 'array', items: { type: 'string' } },
        next_steps: { type: 'array', items: { type: 'string' } },
        summary: { type: 'string', description: 'Short human-readable summary' },
      },
      required: ['session_id'],
    },
  },
  {
    name: 'walle_diary_read',
    description: 'Read recent agent diary entries, optionally filtered by agent or session id.',
    inputSchema: {
      type: 'object',
      properties: {
        agent: { type: 'string', description: 'Optional agent filter' },
        session_id: { type: 'string', description: 'Optional session filter' },
        limit: { type: 'number', description: 'Max entries (default 20, max 100)' },
      },
    },
  },
  {
    name: 'walle_reconnect',
    description: 'Refresh Wall-E MCP process state after external DB/source updates. Runs a passive checkpoint and reports current health.',
    inputSchema: {
      type: 'object',
      properties: {},
    },
  },
  {
    name: 'walle_repair_status',
    description: 'Run a dry integrity scan for brain memory, knowledge, entity, and index consistency.',
    inputSchema: {
      type: 'object',
      properties: {
        limit: { type: 'number', description: 'Max issue rows to return in the sample (default 50, max 500)' },
        include_all: { type: 'boolean', description: 'Return all issue rows instead of a bounded sample' },
      },
    },
  },
  {
    name: 'walle_rebuild_ctm_message_index',
    description: 'Ask the CTM API to backfill session_messages and FTS rows from cached session_conversations. Use dry_run first to inspect planned work; set confirm_write=true for writes.',
    inputSchema: {
      type: 'object',
      properties: {
        limit: { type: 'number', description: 'Max session_conversations rows to repair (default 100, max 1000)' },
        dry_run: { type: 'boolean', description: 'Plan the repair without writing (default false)' },
        confirm_write: { type: 'boolean', description: 'Required when dry_run is false because rebuild writes CTM index rows' },
      },
    },
  },
  {
    name: 'walle_rebuild_source_index',
    description: 'Rebuild the memory/source index from stored memories. Use dry_run to inspect expected work without writing; set confirm_write=true for writes.',
    inputSchema: {
      type: 'object',
      properties: {
        source_id: { type: 'string', description: 'Optional stable source/session id to rebuild' },
        dry_run: { type: 'boolean', description: 'Return current index status without rebuilding' },
        confirm_write: { type: 'boolean', description: 'Required when dry_run is false because rebuild writes Wall-E source-index rows' },
      },
    },
  },
];

function toolTitle(name) {
  return String(name || '')
    .split('_')
    .filter(Boolean)
    .map(part => part.charAt(0).toUpperCase() + part.slice(1))
    .join(' ');
}

function applyMcpToolMetadata(tools) {
  for (const tool of tools) {
    const annotations = TOOL_ANNOTATIONS[tool.name] || READ_ONLY_TOOL_ANNOTATION;
    tool.annotations = { title: toolTitle(tool.name), ...annotations };
    if (TOOL_OUTPUT_SCHEMAS[tool.name]) {
      tool.outputSchema = TOOL_OUTPUT_SCHEMAS[tool.name];
    }
  }
}

applyMcpToolMetadata(MCP_TOOLS);

async function executeTool(name, args) {
  switch (name) {
    case 'walle_route_query': {
      return routeWallEQuery(args.query, { context: args.context });
    }
    case 'walle_lookup_context': {
      return lookupWallEContext(args);
    }
    case 'walle_context_hint': {
      return buildContextHint(args);
    }
    case 'walle_user_profile': {
      return buildUserProfile(args);
    }
    case 'walle_postflight_check': {
      return buildPostflightCheck(args);
    }
    case 'search_memories': {
      const limit = args.limit || 20;
      const filters = {
        source: args.source,
        memory_type: args.memory_type,
        since: args.since,
        until: args.until,
      };
      const hasFilters = Object.values(filters).some(Boolean);
      // Person-aware recall: if the query references a person, expand the search to
      // their aliases / email / @handle so messages that reference them indirectly
      // still match (full-name-only search misses email/handle mentions).
      let personTerms;
      let resolvedPerson = '';
      try {
        const inferred = inferEntityName(args.query);
        if (inferred) {
          const ident = brain.resolvePersonIdentities(inferred);
          if (ident && Array.isArray(ident.terms) && ident.terms.length > 1) {
            personTerms = ident.terms;
            resolvedPerson = ident.canonical;
          }
        }
      } catch {}
      const memorySearch = await searchWallEMemories({
        query: args.query,
        limit,
        filters,
        hybrid: !hasFilters,
        terms: personTerms,
      });
      const out = {
        results: memorySearch.results.map(formatMemoryResult),
        filters_applied: Object.fromEntries(Object.entries(filters).filter(([, value]) => Boolean(value))),
        vector_search_used: memorySearch.vector_search_used,
        source: memorySearch.source,
        warnings: memorySearch.source.ok ? [] : [memorySearch.source],
      };
      if (resolvedPerson) out.resolved_person = resolvedPerson;
      // Empty stored memory is NOT a dead end — steer the caller to escalate to
      // live sources / the full chat pipeline rather than declining.
      if (memorySearch.source.ok && memorySearch.results.length === 0) {
        out.hint = 'No stored memory matched. The brain may not have ingested this yet — this does NOT mean no history exists.';
        out.next_steps = [
          'Call ask_walle with the original request — it can search LIVE Slack / Gmail / Calendar and compose the reply.',
          ...(inferEntityName(args.query) ? ["Call list_people / entity_search to resolve the person's email or @handle, then search those."] : []),
          'If this is a write/compose request, draft it with [placeholders] for any unknowns instead of declining.',
        ];
      }
      return out;
    }
    case 'remember': {
      const result = brain.insertKnowledge({
        category: args.category || 'fact',
        subject: args.subject,
        predicate: args.predicate,
        object: args.object,
      });
      return { stored: true, id: result.id };
    }
    case 'list_knowledge': {
      const entries = brain.findKnowledge({ subject: args.subject, category: args.category });
      return { entries: entries.map(k => ({ id: k.id, category: k.category, subject: k.subject, predicate: k.predicate, object: k.object, confidence: k.confidence })) };
    }
    case 'list_people': {
      const people = brain.listPeople({ relationship: args.relationship, limit: args.limit || 50 });
      return { people: people.map(p => ({ id: p.id, name: p.name, relationship: p.relationship, notes: p.notes })) };
    }
    case 'get_brief': {
      const stats = brain.getBrainStats();
      const recent = brain.listMemories({ limit: 5 });
      const questions = brain.listQuestions({ status: 'pending', limit: 5 });
      const summary = brain.getDailySummary(new Date().toISOString().split('T')[0]);
      return {
        owner: brain.getOwnerName(),
        stats,
        recent_memories: recent.map(m => ({ source: m.source, content: m.content.slice(0, 200), timestamp: m.timestamp })),
        pending_questions: questions.map(q => ({ id: q.id, question: q.question })),
        daily_summary: summary ? summary.summary : null,
      };
    }
    case 'ask_walle': {
      const { chat } = require('./chat');
      const result = await chat(args.question, { channel: 'mcp', session_id: 'mcp-session' });
      return { reply: result.reply, model: result.model };
    }
    case 'entity_search': {
      const results = [];
      if (args.name) {
        const exact = brain.findEntity(args.name);
        if (exact) results.push(exact);
        if (results.length === 0) {
          const fuzzy = brain.findEntityFuzzy(args.name);
          if (fuzzy) results.push(fuzzy);
        }
      }
      if (results.length === 0) {
        const all = brain.listEntities({ entity_type: args.type, limit: args.limit || 20 });
        if (args.name) {
          const lower = args.name.toLowerCase();
          results.push(...all.filter(e => e.canonical_name.toLowerCase().includes(lower)));
        } else {
          results.push(...all);
        }
      }
      return { entities: results.map(e => ({ id: e.id, name: e.canonical_name, type: e.entity_type, aliases: e.aliases })) };
    }
    case 'entity_graph': {
      const entity = brain.findEntity(args.entity_name) || brain.findEntityFuzzy(args.entity_name);
      if (!entity) return { error: `Entity "${args.entity_name}" not found` };
      const depth = Math.min(args.depth || 2, 4);
      const graph = brain.getEntityGraph(entity.id, depth);
      return { entity: entity.canonical_name, ...graph };
    }
    case 'knowledge_timeline': {
      const timeline = brain.getKnowledgeTimeline(args.subject);
      return { subject: args.subject, timeline: timeline.map(k => ({ predicate: k.predicate, object: k.object, valid_from: k.valid_from, valid_to: k.valid_to, status: k.status, confidence: k.confidence })) };
    }
    case 'knowledge_query': {
      const entries = brain.findKnowledge({
        subject: args.subject,
        category: args.category,
        status: args.status || 'active',
        as_of: args.as_of,
        limit: args.limit || 50,
      });
      return { entries: entries.map(k => ({ id: k.id, category: k.category, subject: k.subject, predicate: k.predicate, object: k.object, confidence: k.confidence, valid_from: k.valid_from, valid_to: k.valid_to })) };
    }
    case 'dedup_scan': {
      try {
        const dedup = require('./utils/dedup');
        const result = await dedup.findDuplicates({ threshold: args.threshold || 0.85, limit: args.limit || 200, dryRun: true });
        return { groups: result.groups?.length || 0, total_duplicates: result.totalDuplicates || 0, details: (result.groups || []).slice(0, 10) };
      } catch (err) {
        return { error: err.message };
      }
    }
    case 'export_brain': {
      try {
        const exporter = require('./utils/exporter');
        const result = exporter.exportBrain({ domain: args.domain, format: args.format || 'markdown' });
        return result;
      } catch (err) {
        return { error: err.message };
      }
    }
    case 'brain_stats': {
      const stats = brain.getBrainStats();
      let entityCount = 0, embeddingCount = 0;
      try { entityCount = brain.listEntities({ limit: 1 }).length > 0 ? brain.getDb().prepare('SELECT COUNT(*) as c FROM entities').get().c : 0; } catch {}
      try { embeddingCount = brain.getDb().prepare("SELECT COUNT(*) as c FROM embedding_map").get().c; } catch {}
      return { ...stats, entity_count: entityCount, embedding_count: embeddingCount, storage_risk: brain.getStorageRisk?.() || null };
    }
    case 'walle_memory_status': {
      ensureBuiltinSourceAdapters();
      const stats = brain.getBrainStats();
      const liveCapabilities = getLiveCapabilities();
      return {
        ok: true,
        stats,
        storage_risk: brain.getStorageRisk?.() || null,
        ctm_db: await getCtmDbStatus(),
        resources: {
          static: MCP_RESOURCES.map((resource) => resource.uri),
          templates: MCP_RESOURCE_TEMPLATES.map((template) => template.uriTemplate),
        },
        source_adapters: sourceRegistry.list().map(formatSourceAdapter),
        diary_count: countDiaryEntries(),
        live_capabilities: summarizeLiveCapabilities(liveCapabilities),
        tools: MCP_TOOLS.filter((tool) => tool.name.startsWith('walle_')).map((tool) => tool.name),
      };
    }
    case 'walle_live_capabilities': {
      return getLiveCapabilities();
    }
    case 'walle_ctm_context': {
      return ctmOperationalContext.lookupCtmOperationalContext(args);
    }
    case 'walle_ctm_remote_access_status': {
      const status = await ctmContextClient.getRemoteAccessStatus({}, {
        timeoutMs: args.timeout_ms || 6000,
        cooldownMs: args.cooldown_ms || 0,
      });
      return {
        ok: !status.reason,
        source: status.source || 'ctm-api',
        reason: status.reason || '',
        api_transport: status.api_transport || null,
        remote_access: status.reason ? null : ctmOperationalContext.summarizeRemoteAccess(status),
      };
    }
    case 'walle_list_sources': {
      ensureBuiltinSourceAdapters();
      return {
        sources: sourceRegistry.list({
          privacyClass: args.privacy_class,
          incremental: args.incremental,
        }).map(formatSourceAdapter),
      };
    }
    case 'walle_source_ingest': {
      ensureBuiltinSourceAdapters();
      requireWriteConfirmation('walle_source_ingest', args);
      assertJsonlPath(args.uri);
      const sourceRef = {
        adapterId: args.adapter_id,
        uri: path.resolve(args.uri),
        sourceFile: path.resolve(args.uri),
        sourceId: args.source_id,
        cwd: args.cwd || '',
        privacyClass: args.privacy_class,
        metadata: args.metadata || {},
      };
      const service = createSessionIngestService({ brain });
      return service.ingestSource(sourceRef, {
        privacyFloor: args.privacy_floor || null,
        dryRun: Boolean(args.dry_run),
      });
    }
    case 'walle_search_sessions': {
      const limit = clampLimit(args.limit, 10, 50);
      const ctmResults = args.prefer_db === false
        ? { ok: false, results: [], skipped: true, reason: 'prefer_db_false' }
        : await safeCtmSearchSessions({ query: args.query, limit });
      const memorySearch = args.include_memory === false
        ? {
          results: [],
          source: { searched: false, ok: true, source: 'wall-e-memory', count: 0, reason: '' },
          vector_search_used: false,
        }
        : await searchWallEMemories({ query: args.query, limit: limit * 4, hybrid: false });
      const memoryResults = memorySearch.results
        .filter(isSessionMemory)
        .slice(0, limit * 2)
        .map((memory, index) => ({
          ...formatSessionMemory(memory),
          source_layer: 'wall-e-memory',
          rank: 10000 + index,
        }));
      const ctmSourceLayer = ctmResults.source === 'ctm-api' ? 'ctm-api' : 'ctm-db';
      const dbResults = (ctmResults.results || []).map((result, index) => ({
        ...result,
        source_layer: ctmSourceLayer,
        rank: Number.isFinite(result.rank) ? result.rank : index,
      }));
      const results = ctmSessionContext.dedupeItems([...dbResults, ...memoryResults], {
        limit,
        contentFields: ['snippet', 'content'],
        keyPrefix: (item) => `${item.session_id || item.source_id || ''}:${item.role || item.memory_type || ''}`,
      }).map(stripRank);
      return {
        results,
        sources: {
          ctm_db: {
            ok: Boolean(ctmResults.ok),
            unavailable: Boolean(ctmResults.unavailable),
            source: ctmResults.source || '',
            backend_source: ctmResults.backend_source || '',
            api_transport: ctmResults.api_transport || null,
            db_path: ctmResults.db_path || '',
            active_source: ctmResults.active_source || '',
            fallback_used: Boolean(ctmResults.fallback_used),
            reason: ctmResults.reason || '',
            count: (ctmResults.results || []).length,
          },
          wall_e_memory: {
            searched: args.include_memory !== false,
            ok: Boolean(memorySearch.source.ok),
            source: memorySearch.source.source,
            reason: memorySearch.source.reason || '',
            error: memorySearch.source.error || '',
            count: memoryResults.length,
          },
        },
        deduped: true,
      };
    }
    case 'walle_get_session': {
      const limit = clampLimit(args.limit, 50, 200);
      if (args.adapter_id && args.uri) {
        ensureBuiltinSourceAdapters();
        assertJsonlPath(args.uri);
        const adapter = sourceRegistry.instantiate(args.adapter_id);
        if (!adapter) throw new Error(`Unknown source adapter: ${args.adapter_id}`);
        const sourceRef = {
          adapterId: args.adapter_id,
          uri: path.resolve(args.uri),
          sourceFile: path.resolve(args.uri),
          sourceId: args.source_id,
        };
        const { records, diagnostics, source } = await collectIngestRecords(adapter, sourceRef);
        return {
          source: { adapterId: source.adapterId, sourceId: source.sourceId, sourceFile: source.sourceFile },
          diagnostics,
          records: records
            .filter((record) => record.type === RECORD_TYPES.MEMORY_RECORD)
            .slice(0, limit)
            .map((record) => formatSourceRecord(record, { includeRaw: Boolean(args.include_raw) })),
        };
      }
      const wantsDb = args.prefer_db !== false;
      const dbSessionIds = args.session_ids || args.session_id || args.source_id;
      if (wantsDb && dbSessionIds) {
        const dbContext = await safeCtmSessionContext({
          session_ids: args.session_ids,
          session_id: args.session_id,
          source_id: args.source_id,
          limit: args.limit ?? 200,
          cursor: args.cursor || 0,
          include_raw: Boolean(args.include_raw),
          dedupe: args.dedupe !== false,
          format: args.format || 'messages',
        });
        if (dbContext.ok && (dbContext.sessions.length > 0 || dbContext.messages.length > 0)) {
          return dbContext;
        }
        if (args.session_id || args.session_ids) return dbContext;
      }
      if (!args.source_id) throw new Error('source_id is required when adapter_id and uri are not provided');
      return {
        source_id: args.source_id,
        transfer: {
          full_context_supported: false,
          served_from: 'wall-e-memory',
          jsonl_fallback_used: false,
          ctm_db_preferred: wantsDb,
        },
        records: getSessionMemories(args.source_id, { limit }).map(formatSessionMemory),
      };
    }
    case 'walle_context_pack': {
      return await safeCtmContextPack({
        task: args.task || '',
        query: args.query || '',
        session_ids: args.session_ids,
        limit: args.limit || 5,
        token_budget: args.token_budget || 12000,
        mode: args.mode || 'auto',
        include_raw: Boolean(args.include_raw),
      });
    }
    case 'walle_diary_write': {
      return writeDiaryEntry(args);
    }
    case 'walle_diary_read': {
      return readDiaryEntries(args);
    }
    case 'walle_reconnect': {
      const status = reconnectBrain();
      return { ok: true, ...status };
    }
    case 'walle_repair_status': {
      const repair = require('./utils/repair');
      return compactRepairScan(repair.scanIntegrity(), {
        limit: args.limit,
        include_all: args.include_all,
      });
    }
    case 'walle_rebuild_ctm_message_index': {
      requireWriteConfirmation('walle_rebuild_ctm_message_index', args);
      return ctmContextClient.backfillMessageIndex({
        limit: args.limit || 100,
        dry_run: Boolean(args.dry_run),
      });
    }
    case 'walle_rebuild_source_index': {
      requireWriteConfirmation('walle_rebuild_source_index', args);
      const { rebuildSourceIndex } = require('./memory/source-indexer');
      return rebuildSourceIndex({
        brain,
        sourceId: args.source_id || '',
        dryRun: Boolean(args.dry_run),
      });
    }
    default:
      return null;
  }
}

function routeWallEQuery(query, opts = {}) {
  const text = `${query || ''}\n${opts.context || ''}`.toLowerCase();
  const has = (patterns) => patterns.some(pattern => pattern.test(text));
  const matched = [];
  const add = (name, patterns) => {
    const hits = patterns.filter(pattern => pattern.test(text)).map(pattern => pattern.source);
    if (hits.length) matched.push({ name, hits });
    return hits.length > 0;
  };

  const explicitWalle = add('explicit_wall_e', [/\bwall-?e\b/, /\bwalle\b/, /\bmcp\b/, /\bmemory\b/, /\bctm\b/]);
  const priorContext = add('prior_context', [/\blast time\b/, /\bprevious(ly)?\b/, /\bremember\b/, /\bwhat did we (discuss|decide)\b/, /\bwhat happened\b/, /\bfollow.?up\b/, /\bcontext\b/]);
  const sessionContext = add('session_context', [/\bsession\b/, /\bagent\b/, /\bbranch\b/, /\brestart\b/, /\bresume\b/, /\bhandoff\b/, /\bdiary\b/, /\bbug\b/, /\bregression\b/, /\btitle\b/, /\bcommit\b/]);
  const workMemory = add('work_memory', [/\bslack\b/, /\bemail\b/, /\bcalendar\b/, /\bcolleague\b/, /\bmanager\b/, /\bteam\b/, /\bproject\b/, /\bdoc(ument)?\b/, /\btool\b/, /\bdecision\b/, /\bpreference\b/]);
  const personLookup = add('person_lookup', [/\bwho is\b/, /\bdo you know\b/, /\bwhat is .* role\b/, /\breports? to\b/]);
  const selfProfile = add('self_profile', [/\b(my|me)\b.*\b(writing style|communication style|tone|personality|leadership style|work style)\b/, /\banaly[sz]e my\b/, /\bwhat.*\bpatterns\b.*\bme\b/, /\bam i too\b/]);
  const liveAction = add('live_action', [/\bsend\b/, /\bpost\b/, /\bcreate\b/, /\bschedule\b/, /\bopen\b/, /\bdelete\b/, /\bupdate\b/, /\bunread\b/, /\binbox\b/, /\btoday'?s calendar\b/, /\btomorrow'?s calendar\b/]);
  const publicCurrent = add('public_current', [/\bweb\b/, /\bpublic\b/, /\bnews\b/, /\blatest\b/, /\bcurrent\b/, /\bprice\b/, /\bstock\b/, /\bweather\b/, /\bdocs?\b/, /\bofficial\b/]);
  const ctmOperational = ctmOperationalContext.classifyCtmOperationalQuery(query, { context: opts.context });
  if (ctmOperational.required) {
    matched.push({ name: 'ctm_operational_context', hits: ctmOperational.matched });
    return {
      route: 'ctm_operational_context',
      should_use_walle: true,
      primary_tools: ['walle_ctm_context', 'walle_ctm_remote_access_status', 'walle_search_sessions'],
      secondary_tools: ['walle_lookup_context', 'search_memories'],
      avoid_until_memory_miss: ['web_search', 'web_fetch'],
      reason: ctmOperational.reason,
      matched,
    };
  }

  if (selfProfile) {
    return {
      route: 'walle_private_memory',
      should_use_walle: true,
      primary_tools: ['walle_lookup_context', 'search_memories', 'walle_search_sessions'],
      secondary_tools: ['knowledge_query', 'ask_walle'],
      avoid_until_memory_miss: ['web_search', 'web_fetch'],
      reason: 'The request asks about the user\'s own style, tone, personality, or patterns, which requires private Wall-E memory and prior interaction context.',
      matched,
    };
  }

  if (liveAction && !priorContext && !explicitWalle) {
    return {
      route: 'live_source_or_action',
      should_use_walle: false,
      primary_tools: ['calendar/email/slack/action tools exposed by the host'],
      secondary_tools: ['search_memories only if the user asks for remembered context'],
      avoid_tools: ['walle_context_pack', 'walle_search_sessions'],
      reason: 'The request appears to need live source data or a side-effecting action rather than stored memory.',
      matched,
    };
  }

  if (sessionContext || (explicitWalle && priorContext)) {
    return {
      route: 'walle_session_context',
      should_use_walle: true,
      primary_tools: ['walle_context_pack', 'walle_search_sessions', 'walle_get_session'],
      secondary_tools: ['walle_diary_read', 'search_memories'],
      avoid_until_memory_miss: ['web_search', 'web_fetch'],
      reason: 'The request appears to depend on prior coding-agent, CTM, branch, bug, restart, or handoff context.',
      matched,
    };
  }

  if (priorContext || workMemory || personLookup || explicitWalle) {
    return {
      route: 'walle_private_memory',
      should_use_walle: true,
      primary_tools: personLookup ? ['entity_search', 'search_memories', 'ask_walle'] : ['search_memories', 'knowledge_query', 'ask_walle'],
      secondary_tools: ['entity_graph', 'knowledge_timeline'],
      avoid_until_memory_miss: ['web_search', 'web_fetch'],
      reason: 'The request appears to depend on private, remembered, or work-context data Wall-E owns.',
      matched,
    };
  }

  if (publicCurrent) {
    return {
      route: 'public_or_current_fact',
      should_use_walle: false,
      primary_tools: ['web_search', 'web_fetch', 'official docs or public data tools'],
      secondary_tools: ['search_memories only if the user asks for personal/work context'],
      avoid_tools: ['walle_context_pack'],
      reason: 'The request appears to ask for public/current information rather than private memory.',
      matched,
    };
  }

  return {
    route: 'answer_directly_or_clarify',
    should_use_walle: false,
    primary_tools: [],
    secondary_tools: ['walle_route_query again with more context if uncertain'],
    reason: 'No strong signal that Wall-E memory, live tools, or public web are required.',
    matched,
  };
}

async function lookupWallEContext(args = {}) {
  const query = String(args.query || '').trim();
  if (!query) throw new Error('query is required');
  const route = routeWallEQuery(query, { context: args.context });
  const limit = clampLimit(args.limit, 5, 20);
  const shouldUseWallE = Boolean(route.should_use_walle || args.force_memory);
  const isProfileLookup = route.matched?.some(match => match.name === 'self_profile');
  const includeMemories = args.include_memories === true || (args.include_memories !== false && shouldUseWallE);
  const includeSessions = args.include_sessions === true
    || (args.include_sessions !== false && (route.route === 'walle_session_context' || route.route === 'ctm_operational_context' || isProfileLookup));
  const includeCtmOperational = args.include_ctm_operational === true
    || (args.include_ctm_operational !== false && route.route === 'ctm_operational_context');
  const includeEntities = args.include_entities === true
    || (args.include_entities !== false && (route.route === 'walle_private_memory' || args.force_memory));
  const inferredEntityName = String(args.entity_name || inferEntityName(query) || '').trim();
  const memoryQuery = inferredEntityName || (isProfileLookup ? 'communication style tone personality leadership style' : query);
  const result = {
    route,
    query,
    memory_results: [],
    session_results: [],
    entity_results: [],
    context_pack: null,
    ctm_operational_context: null,
    sources: {
      memories: { searched: false, count: 0 },
      ctm_sessions: { searched: false, count: 0 },
      ctm_operational: { searched: false, count: 0 },
      entities: { searched: false, count: 0 },
    },
  };

  if (includeCtmOperational) {
    const operational = await ctmOperationalContext.lookupCtmOperationalContext({
      query,
      context: args.context,
      limit,
      force: true,
    });
    result.ctm_operational_context = operational;
    result.sources.ctm_operational = {
      searched: true,
      ok: Boolean(operational.ok),
      required: Boolean(operational.required),
      reason: operational.ok ? '' : 'ctm_operational_context_unavailable',
      remote_access: operational.sources?.remote_access || null,
      ctm_sessions: operational.sources?.ctm_sessions || null,
      count: (operational.session_results || []).length,
    };
  }

  if (includeSessions) {
    const sessionQuery = isProfileLookup ? 'thorough review fix issues commit high-agency' : query;
    const sessionSearch = await safeCtmSearchSessions({
      query: sessionQuery,
      limit,
      prefer_db: args.prefer_db !== false,
    });
    result.session_results = (sessionSearch.results || []).slice(0, limit).map(stripRank);
    result.sources.ctm_sessions = {
      searched: true,
      ok: Boolean(sessionSearch.ok),
      source: sessionSearch.source || '',
      reason: sessionSearch.reason || '',
      count: result.session_results.length,
    };
    result.context_pack = await safeCtmContextPack({
      task: query,
      query: sessionQuery,
      limit: Math.min(limit, 5),
      token_budget: 6000,
      mode: 'compact',
      include_raw: false,
    });
  }

  if (includeMemories) {
    const filters = {
      source: args.source,
      memory_type: args.memory_type,
      since: args.since,
      until: args.until,
    };
    const memorySearch = await searchWallEMemories({
      query: memoryQuery,
      limit,
      filters,
      hybrid: !Object.values(filters).some(Boolean),
    });
    result.memory_results = memorySearch.results.map(formatMemoryResult);
    result.sources.memories = {
      searched: true,
      query: memoryQuery,
      filters_applied: Object.fromEntries(Object.entries(filters).filter(([, value]) => Boolean(value))),
      ok: Boolean(memorySearch.source.ok),
      source: memorySearch.source.source,
      reason: memorySearch.source.reason || '',
      error: memorySearch.source.error || '',
      vector_search_used: memorySearch.vector_search_used,
      count: result.memory_results.length,
    };
  }

  if (includeEntities) {
    if (inferredEntityName) {
      const entitySearch = safeSearchEntitiesForContext(inferredEntityName, limit);
      result.entity_results = entitySearch.results;
      result.sources.entities = {
        searched: true,
        query: inferredEntityName,
        ok: Boolean(entitySearch.ok),
        source: entitySearch.source,
        reason: entitySearch.reason || '',
        error: entitySearch.error || '',
        count: result.entity_results.length,
      };
    } else {
      result.sources.entities = {
        searched: false,
        reason: 'no_entity_name_inferred',
        count: 0,
      };
    }
  }

  return result;
}

const PROFILE_ASPECT_QUERIES = Object.freeze({
  communication_style: ['communication style', 'writing style', 'tone', 'direct feedback', 'evidence based'],
  work_patterns: ['high-agency', 'thorough review', 'fix commit', 'test verify', 'debug root cause'],
  preferences: ['preference', 'prefers', 'wants', 'likes', 'explicit request'],
  leadership_style: ['leadership style', 'team strategy', 'direct feedback', 'decision framework'],
  people_context: ['colleague', 'manager', 'team', 'project', 'reports to'],
});

function normalizeAspects(aspects) {
  const list = Array.isArray(aspects) ? aspects : [];
  const allowed = Object.keys(PROFILE_ASPECT_QUERIES);
  const normalized = list
    .map(item => String(item || '').trim().toLowerCase())
    .filter(item => allowed.includes(item));
  return normalized.length ? [...new Set(normalized)] : allowed.slice(0, 4);
}

function compactSnippet(text, max = 260) {
  const cleaned = String(text || '').replace(/\s+/g, ' ').trim();
  return cleaned.length > max ? cleaned.slice(0, Math.max(0, max - 1)).trimEnd() + '...' : cleaned;
}

function classifySourceError(err, fallbackReason = 'source_read_failed') {
  const code = String(err?.code || '');
  const message = String(err?.message || err || '');
  const text = `${code} ${message}`.toLowerCase();
  let reason = fallbackReason;
  if (/sqlite_(corrupt|notadb)|database disk image is malformed|malformed database|database schema is corrupt/.test(text)) {
    reason = 'sqlite_structural_corruption';
  } else if (/sqlite_(busy|locked)|database is locked|database table is locked/.test(text)) {
    reason = 'sqlite_locked';
  } else if (/sqlite_cantopen|unable to open database file/.test(text)) {
    reason = 'sqlite_unavailable';
  } else if (/database not initialized/.test(text)) {
    reason = 'sqlite_not_initialized';
  }
  return {
    ok: false,
    reason,
    error: message,
    code: code || undefined,
  };
}

function sourceStatus(source, patch = {}) {
  return {
    searched: true,
    ok: true,
    source,
    reason: '',
    error: '',
    count: 0,
    ...patch,
  };
}

function safeBrainSearchMemories(args = {}) {
  try {
    const results = brain.searchMemories(args);
    return {
      results,
      source: sourceStatus('wall-e-memory', {
        ok: true,
        count: results.length,
      }),
    };
  } catch (err) {
    const failure = classifySourceError(err, 'wall_e_memory_search_failed');
    return {
      results: [],
      source: sourceStatus('wall-e-memory', {
        ...failure,
        count: 0,
      }),
    };
  }
}

async function searchWallEMemories({ query, limit, filters = {}, hybrid = true, terms } = {}) {
  const base = safeBrainSearchMemories({ query, limit, terms, ...filters });
  let results = base.results;
  let vectorSearchUsed = false;
  const vectorStatus = { attempted: false, ok: true, reason: '', error: '' };

  if (base.source.ok && hybrid && _embeddings && _embeddings.isAvailable()) {
    vectorStatus.attempted = true;
    try {
      const queryEmb = await _embeddings.computeEmbedding(query);
      if (queryEmb) {
        const vectorResults = _embeddings.searchSimilarMemories(queryEmb, limit);
        if (vectorResults.length > 0) {
          results = _embeddings.reciprocalRankFusion(results, vectorResults, limit);
          vectorSearchUsed = true;
        }
      }
    } catch (err) {
      const failure = classifySourceError(err, 'wall_e_vector_search_failed');
      vectorStatus.ok = false;
      vectorStatus.reason = failure.reason;
      vectorStatus.error = failure.error;
    }
  }

  return {
    results,
    vector_search_used: vectorSearchUsed,
    source: sourceStatus('wall-e-memory', {
      ...base.source,
      count: results.length,
      vector: vectorStatus,
    }),
  };
}

function evidenceFromMemory(memory, aspect) {
  return {
    kind: 'memory',
    aspect,
    id: memory.id,
    source: memory.source,
    source_id: memory.source_id,
    memory_type: memory.memory_type,
    timestamp: memory.timestamp,
    snippet: compactSnippet(memory.content),
  };
}

function evidenceFromKnowledge(row, aspect) {
  return {
    kind: 'knowledge',
    aspect,
    id: row.id,
    category: row.category,
    subject: row.subject,
    predicate: row.predicate,
    object: compactSnippet(row.object, 220),
    confidence: row.confidence,
    timestamp: row.last_confirmed || row.first_seen || '',
  };
}

function profileAspectEvidence(aspect, limit) {
  const seen = new Set();
  const rows = [];
  const addMemory = (memory) => {
    const key = `m:${memory.id || memory.source_id || memory.content}`;
    if (seen.has(key)) return;
    seen.add(key);
    rows.push(evidenceFromMemory(memory, aspect));
  };
  const addKnowledge = (row) => {
    const key = `k:${row.id || `${row.subject}:${row.predicate}:${row.object}`}`;
    if (seen.has(key)) return;
    seen.add(key);
    rows.push(evidenceFromKnowledge(row, aspect));
  };

  for (const query of PROFILE_ASPECT_QUERIES[aspect] || []) {
    if (rows.length >= limit) break;
    const memorySearch = safeBrainSearchMemories({ query, limit: Math.max(limit, 4) });
    for (const memory of memorySearch.results) {
      addMemory(memory);
      if (rows.length >= limit) break;
    }
  }

  if (aspect === 'preferences') {
    try {
      for (const row of brain.findKnowledge({ category: 'preference', status: 'active', limit, orderBy: 'confidence DESC' })) {
        addKnowledge(row);
        if (rows.length >= limit) break;
      }
    } catch {}
  }

  return rows.slice(0, limit);
}

function buildUserProfile(args = {}) {
  const limit = clampLimit(args.limit, 6, 20);
  const aspects = normalizeAspects(args.aspects);
  const profile = {
    owner: brain.getOwnerName?.() || brain.getOwner?.('name') || '',
    aspects: {},
    evidence: [],
    updated_at: new Date().toISOString(),
  };

  for (const aspect of aspects) {
    const evidence = profileAspectEvidence(aspect, limit);
    profile.aspects[aspect] = {
      evidence_count: evidence.length,
      signals: evidence.slice(0, 3).map(item => item.snippet || item.object || '').filter(Boolean),
    };
    profile.evidence.push(...evidence);
  }

  return profile;
}

function routeConfidence(route) {
  const names = new Set((route.matched || []).map(match => match.name));
  if (names.has('explicit_wall_e') || names.has('prior_context') || names.has('session_context') || names.has('person_lookup') || names.has('self_profile')) return 'high';
  if (names.has('work_memory')) return 'medium';
  if (route.route === 'public_or_current_fact' || route.route === 'live_source_or_action') return 'high';
  return route.should_use_walle ? 'medium' : 'low';
}

function shouldIncludeProfile(route, args) {
  if (args.include_profile === false) return false;
  const names = new Set((route.matched || []).map(match => match.name));
  return Boolean(route.should_use_walle && (
    names.has('self_profile')
    || names.has('work_memory')
    || names.has('person_lookup')
    || names.has('explicit_wall_e')
  ));
}

function contextLineFromMemory(memory) {
  const date = memory.timestamp ? String(memory.timestamp).slice(0, 10) : '';
  return `- memory ${memory.id || ''}${date ? ` (${date})` : ''}: ${compactSnippet(memory.content || memory.snippet || '', 220)}`;
}

function contextLineFromSession(item) {
  const sid = item.session_id || item.ctm_session_id || item.agent_session_id || '';
  const title = compactSnippet(item.title || '', 80);
  const snippet = compactSnippet(item.snippet || item.content || '', 220);
  return `- session ${sid}${title ? ` "${title}"` : ''}: ${snippet}`;
}

function contextLineFromEntity(entity) {
  return `- entity ${entity.name || entity.canonical_name || ''}${entity.type ? ` (${entity.type})` : ''}`;
}

function renderPromptContext({ route, profile, lookup, budget }) {
  const lines = [
    '<wall-e-context>',
    `Route: ${route.route}; reason: ${route.reason}`,
  ];
  if (route.should_use_walle) {
    lines.push('Instruction: Use this private Wall-E context before public web search or guesswork. If the context is insufficient, call Wall-E MCP tools before answering.');
  }

  const profileSignals = [];
  for (const [aspect, data] of Object.entries(profile?.aspects || {})) {
    for (const signal of data.signals || []) {
      if (signal) profileSignals.push(`- ${aspect}: ${compactSnippet(signal, 180)}`);
      if (profileSignals.length >= 4) break;
    }
    if (profileSignals.length >= 4) break;
  }
  if (profileSignals.length) lines.push('', 'Compact profile:', ...profileSignals);

  const memories = (lookup?.memory_results || []).slice(0, 4).map(contextLineFromMemory);
  if (memories.length) lines.push('', 'Memory evidence:', ...memories);

  const sessions = (lookup?.session_results || []).slice(0, 4).map(contextLineFromSession);
  if (sessions.length) lines.push('', 'Prior session evidence:', ...sessions);

  const entities = (lookup?.entity_results || []).slice(0, 3).map(contextLineFromEntity);
  if (entities.length) lines.push('', 'Entity evidence:', ...entities);

  lines.push('</wall-e-context>');
  const rendered = lines.join('\n');
  return rendered.length > budget ? rendered.slice(0, Math.max(0, budget - 18)).trimEnd() + '\n</wall-e-context>' : rendered;
}

async function buildContextHint(args = {}) {
  const query = String(args.query || '').trim();
  if (!query) throw new Error('query is required');
  const route = routeWallEQuery(query, { context: args.context });
  const mustUseWallE = Boolean(route.should_use_walle || args.force_memory);
  const mode = String(args.mode || 'preflight').toLowerCase();
  const budget = clampLimit(args.budget || 1800, 1800, 8000);
  const recommendedTools = [
    ...new Set([...(route.primary_tools || []), ...(route.secondary_tools || [])]
      .filter(tool => typeof tool === 'string' && !tool.includes('again'))),
  ];

  let compactProfile = null;
  if (shouldIncludeProfile(route, args)) {
    compactProfile = buildUserProfile({
      aspects: route.matched?.some(match => match.name === 'self_profile')
        ? ['communication_style', 'work_patterns', 'leadership_style', 'preferences']
        : ['preferences', 'communication_style', 'work_patterns'],
      limit: mode === 'full' ? 8 : 4,
    });
  }

  let lookup = null;
  if (mustUseWallE && args.include_lookup !== false) {
    lookup = await lookupWallEContext({
      query,
      context: args.context,
      limit: mode === 'full' ? 8 : 4,
      force_memory: args.force_memory,
      include_sessions: true,
      include_memories: true,
      include_entities: true,
    });
  }

  const promptContext = mustUseWallE
    ? renderPromptContext({ route, profile: compactProfile, lookup, budget })
    : '';

  return {
    route,
    confidence: routeConfidence(route),
    must_use_walle_before_answer: mustUseWallE,
    should_inject_context: Boolean(mustUseWallE && promptContext),
    recommended_tools: recommendedTools,
    skipped_reason: mustUseWallE ? '' : route.reason,
    compact_profile: compactProfile,
    lookup,
    prompt_context: promptContext,
    sources: {
      profile: { included: Boolean(compactProfile), evidence_count: compactProfile?.evidence?.length || 0 },
      lookup: lookup?.sources || null,
    },
  };
}

function toolListUsedWalle(usedTools = []) {
  return (Array.isArray(usedTools) ? usedTools : [])
    .some(tool => /^walle_|^mcp__wall[-_]?e|search_memories|ask_walle|entity_search|knowledge_query/i.test(String(tool || '')));
}

function answerShowsMemoryMiss(answer) {
  const text = String(answer || '').toLowerCase();
  return /\b(can't|cannot|couldn't|do not|don't)\s+(access|know|see|retrieve)\b/.test(text)
    || /\bi (only|just) (know|have) (the )?(current|visible) (chat|conversation|context)\b/.test(text)
    || /\bsearched (online|the web|google)\b/.test(text);
}

function buildPostflightCheck(args = {}) {
  const query = String(args.query || '').trim();
  if (!query) throw new Error('query is required');
  const preflight = args.preflight && typeof args.preflight === 'object' ? args.preflight : null;
  const route = preflight?.route || routeWallEQuery(query, { context: args.context });
  const shouldUseWallE = Boolean(preflight?.must_use_walle_before_answer || route.should_use_walle);
  const usedWalle = Boolean(args.used_walle || toolListUsedWalle(args.used_tools) || preflight?.context_was_injected);
  const warnings = [];

  if (shouldUseWallE && !usedWalle) {
    warnings.push('Wall-E context was required by routing, but no Wall-E tool use or injected context was observed.');
  }
  if (shouldUseWallE && answerShowsMemoryMiss(args.answer)) {
    warnings.push('The answer appears to claim missing private context or to substitute public search for Wall-E memory.');
  }

  const missed = warnings.length > 0;
  return {
    status: missed ? 'warning' : 'pass',
    missed_walle: missed,
    should_flag: missed,
    warnings,
    evidence: {
      route: route.route,
      should_use_walle: shouldUseWallE,
      used_walle: usedWalle,
      matched: route.matched || [],
      answer_checked: Boolean(args.answer),
    },
    recommendation: missed
      ? 'Re-run the turn with walle_context_hint or walle_lookup_context before answering.'
      : 'No Wall-E routing miss detected.',
  };
}

function inferEntityName(query) {
  const text = String(query || '').trim();
  // End-anchored question patterns (case-insensitive): the whole tail is the name.
  for (const pattern of [
    /\bdo you know\s+(.+?)[?.!]?$/i,
    /\bwho is\s+(.+?)[?.!]?$/i,
    /\bwhat is\s+(.+?)'s role[?.!]?$/i,
    /\bwhat is the role of\s+(.+?)[?.!]?$/i,
  ]) {
    const match = text.match(pattern);
    if (match?.[1]) {
      return match[1].replace(/^["']|["']$/g, '').trim();
    }
  }
  // Mid-sentence references to a proper-noun name (1-3 capitalized words), e.g.
  // "search history about Eric Gu", "write Eric Gu's anniversary note",
  // "a note for Eric Gu". Case-sensitive so it keys on the capitalized name.
  const NAME = "([A-Z][\\w.-]*(?:\\s+[A-Z][\\w.-]*){0,2})";
  for (const pattern of [
    new RegExp("\\b(?:note|message|email|card|memo)\\s+(?:to|for|about)\\s+" + NAME),
    new RegExp("\\babout\\s+" + NAME),
    new RegExp("\\bfor\\s+" + NAME + "(?:'s)?\\b"),
    new RegExp(NAME + "'s\\b"),
  ]) {
    const match = text.match(pattern);
    if (match?.[1]) return match[1].trim();
  }
  return '';
}

function searchEntitiesForContext(name, limit) {
  const results = [];
  const exact = brain.findEntity(name);
  if (exact) results.push(exact);
  if (results.length === 0) {
    const fuzzy = brain.findEntityFuzzy(name);
    if (fuzzy) results.push(fuzzy);
  }
  if (results.length === 0) {
    const lower = String(name || '').toLowerCase();
    results.push(...brain.listEntities({ limit }).filter(entity =>
      String(entity.canonical_name || '').toLowerCase().includes(lower)
    ));
  }
  return results.slice(0, limit).map(entity => ({
    id: entity.id,
    name: entity.canonical_name,
    type: entity.entity_type,
    aliases: entity.aliases,
  }));
}

function safeSearchEntitiesForContext(name, limit) {
  try {
    const results = searchEntitiesForContext(name, limit);
    return {
      ok: true,
      source: 'wall-e-entities',
      results,
      count: results.length,
    };
  } catch (err) {
    const failure = classifySourceError(err, 'wall_e_entity_search_failed');
    return {
      ...failure,
      source: 'wall-e-entities',
      results: [],
      count: 0,
    };
  }
}

function compactRepairScan(scan = {}, { limit = 50, include_all = false } = {}) {
  const issues = Array.isArray(scan.issues) ? scan.issues : [];
  const cap = include_all ? issues.length : clampLimit(limit, 50, 500);
  const sample = issues.slice(0, cap);
  return {
    ...scan,
    issues: sample,
    issues_sample: sample,
    issues_truncated: sample.length < issues.length,
    total_issues: issues.length,
  };
}

function clampLimit(value, fallback, max) {
  const n = Number(value || fallback);
  if (!Number.isFinite(n)) return fallback;
  return Math.min(Math.max(Math.trunc(n), 1), max);
}

function formatMemoryResult(memory) {
  return {
    id: memory.id,
    source: memory.source,
    source_id: memory.source_id,
    memory_type: memory.memory_type,
    content: memory.content,
    timestamp: memory.timestamp,
  };
}

function requireWriteConfirmation(toolName, args = {}) {
  if (args.dry_run === true || args.confirm_write === true) return;
  throw new Error(`${toolName} writes Wall-E or CTM state. Run with dry_run=true first, or set confirm_write=true to apply changes.`);
}

function stripRank(item) {
  if (!item || typeof item !== 'object') return item;
  const { rank, ...rest } = item;
  return rest;
}

async function getCtmDbStatus() {
  const health = await ctmContextClient.getHealth();
  return {
    available: !health.unavailable,
    db_path: health.db_path,
    active_source: health.active_source || '',
    fallback_used: Boolean(health.fallback_used),
    serving_priority: health.source === 'ctm-api' ? 'ctm-api-first' : 'ctm-db-first',
    source: health.source || 'ctm-db',
    backend_source: health.backend_source || health.source || 'ctm-db',
    api_transport: health.api_transport || null,
    schema_ok: Boolean(health.ok),
    reason: health.reason || '',
    error: health.error || '',
    cached_tables: health.tables || [],
    expected_tables: ctmSessionContext.CTM_SESSION_TABLES || [],
    missing_tables: health.missing_tables || [],
    table_counts: health.table_counts || {},
    attempts: health.attempts || [],
  };
}

async function safeCtmSearchSessions(args) {
  try {
    return await ctmContextClient.searchSessions(args);
  } catch (err) {
    return { ok: false, source: 'ctm-db', results: [], reason: err.message };
  }
}

async function safeCtmSessionContext(args) {
  try {
    return await ctmContextClient.getSessionContext(args);
  } catch (err) {
    return { ok: false, source: 'ctm-db', sessions: [], messages: [], reason: err.message };
  }
}

async function safeCtmContextPack(args) {
  try {
    return await ctmContextClient.buildContextPack(args);
  } catch (err) {
    return { ok: false, source: 'ctm-db', sessions: [], messages: [], reason: err.message };
  }
}

function assertJsonlPath(filePath) {
  if (!filePath || typeof filePath !== 'string') throw new Error('uri must be a JSONL file path');
  if (filePath.split(/[\\/]+/).includes('..')) throw new Error('uri cannot contain traversal segments');
  if (path.extname(filePath) !== '.jsonl') throw new Error('uri must end in .jsonl');
  const resolved = path.resolve(filePath);
  const stat = fs.statSync(resolved);
  if (!stat.isFile()) throw new Error('uri must point to a file');
  return resolved;
}

function formatSourceAdapter(entry) {
  const schema = entry.schema || {};
  return {
    id: entry.id || schema.adapterId,
    adapterId: schema.adapterId,
    version: schema.version,
    supportsIncremental: Boolean(schema.supportsIncremental),
    defaultPrivacyClass: schema.defaultPrivacyClass,
    declaredTransformations: schema.declaredTransformations || [],
  };
}

function isSessionMemory(memory) {
  const source = memory?.source || '';
  const type = memory?.memory_type || '';
  return source === 'walle-diary'
    || source.endsWith('-jsonl')
    || type.startsWith('coding_session_')
    || type === 'agent_diary';
}

function parseMetadata(memory) {
  try {
    const parsed = JSON.parse(memory?.metadata || '{}');
    return parsed && typeof parsed === 'object' ? parsed : {};
  } catch {
    return {};
  }
}

function formatSessionMemory(memory) {
  const metadata = parseMetadata(memory);
  return {
    id: memory.id,
    source: memory.source,
    source_id: memory.source_id,
    session_id: metadata.sessionId || metadata.sourceId || inferSourceId(memory.source_id),
    cwd: metadata.cwd || memory.source_channel || '',
    branch: metadata.gitBranch || metadata.branch || '',
    memory_type: memory.memory_type,
    timestamp: memory.timestamp,
    snippet: String(memory.content || '').slice(0, 800),
    metadata: {
      itemId: metadata.itemId,
      role: metadata.role,
      toolCalls: metadata.toolCalls,
      filesEdited: metadata.filesEdited || metadata.changed_files,
    },
  };
}

function formatSourceRecord(record, { includeRaw = false } = {}) {
  return {
    source_id: record.sourceId,
    item_id: record.itemId,
    role: record.role,
    memory_type: record.memoryType,
    timestamp: record.timestamp,
    snippet: String(record.content || '').slice(0, 800),
    raw: includeRaw ? record.contentRaw : undefined,
    metadata: record.metadata || {},
  };
}

function inferSourceId(sourceId = '') {
  const parts = String(sourceId).split(':');
  if (parts.length <= 2) return sourceId || '';
  return parts.slice(0, 2).join(':');
}

function getSessionMemories(sourceId, { limit }) {
  const db = brain.getDb();
  return db.prepare(`
    SELECT * FROM memories
    WHERE archived_at IS NULL
      AND (source_id = ? OR source_id LIKE ?)
    ORDER BY timestamp ASC
    LIMIT ?
  `).all(sourceId, `${sourceId}:%`, limit);
}

function countDiaryEntries() {
  try {
    return brain.getDb().prepare(`
      SELECT COUNT(*) as c FROM memories
      WHERE archived_at IS NULL AND source = 'walle-diary' AND memory_type = 'agent_diary'
    `).get().c;
  } catch {
    return 0;
  }
}

function writeDiaryEntry(args = {}) {
  if (!args.session_id) throw new Error('session_id is required');
  const agent = sanitizeToken(args.agent || 'walle');
  const event = sanitizeToken(args.event || 'manual');
  const sourceId = `diary:${agent}:${args.session_id}:${event}`;
  const timestamp = new Date().toISOString();
  const metadata = {
    agent,
    sessionId: args.session_id,
    event,
    cwd: args.cwd || '',
    branch: args.branch || '',
    changed_files: arrayOfStrings(args.changed_files),
    commands: arrayOfStrings(args.commands),
    decisions: arrayOfStrings(args.decisions),
    blockers: arrayOfStrings(args.blockers),
    next_steps: arrayOfStrings(args.next_steps),
  };
  const content = formatDiaryContent({ ...metadata, summary: args.summary || '' });
  const inserted = brain.insertMemory({
    source: 'walle-diary',
    source_id: sourceId,
    source_channel: metadata.cwd,
    memory_type: 'agent_diary',
    direction: 'outbound',
    participants: agent,
    subject: args.session_id,
    content,
    content_raw: content,
    metadata: JSON.stringify(metadata),
    importance: 0.75,
    timestamp,
  });
  return {
    stored: Boolean(inserted),
    duplicate: !inserted,
    id: inserted?.id || null,
    source_id: sourceId,
  };
}

function readDiaryEntries(args = {}) {
  const limit = clampLimit(args.limit, 20, 100);
  const entries = brain.listMemories({ source: 'walle-diary', limit: limit * 3 })
    .filter((memory) => memory.memory_type === 'agent_diary')
    .map((memory) => ({ memory, metadata: parseMetadata(memory) }))
    .filter(({ metadata }) => !args.agent || metadata.agent === args.agent)
    .filter(({ metadata }) => !args.session_id || metadata.sessionId === args.session_id)
    .slice(0, limit)
    .map(({ memory, metadata }) => ({
      id: memory.id,
      source_id: memory.source_id,
      session_id: metadata.sessionId || '',
      agent: metadata.agent || '',
      event: metadata.event || '',
      cwd: metadata.cwd || '',
      branch: metadata.branch || '',
      timestamp: memory.timestamp,
      content: memory.content,
      changed_files: metadata.changed_files || [],
      commands: metadata.commands || [],
      decisions: metadata.decisions || [],
      blockers: metadata.blockers || [],
      next_steps: metadata.next_steps || [],
    }));
  return { entries };
}

function formatDiaryContent(entry) {
  const lines = [];
  lines.push(`Agent diary for ${entry.agent} session ${entry.sessionId} (${entry.event})`);
  if (entry.summary) lines.push(`Summary: ${entry.summary}`);
  if (entry.cwd) lines.push(`CWD: ${entry.cwd}`);
  if (entry.branch) lines.push(`Branch: ${entry.branch}`);
  for (const [label, values] of [
    ['Changed files', entry.changed_files],
    ['Commands', entry.commands],
    ['Decisions', entry.decisions],
    ['Blockers', entry.blockers],
    ['Next steps', entry.next_steps],
  ]) {
    if (values.length) lines.push(`${label}: ${values.join('; ')}`);
  }
  return lines.join('\n');
}

function arrayOfStrings(value) {
  return Array.isArray(value)
    ? value.filter((item) => typeof item === 'string' && item.trim()).map((item) => item.trim())
    : [];
}

function sanitizeToken(value) {
  return String(value || '').trim().replace(/[^A-Za-z0-9_.:-]+/g, '-').slice(0, 80) || 'walle';
}

function reconnectBrain() {
  const db = brain.getDb();
  let checkpoint = null;
  try { checkpoint = db.pragma('wal_checkpoint(PASSIVE)'); } catch {}
  return {
    stats: brain.getBrainStats(),
    checkpoint,
  };
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    let size = 0;
    req.on('data', chunk => {
      size += chunk.length;
      if (size > 1024 * 1024) { req.destroy(); reject(new Error('Body too large')); return; }
      chunks.push(chunk);
    });
    req.on('end', () => {
      try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
      catch { resolve(null); }
    });
    req.on('error', reject);
  });
}

function jsonResponse(res, data, status = 200) {
  const body = JSON.stringify(data);
  res.writeHead(status, {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(body),
  });
  res.end(body);
}

function mcpResponse(id, result) {
  return { jsonrpc: '2.0', id, result };
}

function mcpError(id, code, message) {
  return { jsonrpc: '2.0', id: id === undefined ? null : id, error: { code, message } };
}

async function readMcpResource(uri) {
  if (!uri || typeof uri !== 'string') throw new Error('resource uri is required');

  if (uri === 'walle://status/session-memory') {
    return {
      uri,
      mimeType: 'application/json',
      text: JSON.stringify(await executeTool('walle_memory_status', {})),
    };
  }
  if (uri === 'walle://profile/compact') {
    return {
      uri,
      mimeType: 'application/json',
      text: JSON.stringify(buildUserProfile({ limit: 6 })),
    };
  }

  let parsed;
  try {
    parsed = new URL(uri);
  } catch {
    throw new Error(`Invalid resource uri: ${uri}`);
  }

  if (parsed.protocol === 'walle-session:' && parsed.hostname === 'ctm') {
    const [rawSessionId, rawMode] = parsed.pathname.split('/').filter(Boolean);
    if (!rawSessionId) throw new Error('walle-session resource requires a session id');
    const sessionId = decodeURIComponent(rawSessionId);
    const mode = rawMode || 'full';
    const context = await ctmContextClient.getSessionContext({
      session_id: sessionId,
      limit: mode === 'full' || mode === 'messages' ? 0 : 200,
      format: mode === 'compact' || mode === 'summary' ? 'compact' : 'messages',
      dedupe: true,
    });
    if (mode === 'compact' || mode === 'summary') {
      return {
        uri,
        mimeType: 'text/markdown',
        text: context.text || ctmSessionContext.renderContextMarkdown(context, { compact: true }),
      };
    }
    return {
      uri,
      mimeType: 'application/json',
      text: JSON.stringify(context),
    };
  }

  if (parsed.protocol === 'walle-context:' && parsed.hostname === 'task') {
    const task = decodeURIComponent(parsed.pathname.replace(/^\/+/, ''));
    if (!task) throw new Error('walle-context task resource requires an encoded task query');
    const pack = await ctmContextClient.buildContextPack({ task, mode: 'compact' });
    return {
      uri,
      mimeType: 'text/markdown',
      text: pack.text || ctmSessionContext.renderContextMarkdown(pack, { compact: true }),
    };
  }

  throw new Error(`Unknown resource uri: ${uri}`);
}

function memoryRoutingPromptText(wallePort) {
  return wallEAgentMemoryInstructions(wallePort)
    .replace(/<!-- wall-e-memory-routing:start -->\n?/g, '')
    .replace(/<!-- wall-e-memory-routing:end -->\n?/g, '')
    .trim();
}

function readMcpPrompt(name, args = {}) {
  if (name === 'walle_memory_routing') {
    const port = Number(args.walle_port || process.env.WALL_E_PORT || 3457);
    return {
      description: 'Wall-E MCP memory routing policy',
      messages: [{
        role: 'user',
        content: { type: 'text', text: memoryRoutingPromptText(port) },
      }],
    };
  }
  if (name === 'walle_coding_resume') {
    const task = String(args.task || '').trim();
    const sessionId = String(args.session_id || '').trim();
    const lines = [
      'Resume this coding work using Wall-E MCP session memory before relying on web search or raw files.',
      '',
      'Steps:',
      '1. If Wall-E health is uncertain, call `walle_memory_status`.',
      '2. If the task references Slack, Gmail, calendar, or other live-source access, call `walle_live_capabilities` before assuming tools are missing.',
      '3. Call `walle_context_pack` with the current task to recover prior attempts, decisions, changed files, and next steps.',
      '4. If a session id is provided, call `walle_get_session` for exact source context.',
      '5. Use `walle_search_sessions` for targeted follow-up searches and `walle_diary_read` for recent handoff notes.',
      '6. Cite returned session ids/timestamps when using retrieved context. Use public web only for public/current facts or after Wall-E misses.',
    ];
    if (task) lines.push('', `Task: ${task}`);
    if (sessionId) lines.push(`Session id: ${sessionId}`);
    return {
      description: 'Resume coding work with Wall-E CTM session context',
      messages: [{
        role: 'user',
        content: { type: 'text', text: lines.join('\n') },
      }],
    };
  }
  return null;
}

function buildToolResourceLinks(toolName, result) {
  if (!result || typeof result !== 'object') return [];
  const links = [];
  const addSession = (sessionId, title) => {
    if (!sessionId || links.some(link => link.uri.includes(encodeURIComponent(sessionId)))) return;
    links.push({
      type: 'resource_link',
      uri: `walle-session://ctm/${encodeURIComponent(sessionId)}/compact`,
      name: title || `CTM session ${sessionId}`,
      description: 'Compact CTM session context linked from Wall-E tool output',
      mimeType: 'text/markdown',
      annotations: { audience: ['assistant'], priority: 0.85 },
    });
  };

  if (toolName === 'walle_search_sessions') {
    for (const item of result.results || []) {
      if (item.source_layer === 'ctm-db' || item.source_layer === 'ctm-api') {
        addSession(item.session_id, item.title);
      }
    }
  }
  if (toolName === 'walle_context_pack') {
    for (const item of result.sessions || []) {
      addSession(item.session_id || item.id, item.title);
    }
  }
  if (toolName === 'walle_context_hint' && result.lookup) {
    for (const item of result.lookup.session_results || []) {
      addSession(item.session_id || item.ctm_session_id || item.agent_session_id, item.title);
    }
  }
  return links.slice(0, 5);
}

async function handleMcpMessage(msg, context = {}) {
  if (!msg || !msg.jsonrpc) {
    return { response: mcpError(null, -32700, 'Parse error'), status: 400 };
  }

  // Notifications and responses have no id. Transports decide whether that
  // means "no stdout response" (stdio) or an accepted empty HTTP response.
  if (msg.id === undefined || msg.id === null) {
    return { response: null, status: 202, notification: true };
  }

  const { id, method, params } = msg;

  switch (method) {
    case 'initialize': {
      const sessionId = uuidv4();
      return {
        sessionId,
        response: mcpResponse(id, {
          protocolVersion: PROTOCOL_VERSION,
          capabilities: { tools: {}, resources: {}, prompts: {} },
          serverInfo: { name: 'wall-e', version: (() => { try { return require('./package.json').version; } catch { return '0.0.0'; } })() },
        }),
      };
    }

    case 'tools/list': {
      return { response: mcpResponse(id, { tools: MCP_TOOLS }) };
    }

    case 'resources/list': {
      return { response: mcpResponse(id, { resources: MCP_RESOURCES }) };
    }

    case 'resources/templates/list': {
      return { response: mcpResponse(id, { resourceTemplates: MCP_RESOURCE_TEMPLATES }) };
    }

    case 'prompts/list': {
      return { response: mcpResponse(id, { prompts: MCP_PROMPTS }) };
    }

    case 'prompts/get': {
      const prompt = readMcpPrompt(params?.name, params?.arguments || {});
      if (!prompt) {
        return { response: mcpResponse(id, { content: [{ type: 'text', text: `Unknown prompt: ${params?.name}` }], isError: true }) };
      }
      return { response: mcpResponse(id, prompt) };
    }

    case 'resources/read': {
      try {
        const content = await readMcpResource(params?.uri);
        return { response: mcpResponse(id, { contents: [{ uri: content.uri, mimeType: content.mimeType, text: content.text }] }) };
      } catch (err) {
        return { response: mcpResponse(id, { contents: [{ uri: params?.uri || '', mimeType: 'text/plain', text: `Error: ${err.message}` }], isError: true }) };
      }
    }

    case 'tools/call': {
      const toolName = params?.name;
      const toolArgs = params?.arguments || {};
      try {
        const result = await executeTool(toolName, toolArgs, context);
        if (result === null) {
          return { response: mcpResponse(id, { content: [{ type: 'text', text: `Unknown tool: ${toolName}` }], isError: true }) };
        }
        const content = [{ type: 'text', text: JSON.stringify(result) }].concat(buildToolResourceLinks(toolName, result));
        return { response: mcpResponse(id, { content, structuredContent: result }) };
      } catch (err) {
        return { response: mcpResponse(id, { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true }) };
      }
    }

    default:
      return { response: mcpError(id, -32601, `Method not found: ${method}`) };
  }
}

async function handleMcp(req, res) {
  let msg;
  try {
    msg = await readBody(req);
  } catch {
    return jsonResponse(res, mcpError(null, -32700, 'Parse error'), 400);
  }

  const result = await handleMcpMessage(msg, {
    transport: 'http',
    sessionId: req.headers['mcp-session-id'] || '',
    protocolVersion: req.headers['mcp-protocol-version'] || '',
  });
  if (result.sessionId) res.setHeader('Mcp-Session-Id', result.sessionId);
  res.setHeader('MCP-Protocol-Version', PROTOCOL_VERSION);
  if (!result.response) {
    res.writeHead(result.status || 202);
    res.end();
    return;
  }
  return jsonResponse(res, result.response, result.status || 200);
}

module.exports = {
  handleMcp,
  handleMcpMessage,
  MCP_TOOLS,
  MCP_RESOURCES,
  MCP_RESOURCE_TEMPLATES,
  MCP_PROMPTS,
  executeTool,
  buildContextHint,
  buildPostflightCheck,
  buildUserProfile,
  lookupWallEContext,
  readMcpPrompt,
  readMcpResource,
  routeWallEQuery,
};
