/**
 * xmem — Graph-based Long-Term Memory Plugin for OpenClaw
 * v8.1.0 "Cold-start fix" — scan-root guard (never deep-scan home/system dirs),
 *   non-blocking onboarding check (8s timeout, fail-open), per-call bbFetch timeout.
 * v8.0.3 "Hardening" — AbortController timeouts (20s) on the two direct LLM
 *                      fetch calls (llmExtract + resolve-contradictions) so a
 *                      busy/blocked local gateway LLM can never hang an agent
 *                      run. Matches the existing 30s bbFetch protection.
 * v8.0.0 "AST & Intelligence" — AST code analysis, audio/video transcription,
 *                                community detection, god-node hygiene, surprising
 *                                connections, and full graph health reports.
 * v7.4.0 "Cache & Ignore" — persistent SHA256 cache (.xmem-hashes.json) skips
 *                           unchanged files; .xmemignore for user-configurable exclusions.
 * v7.3.0 "Deep Capture" — full workspace ingestion (all relevant files, 4 levels
 *                         deep), periodic re-scan every 2h, multi-agent aware.
 *
 * Tools:
 *  - memtap_scan          — workspace file scanner (action: workspace|ingest|ingest-all|status|diff)
 *  - memtap_recall        — semantic graph recall
 *  - memtap_remember      — store a memory in the graph (supports immutable flag)
 *  - memtap_health        — server health check and statistics
 *  - memtap_intent        — intent detection & pattern matching
 *  - memtap_manage        — memory management (actions: get|update|delete|list-entities|merge-entities|entity-memories|create-edge|graph-overview|graph-gaps|graph-clusters|graph-connections|graph-traverse|decay-report|contradictions|dedup-scan|resolve-contradictions|run-all|attach)
 *  - memtap_analyze       — analysis tools (actions: graphrag|bulletin|infer|profile|export)
 *  - memtap_track         — decision tracking (actions: list-decisions|create-decision|resolve-decision|defer-decision|record-outcome|consolidate)
 *  - memtap_code          — AST code analysis (actions: languages|stats|symbols|calls)
 *  - memtap_transcribe    — audio/video transcription via server
 *  - memtap_communities   — Louvain community detection (actions: list|compute|get)
 *  - memtap_graph_hygiene — god-node detection for graph pollution
 *  - memtap_surprising    — discover semantically close but unlinked entity pairs
 *  - memtap_graph_report  — comprehensive graph health report (json|markdown)
 *  - memory_recall        — OpenClaw alias for recall
 *  - memory_store         — OpenClaw alias for remember
 *  - memory_forget        — OpenClaw alias for delete
 *  - memory_search        — OpenClaw alias for search
 *
 * Supplements (OpenClaw v2026.4.9):
 *  - MemoryCorpusSupplement  — exposes xmem via standard memory_search tool
 *  - MemoryPromptSupplement  — injects bulletin as memory prompt section on session start
 *
 * Hooks:
 *  - preMessage         — neuromimetic tiered recall with working memory simulation + adaptive decay reinforcement
 *                         + proactive surfacing of thematically related memories
 *                         + decision outcome context injection (learning loop)
 *  - message_completed  — attention-gated encoding with emotional weighting + auto-category assignment
 *                         + proactive memory usage tracking with importance reinforcement
 *  - pre-compact-buffer — periodic + token-threshold + message-count extraction to preserve knowledge before compaction
 *  - session-end-capture — flush remaining conversation buffer at session end
 *  - agent:bootstrap    — auto-onboarding (MD scrape on first run) + instructions injection
 *  - periodic           — persistent dream-cycle (API-backed) + workspace MD capture + neural maintenance
 *  - session_end        — performance monitoring and neural analytics
 */

import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as crypto from 'crypto';

// ── Helpers ──────────────────────────────────────────────────────────────────

// [v7.2] Per-agent Space/Key override for Multi-Agent-on-one-Instance setups.
//   agents: {
//     "memtap":      { "apiKey": "xm_live_AAA...", "serverUrl": "https://..." },
//     "psiclips":    { "apiKey": "xm_live_BBB..." },
//     "content-bot": { "apiKey": "xm_live_CCC..." }
//   }
// If no per-agent entry exists, plugin falls back to top-level apiKey/serverUrl.
interface MemTapAgentOverride {
  apiKey?: string;
  serverUrl?: string;
}

interface MemTapConfig {
  serverUrl?: string;
  apiKey?: string;
  agentId?: string;
  /** v7.2 — per-agent overrides (apiKey/serverUrl). Key = agentId from openclaw.json agents.list[].id */
  agents?: Record<string, MemTapAgentOverride>;
  autoCapture?: boolean;
  captureEnabled?: boolean;
  captureMinLength?: number;
  debug?: boolean;
  bulletinOnBoot?: boolean;
  bulletinTopics?: string[];
  llmUrl?: string;
  llmModel?: string;
  embeddingUrl?: string;
  embeddingModel?: string;
  embeddingApiKey?: string;
  decayRate?: number;
  dreamEnabled?: boolean;
  dreamTime?: string;
  instructions?: {
    include?: string[];
    exclude?: string[];
  };
}

function getConfig(api: any): MemTapConfig {
  const entries = api.config?.plugins?.entries ?? {};
  return entries.memtap?.config ?? {};
}

/**
 * [v7.2] Resolve effective config for a specific agent. Falls back to base config.
 * @param cfg Base plugin config
 * @param resolvedAgentId The agent id we're currently acting for (from event.context or config)
 */
function resolveConfigForAgent(cfg: MemTapConfig, resolvedAgentId: string): MemTapConfig {
  const override = cfg.agents?.[resolvedAgentId];
  if (!override) return cfg;
  return {
    ...cfg,
    apiKey: override.apiKey ?? cfg.apiKey,
    serverUrl: override.serverUrl ?? cfg.serverUrl,
  };
}

function baseUrl(cfg: MemTapConfig): string {
  const url = (cfg.serverUrl || 'https://api.xmem.space').replace(/\/$/, '');
  // Ensure /v1 prefix for production API
  return url.endsWith('/v1') ? url : `${url}/v1`;
}

/**
 * [v7.2] Static agent id from config. Use `resolveAgentId(event, cfg, api)` in hook handlers
 * to honor the dynamic agentId supplied by OpenClaw per event.
 */
function agentId(cfg: MemTapConfig, api: any): string {
  return cfg.agentId || api.config?.agents?.defaults?.id || 'main';
}

/**
 * [v7.2] Resolve the active agent id for a given hook event.
 * Priority:
 *   1. event.context.agentId (OpenClaw passes this in every hook)
 *   2. event.agentId (some hooks expose it at top level)
 *   3. Fallback to static config (backward compatible)
 */
function resolveAgentId(event: any, cfg: MemTapConfig, api: any): string {
  const fromCtx = event?.context?.agentId;
  if (typeof fromCtx === 'string' && fromCtx.length > 0) return fromCtx;
  const fromEvt = event?.agentId;
  if (typeof fromEvt === 'string' && fromEvt.length > 0) return fromEvt;
  return agentId(cfg, api);
}

async function bbFetch(cfg: MemTapConfig, url: string, opts: RequestInit & { timeoutMs?: number } = {}): Promise<any> {
  const headers: any = { 'Content-Type': 'application/json', ...(opts.headers || {}) };

  // Add API key authentication if available
  if (cfg.apiKey) {
    headers['Authorization'] = `Bearer ${cfg.apiKey}`;
  }

  // [v8.1] Per-call timeout override. Default 30s for general calls, but
  // latency-sensitive paths (onboarding check) pass a short timeout so a
  // single slow/hanging call can never stall an agent bootstrap turn.
  const timeoutMs = typeof opts.timeoutMs === 'number' ? opts.timeoutMs : 30_000;
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);

  let res: Response;
  try {
    res = await fetch(url, {
      ...opts,
      headers,
      signal: controller.signal,
    });
  } catch (err: any) {
    clearTimeout(timeout);
    if (err.name === 'AbortError') {
      throw new Error(`xmem API request timed out after 30s: ${url}`);
    }
    throw new Error(`xmem API unreachable (${err.message}). Check serverUrl and network connectivity.`);
  } finally {
    clearTimeout(timeout);
  }

  if (!res.ok) {
    const text = await res.text().catch(() => '');
    if (res.status === 401) {
      throw new Error('xmem authentication failed. Please check your API key.');
    }
    throw new Error(`xmem API ${res.status}: ${text}`);
  }
  return res.json();
}

/** Server stores importance as 0-1, we display 1-10 to users */
function displayImportance(serverValue: number): number {
  return Math.round(serverValue * 10) || 1;
}

/** Users provide importance as 1-10, we store as 0-1 */
function storeImportance(userValue: number): number {
  return Math.min(1, Math.max(0, userValue / 10));
}

/** SHA256 content hash for file change detection */
function sha256Hash(s: string): string {
  return crypto.createHash('sha256').update(s, 'utf-8').digest('hex');
}

/** @deprecated v7.4 — use sha256Hash. Kept for ingest/ingest-all backward compat. */
function simpleHash(s: string): string {
  let h = 0;
  for (let i = 0; i < s.length; i++) {
    h = ((h << 5) - h + s.charCodeAt(i)) | 0;
  }
  return h.toString(36);
}

// ── Persistent SHA256 Cache (.xmem-hashes.json) ────────────────────────────

interface HashCacheFileEntry {
  sha256: string;
  sizeBytes: number;
  mtimeMs: number;
  lastSentAt: string;
  endpoints: string[];
}

interface HashCacheAgentData {
  lastScan: string;
  serverUrlHash: string;
  files: Record<string, HashCacheFileEntry>;
}

interface HashCacheFile {
  version: number;
  agents: Record<string, HashCacheAgentData>;
}

function loadHashCache(workspacePath: string): HashCacheFile {
  const cacheFile = path.join(workspacePath, '.xmem-hashes.json');
  try {
    const raw = fs.readFileSync(cacheFile, 'utf-8');
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object' && parsed.version === 1 && parsed.agents) {
      return parsed as HashCacheFile;
    }
    console.warn('[memtap] .xmem-hashes.json: unexpected format, starting fresh');
  } catch (err: any) {
    if (err.code !== 'ENOENT') {
      console.warn(`[memtap] .xmem-hashes.json: could not read (${err.message}), starting fresh`);
    }
  }
  return { version: 1, agents: {} };
}

function saveHashCache(workspacePath: string, cache: HashCacheFile): void {
  const cacheFile = path.join(workspacePath, '.xmem-hashes.json');
  try {
    fs.writeFileSync(cacheFile, JSON.stringify(cache, null, 2), 'utf-8');
  } catch (err: any) {
    console.warn(`[memtap] .xmem-hashes.json: could not write (${err.message}), cache not persisted`);
  }
}

function getAgentCache(cache: HashCacheFile, agentId: string, serverUrl: string): HashCacheAgentData {
  const serverUrlHash = crypto.createHash('sha256').update(serverUrl).digest('hex').slice(0, 16);
  if (!cache.agents[agentId]) {
    cache.agents[agentId] = { lastScan: '', serverUrlHash, files: {} };
  }
  const agentCache = cache.agents[agentId];
  // Invalidate if server changed
  if (agentCache.serverUrlHash !== serverUrlHash) {
    console.log(`[memtap] cache invalidated for agent=${agentId}: serverUrl changed`);
    cache.agents[agentId] = { lastScan: '', serverUrlHash, files: {} };
  }
  return cache.agents[agentId];
}

// ── .xmemignore Support ────────────────────────────────────────────────────

let ignoreModule: any = null;

function loadIgnoreInstance(workspacePath: string, debug?: boolean): any | null {
  // Lazy-load the ignore package
  if (!ignoreModule) {
    try {
      ignoreModule = require('ignore');
    } catch {
      if (debug) console.warn('[memtap] ignore package not available, .xmemignore disabled');
      return null;
    }
  }

  const ignorePath = path.join(workspacePath, '.xmemignore');
  try {
    const content = fs.readFileSync(ignorePath, 'utf-8');
    const ig = ignoreModule.default ? ignoreModule.default() : ignoreModule();
    ig.add(content);
    const patternCount = content.split(/\r?\n/).filter((l: string) => l.trim() && !l.trim().startsWith('#')).length;
    if (debug) console.log(`[memtap] .xmemignore loaded: ${patternCount} patterns`);
    return ig;
  } catch (err: any) {
    if (err.code !== 'ENOENT') {
      console.warn(`[memtap] .xmemignore: could not read (${err.message}), proceeding without`);
    }
    return null;
  }
}

/** Workspace files tracking for ingestion */
interface WorkspaceFileTracker {
  filename: string;
  lastHash: string;
  lastSeen: number;
}

// Global workspace files tracking (per agent)
const workspaceFileHashes = new Map<string, Map<string, WorkspaceFileTracker>>();

function getWorkspaceFileTracker(agentId: string): Map<string, WorkspaceFileTracker> {
  if (!workspaceFileHashes.has(agentId)) {
    workspaceFileHashes.set(agentId, new Map());
  }
  return workspaceFileHashes.get(agentId)!;
}

// Dream mode scheduling — persisted via xmem API (survives gateway restarts)

/** Fetch lastDreamDate from API (tag system:dream-schedule) */
async function getLastDreamDate(cfg: MemTapConfig, agent: string): Promise<string> {
  try {
    const res = await bbFetch(cfg, `${baseUrl(cfg)}/memories?tags=system:dream-schedule&agent=${agent}&limit=1`);
    const memories = res.memories || res.results || res || [];
    const mem = Array.isArray(memories) ? memories[0] : null;
    return mem?.metadata?.lastDreamDate || mem?.content || '';
  } catch { return ''; }
}

/** Persist lastDreamDate to API */
async function persistDreamDate(cfg: MemTapConfig, agent: string, date: string): Promise<void> {
  try {
    // Try to find existing dream-schedule memory
    const res = await bbFetch(cfg, `${baseUrl(cfg)}/memories?tags=system:dream-schedule&agent=${agent}&limit=1`);
    const memories = res.memories || res.results || res || [];
    const existing = Array.isArray(memories) ? memories[0] : null;

    if (existing?.id || existing?._key) {
      const id = existing.id || existing._key;
      await bbFetch(cfg, `${baseUrl(cfg)}/memories/${id}`, {
        method: 'PUT',
        body: JSON.stringify({ content: date, metadata: { lastDreamDate: date } }),
      });
    } else {
      await bbFetch(cfg, `${baseUrl(cfg)}/memories`, {
        method: 'POST',
        body: JSON.stringify({
          content: date,
          agent,
          type: 'fact',
          importance: 0.1,
          tags: ['system:dream-schedule'],
          metadata: { lastDreamDate: date },
          source: 'plugin:dream-schedule',
        }),
      });
    }
  } catch { /* silent */ }
}

async function shouldRunDreamMode(cfg: MemTapConfig, agent: string): Promise<boolean> {
  if (cfg.dreamEnabled === false) return false;

  const dreamTime = cfg.dreamTime || '03:00';
  const now = new Date();
  const todayDate = now.toISOString().split('T')[0];
  const currentTime = now.toTimeString().slice(0, 5);

  const lastDate = await getLastDreamDate(cfg, agent);
  if (lastDate === todayDate) return false;

  const [dreamHour, dreamMinute] = dreamTime.split(':').map(Number);
  const [currentHour, currentMinute] = currentTime.split(':').map(Number);
  const dreamMinutes = dreamHour * 60 + dreamMinute;
  const currentMinutes = currentHour * 60 + currentMinute;

  return Math.abs(dreamMinutes - currentMinutes) <= 30;
}

async function markDreamCompleted(cfg: MemTapConfig, agent: string): Promise<void> {
  const todayDate = new Date().toISOString().split('T')[0];
  await persistDreamDate(cfg, agent, todayDate);
}

// [v6.0] Files matching these patterns are NEVER sent to the capture API.
// This is a security guard — passwords, keys, credentials, and similar files
// must not leak into the knowledge graph.
const SENSITIVE_FILE_PATTERNS: RegExp[] = [
  /password/i,
  /secret/i,
  /credential/i,
  /\bkey(s)?\b/i,
  /\btoken(s)?\b/i,
  /\bapi[-_.]?key/i,
  /\.env/i,
  /private/i,
  /\bauth\b/i,
];

// Files whose content looks like secrets are rejected even if the filename is innocent.
const SECRET_CONTENT_PATTERNS: RegExp[] = [
  /\b(?:sk|pk|rk)-[A-Za-z0-9_-]{20,}/,           // Stripe, OpenAI, etc.
  /\bmt_(?:live|test)_[a-f0-9]{40,}/i,            // xmem API keys
  /AKIA[0-9A-Z]{16}/,                              // AWS access key id
  /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PRIVATE) KEY-----/,
  /ghp_[A-Za-z0-9]{30,}/,                          // GitHub personal access token
  /xox[baprs]-[A-Za-z0-9-]{10,}/,                  // Slack token
];

function isSensitiveFilename(filename: string): boolean {
  const base = filename.toLowerCase();
  return SENSITIVE_FILE_PATTERNS.some((re) => re.test(base));
}

function containsSecrets(content: string): boolean {
  return SECRET_CONTENT_PATTERNS.some((re) => re.test(content));
}

// [v7.3] Deep Capture — all files an OpenClaw instance produces, not just .md.
// Extensions the plugin considers worth ingesting. Binary + huge-repo files are
// excluded by dir-skiplist + size cap + extension allowlist.
const CAPTURE_EXTENSIONS = new Set([
  // Docs & notes
  '.md', '.markdown', '.txt', '.rst', '.org', '.adoc',
  // Config & data
  '.json', '.jsonc', '.json5', '.yaml', '.yml', '.toml', '.ini', '.env.example', '.conf',
  // Code
  '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
  '.py', '.go', '.rs', '.java', '.kt', '.swift', '.rb', '.php', '.cs',
  '.c', '.h', '.cpp', '.hpp', '.sh', '.bash', '.zsh', '.fish',
  '.sql', '.graphql', '.proto',
  // Web
  '.html', '.htm', '.xml', '.svg', '.vue', '.svelte', '.astro',
  '.css', '.scss', '.sass', '.less',
  // Infra
  '.dockerfile', '.tf', '.tfvars', '.hcl',
  // Data
  '.csv', '.tsv',
  // OpenClaw/xmem specific
  '.plugin.json', '.openclaw.json',
]);

// Special filenames without traditional extensions
const CAPTURE_FILENAMES = new Set([
  'Dockerfile', 'Makefile', 'Rakefile', 'Gemfile', 'Procfile', 'Pipfile',
  'CHANGELOG', 'README', 'LICENSE', 'CONTRIBUTING', 'AUTHORS', 'NOTICE',
  'openclaw.json', 'package.json', 'tsconfig.json', 'pyproject.toml',
  'go.mod', 'go.sum', 'Cargo.toml', 'requirements.txt',
  '.gitignore', '.dockerignore', '.editorconfig',
]);

function isCaptureFile(name: string): boolean {
  // [v7.4] Never capture our own cache file — avoids a feedback loop where
  // the SHA cache gets uploaded as an artifact on every scan.
  if (name === '.xmem-hashes.json') return false;
  if (CAPTURE_FILENAMES.has(name)) return true;
  const lower = name.toLowerCase();
  // Exact dotfiles with no extension logic
  if (CAPTURE_FILENAMES.has(lower)) return true;
  // Extension check
  const ext = path.extname(lower);
  if (ext && CAPTURE_EXTENSIONS.has(ext)) return true;
  // Compound extensions like .plugin.json, .openclaw.json
  if (lower.endsWith('.plugin.json') || lower.endsWith('.openclaw.json')) return true;
  return false;
}

/** [v7.3] Collect all capturable files in workspace (not just .md).
 *  [v7.4] Added ignoreInstance param for .xmemignore support. */
/**
 * [v8.1] Guard against scanning an oversized / unsafe root (e.g. the whole
 * home directory). A misconfigured or missing workspace context previously
 * fell back to process.cwd(), which on some hosts is the user's entire
 * home (/home/sebbo). Deep-scanning that on every bootstrap caused the
 * multi-minute cold-start hangs and PayloadTooLarge floods.
 *
 * Returns a safe root to scan, or null if scanning should be skipped entirely.
 */
function resolveSafeScanRoot(rootPath: string): string | null {
  try {
    const resolved = path.resolve(rootPath);
    const home = (os.homedir && os.homedir()) || process.env.HOME || '';
    const homeResolved = home ? path.resolve(home) : '';

    // Never scan the home directory itself or any ancestor of it.
    if (homeResolved && (resolved === homeResolved || homeResolved.startsWith(resolved + path.sep))) {
      // Prefer an explicit workspace subdir if one exists under home.
      const candidates = [
        path.join(homeResolved, '.openclaw', 'workspace'),
        path.join(homeResolved, 'workspace'),
      ];
      for (const c of candidates) {
        try { if (fs.statSync(c).isDirectory()) return c; } catch { /* ignore */ }
      }
      console.warn(`[memtap] scan-root guard: refusing to deep-scan home dir (${resolved}) and no workspace subdir found — skipping scan`);
      return null;
    }

    // Refuse obvious system roots.
    const forbidden = new Set(['/', '/root', '/etc', '/usr', '/var', '/tmp', '/opt', '/home']);
    if (forbidden.has(resolved)) {
      console.warn(`[memtap] scan-root guard: refusing to deep-scan system path (${resolved}) — skipping scan`);
      return null;
    }
    return resolved;
  } catch {
    return rootPath;
  }
}

function collectWorkspaceFiles(rootPath: string, maxDepth = 4, ignoreInstance?: any): string[] {
  const results: string[] = [];
  const seen = new Set<string>();
  let ignoredCount = 0;
  const skipDirs = new Set([
    'node_modules', '.git', '.openclaw', 'dist', 'build', 'out', '.next', '.nuxt',
    'coverage', '.cache', '.parcel-cache', '.turbo', '.vite', '.svelte-kit',
    'target', 'vendor', '__pycache__', '.pytest_cache', '.mypy_cache', '.tox',
    'venv', '.venv', 'env', '.env',
    // Sensitive directory names — never descend.
    'secrets', 'credentials', '.ssh', '.gnupg', '.aws', '.kube',
  ]);

  function walk(dir: string, depth: number) {
    if (depth > maxDepth) return;
    let entries: fs.Dirent[] = [];
    try {
      entries = fs.readdirSync(dir, { withFileTypes: true });
    } catch {
      return;
    }

    for (const entry of entries) {
      const full = path.join(dir, entry.name);
      const relativePath = path.relative(rootPath, full);
      if (entry.isDirectory()) {
        if (skipDirs.has(entry.name)) continue;
        if (entry.name.startsWith('.') && entry.name !== '.openclaw' && depth > 0) continue;
        // Check .xmemignore for directories (append / for directory matching)
        if (ignoreInstance && ignoreInstance.ignores(relativePath + '/')) {
          ignoredCount++;
          continue;
        }
        walk(full, depth + 1);
        continue;
      }
      if (!entry.isFile()) continue;
      if (!isCaptureFile(entry.name)) continue;
      if (isSensitiveFilename(entry.name)) continue;
      // Check .xmemignore for files
      if (ignoreInstance && ignoreInstance.ignores(relativePath)) {
        ignoredCount++;
        continue;
      }
      if (seen.has(full)) continue;
      // Size check — skip files > 512KB inline (they can still come via /attachments)
      try {
        const stat = fs.statSync(full);
        if (stat.size > 512 * 1024) continue;
      } catch { continue; }
      seen.add(full);
      results.push(full);
    }
  }

  const safeRoot = resolveSafeScanRoot(rootPath);
  if (!safeRoot) return results; // guard tripped — skip scan entirely
  walk(safeRoot, 0);
  if (ignoreInstance && ignoredCount > 0) {
    console.log(`[memtap] .xmemignore: ${ignoredCount} paths excluded`);
  }
  return results;
}

/** @deprecated use collectWorkspaceFiles. Kept for back-compat. */
function collectWorkspaceMdFiles(rootPath: string, maxDepth = 2): string[] {
  return collectWorkspaceFiles(rootPath, maxDepth).filter(f => f.toLowerCase().endsWith('.md'));
}

/** [v7.3] Detect mime type from filename (very rough, enough for xmem server). */
function detectMimeType(filename: string): string {
  const lower = filename.toLowerCase();
  const base = path.basename(lower);
  if (base === 'dockerfile') return 'text/x-dockerfile';
  if (base === 'makefile') return 'text/x-makefile';
  if (lower.endsWith('.md') || lower.endsWith('.markdown')) return 'text/markdown';
  if (lower.endsWith('.json') || lower.endsWith('.jsonc') || lower.endsWith('.json5')) return 'application/json';
  if (lower.endsWith('.yaml') || lower.endsWith('.yml')) return 'application/yaml';
  if (lower.endsWith('.toml')) return 'application/toml';
  if (lower.endsWith('.html') || lower.endsWith('.htm')) return 'text/html';
  if (lower.endsWith('.xml') || lower.endsWith('.svg')) return 'application/xml';
  if (lower.endsWith('.csv')) return 'text/csv';
  if (lower.endsWith('.tsv')) return 'text/tab-separated-values';
  if (lower.endsWith('.ts') || lower.endsWith('.tsx')) return 'text/typescript';
  if (lower.endsWith('.js') || lower.endsWith('.jsx') || lower.endsWith('.mjs') || lower.endsWith('.cjs')) return 'text/javascript';
  if (lower.endsWith('.py')) return 'text/x-python';
  if (lower.endsWith('.go')) return 'text/x-go';
  if (lower.endsWith('.rs')) return 'text/x-rust';
  if (lower.endsWith('.java')) return 'text/x-java';
  if (lower.endsWith('.rb')) return 'text/x-ruby';
  if (lower.endsWith('.php')) return 'text/x-php';
  if (lower.endsWith('.sh') || lower.endsWith('.bash') || lower.endsWith('.zsh')) return 'text/x-shellscript';
  if (lower.endsWith('.sql')) return 'application/sql';
  if (lower.endsWith('.css')) return 'text/css';
  if (lower.endsWith('.scss') || lower.endsWith('.sass') || lower.endsWith('.less')) return 'text/css';
  if (lower.endsWith('.tf') || lower.endsWith('.tfvars') || lower.endsWith('.hcl')) return 'text/x-hcl';
  if (lower.endsWith('.graphql')) return 'application/graphql';
  if (lower.endsWith('.proto')) return 'text/x-protobuf';
  return 'text/plain';
}

/** Classify a file so captures get useful tags + channel labels. */
function classifyFile(filename: string): { kind: string; tags: string[] } {
  const lower = path.basename(filename).toLowerCase();
  const ext = path.extname(lower);
  if (lower.endsWith('.md') || lower.endsWith('.markdown') || lower === 'readme' || lower === 'readme.md' || lower.startsWith('readme.')) {
    return { kind: 'doc', tags: ['doc', 'markdown'] };
  }
  if (['.json', '.jsonc', '.json5', '.yaml', '.yml', '.toml', '.ini', '.conf', '.hcl', '.tf', '.tfvars'].includes(ext) ||
      ['openclaw.json', 'package.json', 'tsconfig.json', 'pyproject.toml', 'cargo.toml', 'go.mod', 'go.sum', 'requirements.txt', 'gemfile', 'pipfile'].includes(lower)) {
    return { kind: 'config', tags: ['config'] };
  }
  if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.rb', '.php', '.cs', '.c', '.h', '.cpp', '.hpp', '.sh', '.bash', '.zsh', '.fish', '.sql', '.graphql', '.proto', '.vue', '.svelte', '.astro'].includes(ext)) {
    return { kind: 'code', tags: ['code'] };
  }
  if (['.html', '.htm', '.xml', '.svg', '.css', '.scss', '.sass', '.less'].includes(ext)) {
    return { kind: 'web', tags: ['web'] };
  }
  if (['.csv', '.tsv'].includes(ext)) {
    return { kind: 'data', tags: ['data'] };
  }
  if (lower === 'dockerfile' || lower === 'makefile' || lower === 'rakefile' || lower === 'gemfile' || lower === 'procfile') {
    return { kind: 'infra', tags: ['infra'] };
  }
  if (ext === '.dockerfile' || lower.endsWith('.env.example')) {
    return { kind: 'infra', tags: ['infra'] };
  }
  return { kind: 'doc', tags: ['text'] };
}

/**
 * [v7.3] Deep workspace capture — all relevant files, not just .md.
 * [v7.4] Added persistent SHA256 cache + .xmemignore support.
 * Drop-in replacement for captureWorkspaceMdFiles (kept as alias below).
 */
async function captureWorkspaceFiles(cfg: MemTapConfig, agent: string, workspacePath?: string, options?: { force?: boolean }): Promise<{ captured: number; skipped: number; errors: number; cacheHits: number }> {
  const cwd = workspacePath || process.cwd();
  const force = options?.force ?? false;
  const stats = { captured: 0, skipped: 0, errors: 0, cacheHits: 0 };
  try {
    // Load .xmemignore
    const ig = loadIgnoreInstance(cwd, cfg.debug);

    // Load persistent hash cache
    const cache = loadHashCache(cwd);
    const serverUrl = cfg.serverUrl || 'https://api.xmem.space';
    const agentCache = getAgentCache(cache, agent, serverUrl);

    const files = collectWorkspaceFiles(cwd, 4, ig);
    if (cfg.debug) console.log(`[memtap] deep-scan: found ${files.length} files in ${cwd}`);

    // Log cache stats
    const cachedFileCount = Object.keys(agentCache.files).length;
    if (cachedFileCount > 0 || cfg.debug) {
      console.log(`[memtap] cache loaded: ${cachedFileCount} files cached${force ? ' (force=true, bypassing)' : ''}`);
    }

    let newCount = 0, changedCount = 0, unchangedCount = 0;

    for (const fullPath of files) {
      try {
        const relativeName = path.relative(cwd, fullPath) || path.basename(fullPath);

        // Stat the file for mtime + size
        let fileStat: fs.Stats;
        try {
          fileStat = fs.statSync(fullPath);
        } catch { stats.errors++; continue; }

        const content = fs.readFileSync(fullPath, 'utf-8');
        if (content.length < 20) { stats.skipped++; continue; }
        if (containsSecrets(content)) {
          if (cfg.debug) console.warn(`[memtap] skip ${fullPath} — secret-like content`);
          stats.skipped++;
          continue;
        }

        // SHA256 hash for cache comparison
        const hash = sha256Hash(content);
        const cached = agentCache.files[relativeName];

        // Cache hit: skip if hash matches and not forced
        if (!force && cached && cached.sha256 === hash && cached.mtimeMs === fileStat.mtimeMs) {
          stats.cacheHits++;
          unchangedCount++;
          continue;
        }

        // Determine if new or changed
        if (cached) { changedCount++; } else { newCount++; }

        const classification = classifyFile(relativeName);
        const mimeType = detectMimeType(relativeName);
        const sentEndpoints: string[] = [];

        // 1) /v1/artifacts — chunked, embedded, RAG-searchable
        try {
          await bbFetch(cfg, `${baseUrl(cfg)}/artifacts`, {
            method: 'POST',
            body: JSON.stringify({
              agent,
              path: fullPath,
              content,
              type: classification.kind,
              tool: 'plugin:deep-scan',
            }),
          });
          sentEndpoints.push('artifact');
        } catch (e: any) {
          if (cfg.debug) console.warn(`[memtap] artifact failed ${relativeName}: ${e.message}`);
        }

        // 2) /v1/attachments — raw blob (SHA-256 dedup, 409 on unchanged)
        try {
          const buf = Buffer.from(content, 'utf-8');
          if (buf.length <= 5 * 1024 * 1024) {
            await bbFetch(cfg, `${baseUrl(cfg)}/attachments`, {
              method: 'POST',
              body: JSON.stringify({
                agent,
                filename: relativeName,
                mimetype: mimeType,
                data_base64: buf.toString('base64'),
                description: `Workspace ${classification.kind}: ${relativeName}`,
                tags: ['workspace', 'auto-capture', ...classification.tags],
              }),
            });
            sentEndpoints.push('attachment');
          }
        } catch (e: any) {
          const msg = String(e?.message || '');
          if (cfg.debug && !msg.includes('409') && !msg.toLowerCase().includes('duplicate')) {
            console.warn(`[memtap] attachment failed ${relativeName}: ${msg}`);
          }
        }

        // 3) /v1/capture — LLM-extract semantic memories into graph.
        //    Only for doc/config/infra (code is captured via artifacts+attachments;
        //    LLM extraction over code is too noisy and burns tokens).
        if (classification.kind === 'doc' || classification.kind === 'config' || classification.kind === 'infra') {
          try {
            await bbFetch(cfg, `${baseUrl(cfg)}/capture`, {
              method: 'POST',
              body: JSON.stringify({
                agent,
                conversation: {
                  assistantMessage: `Workspace ${classification.kind} "${relativeName}":\n\n${content}`,
                  context: {
                    channel: `workspace-${classification.kind}`,
                    timestamp: new Date().toISOString(),
                    conversationId: `workspace:${relativeName}`,
                    filename: relativeName,
                  },
                },
                options: { maxMemories: 15 },
              }),
            });
            sentEndpoints.push('capture');
          } catch { /* non-fatal */ }
        }

        // Update cache entry on successful send
        agentCache.files[relativeName] = {
          sha256: hash,
          sizeBytes: fileStat.size,
          mtimeMs: fileStat.mtimeMs,
          lastSentAt: new Date().toISOString(),
          endpoints: sentEndpoints,
        };

        stats.captured++;
      } catch (e: any) {
        stats.errors++;
        if (cfg.debug) console.warn(`[memtap] deep-scan read failed for ${fullPath}: ${e?.message || e}`);
      }
    }

    // Persist cache
    agentCache.lastScan = new Date().toISOString();
    saveHashCache(cwd, cache);

    if (cfg.debug || stats.cacheHits > 0) {
      console.log(`[memtap] deep-scan complete: ${stats.captured} captured, ${stats.cacheHits} cache-hits (${newCount} new, ${changedCount} changed, ${unchangedCount} unchanged), ${stats.skipped} skipped, ${stats.errors} errors`);
    }
  } catch { /* cwd not readable */ }
  return stats;
}

/** @deprecated v7.3 — use captureWorkspaceFiles. Keeps same call signature returning count. */
async function captureWorkspaceMdFiles(cfg: MemTapConfig, agent: string, workspacePath?: string): Promise<number> {
  const r = await captureWorkspaceFiles(cfg, agent, workspacePath);
  return r.captured;
}

// ── Memory types ─────────────────────────────────────────────────────────────

const MEMORY_TYPES = ['fact', 'preference', 'decision', 'identity', 'event', 'observation', 'goal', 'task', 'consolidated', 'outcome', 'inferred'] as const;

const MEMORY_CATEGORIES = ['personal', 'professional', 'technical', 'project', 'health', 'preferences'] as const;

// ── Memory Intent Detection Patterns ─────────────────────────────────────────

interface MemoryIntentPattern {
  regex: RegExp;
  type: 'explicit' | 'preference' | 'fact' | 'instruction';
  description: string;
}

const DEFAULT_MEMORY_PATTERNS: MemoryIntentPattern[] = [
  // Explicit memory requests (German)
  { regex: /\b(merk\s*(?:dir|dir\s+das)?|merke\s+(?:dir|das)?|schreib(?:\s+das)?\s+auf|notier(?:e)?\s*(?:dir|das)?|vergiss\s+(?:nicht|das\s+nicht)|schreibs?\s+in\s+den\s+vault)\b/i, type: 'explicit', description: 'German explicit memory requests' },

  // Explicit memory requests (English)
  { regex: /\b(remember\s+(?:this|that)|note\s+(?:this|that|down)|keep\s+in\s+mind|don\'?t\s+forget|save\s+(?:this|that)|store\s+(?:this|that))\b/i, type: 'explicit', description: 'English explicit memory requests' },

  // Preferences (German)
  { regex: /\b(ich\s+(?:mag|bevorzuge|hasse|liebe)|mein(?:e)?\s+lieblings|ich\s+stehe\s+auf|ich\s+kann\s+nicht\s+leiden)\b/i, type: 'preference', description: 'German preference expressions' },

  // Preferences (English)
  { regex: /\b(i\s+(?:like|prefer|hate|love|enjoy|dislike)|my\s+favorite|i\'m\s+(?:into|fond\s+of)|i\s+can\'?t\s+stand)\b/i, type: 'preference', description: 'English preference expressions' },

  // Facts (German)
  { regex: /\b(mein(?:e)?\s+(?:adresse|name|telefon|email|geburt)|ich\s+(?:arbeite\s+bei|wohne\s+in|bin\s+geboren|komme\s+aus|heisse))\b/i, type: 'fact', description: 'German personal facts' },

  // Facts (English)
  { regex: /\b(my\s+(?:address|name|phone|email|birthday|birth)|i\s+(?:work\s+at|live\s+in|was\s+born|come\s+from|am\s+from))\b/i, type: 'fact', description: 'English personal facts' },

  // Instructions (German)
  { regex: /\b(ab\s+jetzt\s+(?:immer|stets|soll)|bitte\s+(?:immer|stets)|von\s+nun\s+an|in\s+zukunft\s+(?:immer|bitte))\b/i, type: 'instruction', description: 'German persistent instructions' },

  // Instructions (English)
  { regex: /\b(from\s+now\s+on|always\s+(?:do|remember\s+to)|going\s+forward|in\s+the\s+future)\b/i, type: 'instruction', description: 'English persistent instructions' }
];

// Runtime pattern management
let customMemoryPatterns: MemoryIntentPattern[] = [];
let memoryIntentStats = new Map<string, { count: number; lastTriggered: number }>();

// ── System Noise Filter (v6.1) ──────────────────────────────────────────────
// These messages are NOT real conversation and must be filtered out before
// they reach the capture buffer. Cron-job announcements, heartbeats, status
// pings, and scheduler artefacts must never become memories.

const SYSTEM_NOISE_PATTERNS: RegExp[] = [
  // Cron / scheduled task firings
  /^Cron\s+(?:Job|Task)\s/i,
  /^Scheduled\s+(?:job|task)\s/i,
  /\bcron\s+(?:gestartet|started|triggered|fired)\b/i,
  /\b(?:gestartet|started)\s+um\s+\d{2}:\d{2}\s*UTC/i,    // "... gestartet um 10:02 UTC"
  /^FR\s+TikTok\s+Warm/i,                                  // legacy psiClips cron name
  /^TikTok\s+Daily\s+Upload/i,
  // Heartbeat / system poll noise
  /^HEARTBEAT(_OK)?$/,
  /^heartbeat\s*(check|poll|tick)\b/i,
  // OpenClaw runtime artefacts
  /^Runtime[- ]generated\s+completion\s+event/i,
  /^System\s+\(untrusted\):\s+\[/,                          // injected system meta
  // Onboarding / status markers (machine-generated, not memory-worthy)
  /^Onboarded\s+on\s+\d{4}-\d{2}-\d{2}T/i,
  /^Auto-onboarding\s+(complete|failed)/i,
  /captured\s+\d+\s+workspace\s+\.md\s+files/i,
];

function isSystemNoise(content: string, ctx?: any): boolean {
  // Channel-based: scheduler/cron events have a specific channel
  const channel = ctx?.channelId || ctx?.channel || '';
  if (typeof channel === 'string' && /^(?:scheduler|cron|system|heartbeat)/i.test(channel)) {
    return true;
  }
  // From-field: many scheduler events come from a 'cron' or 'system' sender
  const from = ctx?.from || ctx?.senderId || '';
  if (typeof from === 'string' && /^(?:cron|scheduler|system)/i.test(from)) {
    return true;
  }
  // Pattern-based content match
  const trimmed = content.trim();
  return SYSTEM_NOISE_PATTERNS.some(re => re.test(trimmed));
}

function detectMemoryIntent(message: string): {
  hasIntent: boolean;
  intentType?: 'explicit' | 'preference' | 'fact' | 'instruction';
  matchedPattern?: string;
} {
  const allPatterns = [...DEFAULT_MEMORY_PATTERNS, ...customMemoryPatterns];

  for (const pattern of allPatterns) {
    if (pattern.regex.test(message)) {
      // Update stats
      const key = pattern.description;
      const stats = memoryIntentStats.get(key) || { count: 0, lastTriggered: 0 };
      stats.count++;
      stats.lastTriggered = Date.now();
      memoryIntentStats.set(key, stats);

      return {
        hasIntent: true,
        intentType: pattern.type,
        matchedPattern: pattern.description
      };
    }
  }

  return { hasIntent: false };
}

// ── Neuromimetic Memory System (v2.1 "The Neuron") ──────────────────────────

interface RecallLevel {
  intensity: number; // 0 = no recall, 1 = light, 2 = standard, 3 = deep
  topics: string[];
  confidence: number; // 0-1, how sure we are about this classification
  reasoning: string;
}

interface ConversationContext {
  recentTopics: string[];
  memoryQueryCount: number;
  lastMemoryAccess?: number;
  dominantTopic?: string;
  userEngagement: 'low' | 'medium' | 'high';
  attentionLevel: 'focused' | 'distracted' | 'flow'; // Determines encoding strength
  emotionalContext: 'positive' | 'neutral' | 'negative' | 'excited';
}

interface UserProfile {
  recallSensitivity: 'low' | 'medium' | 'high';
  preferredMemoryTypes: string[];
  averageQueryComplexity: number;
  lastActive: number;
  totalQueries: number;
  successfulRecalls: number;
  sleepCycles: number; // For consolidation tracking
  attentionPatterns: Array<{ timestamp: number; level: string }>;
}

interface WorkingMemory {
  currentFocus: string[];           // 5-7 most important current topics
  activeMemories: any[];            // Pre-loaded memories for instant access
  attentionSpotlight: string;       // Primary focus topic
  cognitiveLoad: number;            // 0-1, affects new memory encoding
  lastUpdate: number;
}

interface EpisodicMemory {
  event: string;
  timestamp: number;
  location: 'telegram' | 'discord' | 'local' | 'unknown';
  participants: string[];
  emotionalIntensity: number;       // 0-1, affects retention
  contextualCues: string[];         // For context-dependent retrieval
  consolidationScore: number;       // How well consolidated this memory is
}

interface MemoryChunk {
  id: string;
  relatedMemories: string[];
  abstractConcept: string;
  strength: number;                 // How often accessed together
  lastActivation: number;
}

// Emotional weighting for different memory types
const EMOTIONAL_WEIGHTS = {
  'decision': 1.5,     // Decisions are crucial
  'problem': 1.3,      // Problems stick in memory  
  'success': 1.2,      // We remember wins
  'failure': 1.4,      // We really remember losses
  'event': 1.1,        // Events have mild boost
  'fact': 1.0,         // Baseline
  'preference': 0.9,   // Less emotionally salient
  'observation': 0.8,  // Often forgotten
  'routine': 0.7       // Quickly forgotten
};

// Forgetting curve parameters (Ebbinghaus + emotional modulation)
const FORGETTING_CURVE = {
  baseDecayRate: 0.01,     // Base daily decay
  emotionalProtection: 0.3, // How much emotion protects from decay
  retrievalStrengthening: 0.15, // Boost from each retrieval
  consolidationBonus: 0.1   // Bonus from sleep/consolidation
};

// Global state for neuromimetic features
const conversationState = new Map<string, ConversationContext>();
const userProfiles = new Map<string, UserProfile>();
const workingMemoryState = new Map<string, WorkingMemory>();
const episodicMemories = new Map<string, EpisodicMemory[]>();
const memoryChunks = new Map<string, MemoryChunk[]>();
const memoryCache = new Map<string, { data: any[]; timestamp: number; query: string; retrievalCount: number }>();

// Attention tracking for encoding decisions
let attentionHistory: Array<{ timestamp: number; agent: string; level: string; trigger: string }> = [];

// Proactive surfacing tracking (which proactive memories were injected per agent)
const proactiveSurfacedMemories = new Map<string, { memoryIds: string[]; timestamp: number }>();

// ── Neuromimetic Functions ──────────────────────────────────────────────────

function updateWorkingMemory(agentId: string, topics: string[], memories: any[] = []): WorkingMemory {
  const existing = workingMemoryState.get(agentId) || {
    currentFocus: [],
    activeMemories: [],
    attentionSpotlight: '',
    cognitiveLoad: 0,
    lastUpdate: 0
  };

  // Update focus (maintain 5-7 items max, like human working memory)
  existing.currentFocus = [...existing.currentFocus, ...topics]
    .slice(-7) // Keep only recent 7 items
    .filter((item, index, arr) => arr.indexOf(item) === index); // dedupe

  // Update active memories (pre-loaded for instant access)
  existing.activeMemories = memories.slice(0, 5); // Max 5 active

  // Determine attention spotlight (most frequent recent topic)
  const topicCounts = existing.currentFocus.reduce((acc, topic) => {
    acc[topic] = (acc[topic] || 0) + 1;
    return acc;
  }, {} as Record<string, number>);
  
  existing.attentionSpotlight = Object.entries(topicCounts)
    .sort(([,a], [,b]) => b - a)[0]?.[0] || '';

  // Calculate cognitive load (affects new encoding)
  existing.cognitiveLoad = Math.min(1, existing.currentFocus.length / 7);
  existing.lastUpdate = Date.now();

  workingMemoryState.set(agentId, existing);
  return existing;
}

function analyzeAttentionLevel(message: string, context?: ConversationContext): string {
  const msg = message.toLowerCase();
  
  // High attention triggers
  if (/\b(wichtig|urgent|critical|problem|fehler|error|entscheidung|decision)\b/i.test(msg)) {
    return 'focused';
  }
  
  // Flow state indicators (long, detailed messages)
  if (message.length > 200 && /\b(projekt|entwicklung|implementierung|strategie)\b/i.test(msg)) {
    return 'flow';
  }
  
  // Distracted indicators
  if (message.length < 20 || /\b(btw|übrigens|kurz|quick|mal eben)\b/i.test(msg)) {
    return 'distracted';
  }
  
  return 'focused'; // Default to focused
}

function calculateEmotionalContext(message: string): string {
  const msg = message.toLowerCase();
  
  // Positive indicators
  if (/\b(super|great|toll|perfekt|excellent|love|awesome|gut gelöst)\b/i.test(msg)) {
    return 'positive';
  }
  
  // Excited indicators  
  if (/[!]{2,}|🚀|🎉|💯|genial|krass|unglaublich/i.test(message)) {
    return 'excited';
  }
  
  // Negative indicators
  if (/\b(problem|fehler|bug|broken|schlecht|terrible|failed|shit)\b/i.test(msg)) {
    return 'negative';
  }
  
  return 'neutral';
}

function shouldEncodeMemory(content: string, attentionLevel: string, emotionalContext: string, cognitiveLoad: number): boolean {
  // Attention-gated encoding - like real brain filtering
  
  let encodeProbability = 0.5; // Base probability
  
  // Attention modulation
  if (attentionLevel === 'focused') encodeProbability += 0.3;
  else if (attentionLevel === 'flow') encodeProbability += 0.4;
  else if (attentionLevel === 'distracted') encodeProbability -= 0.3;
  
  // Emotional modulation (emotional events are better encoded)
  if (emotionalContext === 'excited') encodeProbability += 0.3;
  else if (emotionalContext === 'positive') encodeProbability += 0.1;
  else if (emotionalContext === 'negative') encodeProbability += 0.2; // We remember bad things
  
  // Cognitive load (harder to encode when overloaded)
  encodeProbability -= cognitiveLoad * 0.2;
  
  // Content length and importance
  if (content.length > 100) encodeProbability += 0.1;
  if (/\b(entscheidung|decision|wichtig|important)\b/i.test(content)) encodeProbability += 0.2;
  
  return Math.random() < Math.min(1, Math.max(0, encodeProbability));
}

function createEpisodicMemory(agentId: string, content: string, context: ConversationContext): EpisodicMemory {
  // Extract contextual information for episodic encoding
  const episodic: EpisodicMemory = {
    event: content,
    timestamp: Date.now(),
    location: 'telegram', // Could be extracted from context
    participants: [agentId], // Could include other participants
    emotionalIntensity: context.emotionalContext === 'excited' ? 0.8 :
                       context.emotionalContext === 'positive' ? 0.6 :
                       context.emotionalContext === 'negative' ? 0.7 : 0.4,
    contextualCues: context.recentTopics.slice(-3), // Last 3 topics as retrieval cues
    consolidationScore: 0.1 // Start low, increases with sleep cycles
  };
  
  return episodic;
}

function updateMemoryChunks(agentId: string, relatedMemories: any[]) {
  if (relatedMemories.length < 2) return; // Need at least 2 memories to chunk
  
  const chunks = memoryChunks.get(agentId) || [];
  const memoryIds = relatedMemories.map(m => m.id || m._key).filter(Boolean);
  
  if (memoryIds.length < 2) return;
  
  // Find existing chunk or create new one
  let chunk = chunks.find(c => 
    c.relatedMemories.some(id => memoryIds.includes(id))
  );
  
  if (chunk) {
    // Strengthen existing chunk
    chunk.relatedMemories = [...new Set([...chunk.relatedMemories, ...memoryIds])];
    chunk.strength += 0.1;
    chunk.lastActivation = Date.now();
  } else {
    // Create new chunk
    const abstractConcept = extractAbstractConcept(relatedMemories);
    chunk = {
      id: `chunk_${Date.now()}`,
      relatedMemories: memoryIds,
      abstractConcept,
      strength: 1.0,
      lastActivation: Date.now()
    };
    chunks.push(chunk);
  }
  
  memoryChunks.set(agentId, chunks);
}

function extractAbstractConcept(memories: any[]): string {
  // Simple concept extraction based on common words
  const allContent = memories.map(m => m.content || '').join(' ').toLowerCase();
  
  if (/memtap.*development|plugin.*code/i.test(allContent)) return 'xmem Development';
  if (/business.*model|pricing.*strategy/i.test(allContent)) return 'Business Strategy';
  if (/server.*deploy|infrastructure/i.test(allContent)) return 'Infrastructure';
  if (/problem.*solution|debug|fix/i.test(allContent)) return 'Problem Solving';
  
  return 'General Knowledge';
}

function analyzeConversationContext(agentId: string, message: string): ConversationContext {
  const existing = conversationState.get(agentId) || {
    recentTopics: [],
    memoryQueryCount: 0,
    userEngagement: 'medium',
    attentionLevel: 'focused',
    emotionalContext: 'neutral'
  };

  // Extract topics from current message
  const topics: string[] = [];
  const msg = message.toLowerCase();
  
  if (/memtap/i.test(msg)) topics.push('memtap');
  if (/business|pricing|model/i.test(msg)) topics.push('business');
  if (/server|deployment|vps|infrastructure/i.test(msg)) topics.push('infrastructure');
  if (/plugin|entwicklung|development|code/i.test(msg)) topics.push('development');
  if (/entscheidung|decision/i.test(msg)) topics.push('decisions');
  if (/problem|issue|fehler|bug|error/i.test(msg)) topics.push('problems');
  
  // Update recent topics (sliding window of 5)
  existing.recentTopics = [...existing.recentTopics, ...topics].slice(-5);
  
  // Analyze attention and emotional state
  existing.attentionLevel = analyzeAttentionLevel(message, existing) as any;
  existing.emotionalContext = calculateEmotionalContext(message) as any;
  
  // Track attention patterns for user profile
  attentionHistory.push({
    timestamp: Date.now(),
    agent: agentId,
    level: existing.attentionLevel,
    trigger: message.substring(0, 50)
  });
  
  // Limit attention history size
  if (attentionHistory.length > 1000) {
    attentionHistory = attentionHistory.slice(-500);
  }
  
  // Count memory-related queries
  if (isMemoryQuery(message)) {
    existing.memoryQueryCount++;
    existing.lastMemoryAccess = Date.now();
  }
  
  // Determine dominant topic
  const topicCounts = existing.recentTopics.reduce((acc, topic) => {
    acc[topic] = (acc[topic] || 0) + 1;
    return acc;
  }, {} as Record<string, number>);
  
  existing.dominantTopic = Object.entries(topicCounts)
    .sort(([,a], [,b]) => b - a)[0]?.[0];
  
  // Calculate engagement based on frequency, complexity, and attention
  const queryFreq = existing.memoryQueryCount;
  const attentionBoost = existing.attentionLevel === 'flow' ? 1 : 
                        existing.attentionLevel === 'focused' ? 0.5 : 0;
  const adjustedFreq = queryFreq + attentionBoost;
  
  existing.userEngagement = adjustedFreq > 3 ? 'high' : adjustedFreq > 1 ? 'medium' : 'low';
  
  conversationState.set(agentId, existing);
  return existing;
}

function isMemoryQuery(message: string): boolean {
  const memoryKeywords = [
    /\b(erinnerst|remember|recall|was war|what was|früher|previously)\b/i,
    /\b(entscheidung|decision|status|projekt|project)\b/i,
    /\b(wie haben wir|how did we|wo stehen|where are)\b/i
  ];
  return memoryKeywords.some(kw => kw.test(message));
}

function getUserProfile(agentId: string): UserProfile {
  return userProfiles.get(agentId) || {
    recallSensitivity: 'medium',
    preferredMemoryTypes: ['fact', 'decision', 'event'],
    averageQueryComplexity: 2,
    lastActive: Date.now(),
    totalQueries: 0,
    successfulRecalls: 0,
    sleepCycles: 0,
    attentionPatterns: []
  };
}

function updateUserProfile(agentId: string, queryComplexity: number, successful: boolean) {
  const profile = getUserProfile(agentId);
  profile.totalQueries++;
  profile.lastActive = Date.now();
  profile.averageQueryComplexity = (profile.averageQueryComplexity + queryComplexity) / 2;
  if (successful) profile.successfulRecalls++;
  userProfiles.set(agentId, profile);
}

function predictiveTopicBoost(context: ConversationContext, baseTopics: string[]): string[] {
  const enhanced = [...baseTopics];
  
  // If we're in a topic-focused conversation, boost related topics
  if (context.dominantTopic === 'memtap' && context.recentTopics.length > 2) {
    enhanced.push('development', 'business', 'infrastructure');
  }
  
  if (context.dominantTopic === 'problems' && context.recentTopics.includes('memtap')) {
    enhanced.push('debugging', 'server issues', 'deployment');
  }
  
  return [...new Set(enhanced)]; // dedupe
}

function analyzeRecallLevel(message: string, agentId: string): RecallLevel {
  const msg = message.toLowerCase();
  const context = analyzeConversationContext(agentId, message);
  const userProfile = getUserProfile(agentId);
  
  // Base keyword analysis
  let intensity = 0;
  let confidence = 0.5;
  let reasoning = '';
  
  // Deep recall triggers (level 3)
  const deepTriggers = [
    /\b(project status|projektstand|wo stehen wir|complete overview)\b/i,
    /\b(alle entscheidungen|all decisions|full context|comprehensive)\b/i,
    /\b(memtap.*status|memtap.*fortschritt|memtap.*stand)\b/i,
    /\b(business.*model|geschäfts.*modell|pricing.*strategy)\b/i
  ];
  
  // Standard recall triggers (level 2)
  const standardTriggers = [
    /\b(erinnerst du|remember|recall)\b/i,
    /\b(entscheidung|decision|beschlossen|agreed)\b/i,
    /\b(was war|what was|wie haben wir|how did we)\b/i,
    /\b(letzte mal|last time|früher|previously)\b/i,
    /\b(wer ist|who is|wer war|who was)\b/i
  ];
  
  // Light recall triggers (level 1)
  const lightTriggers = [
    /\b(status|update|aktuell|current)\b/i,
    /\b(problem|issue|fehler|bug)\b/i,
    /\b(wie geht|how.*going|weiter|next)\b/i,
    /\b(info|information|details|erklärung)\b/i
  ];
  
  // Base classification
  if (deepTriggers.some(t => t.test(msg))) {
    intensity = 3;
    confidence = 0.9;
    reasoning = 'Deep trigger detected';
  } else if (standardTriggers.some(t => t.test(msg))) {
    intensity = 2;
    confidence = 0.8;
    reasoning = 'Standard memory query';
  } else if (lightTriggers.some(t => t.test(msg))) {
    intensity = 1;
    confidence = 0.6;
    reasoning = 'Light context hint';
  }
  
  // Conversation context adjustments
  if (context.dominantTopic === 'memtap' && context.recentTopics.length > 2) {
    intensity = Math.min(3, intensity + 1);
    confidence += 0.2;
    reasoning += ' + xmem conversation context';
  }
  
  if (context.memoryQueryCount > 2 && intensity > 0) {
    intensity = Math.min(3, intensity + 1);
    confidence += 0.1;
    reasoning += ' + frequent memory queries';
  }
  
  // User profile adjustments
  if (userProfile.recallSensitivity === 'high' && intensity > 0) {
    intensity = Math.min(3, intensity + 1);
    reasoning += ' + high user sensitivity';
  } else if (userProfile.recallSensitivity === 'low' && intensity > 0) {
    intensity = Math.max(1, intensity - 1);
    reasoning += ' + low user sensitivity';
  }
  
  // Recent successful recalls boost confidence
  if (userProfile.successfulRecalls > userProfile.totalQueries * 0.7) {
    confidence += 0.1;
  }
  
  // Topic extraction with predictive enhancement
  const baseTopics: string[] = [];
  if (/memtap/i.test(msg)) baseTopics.push('xmem');
  if (/business|pricing|model/i.test(msg)) baseTopics.push('business model');
  if (/server|deployment|vps/i.test(msg)) baseTopics.push('infrastructure');
  if (/plugin|entwicklung|development/i.test(msg)) baseTopics.push('development');
  if (/entscheidung|decision/i.test(msg)) baseTopics.push('decisions');
  
  const topics = predictiveTopicBoost(context, baseTopics);
  
  // Clamp confidence
  confidence = Math.min(1, Math.max(0, confidence));
  
  return { 
    intensity, 
    topics: topics.length ? topics : ['recent activity'], 
    confidence,
    reasoning: reasoning || 'no specific triggers'
  };
}

// ── LLM Memory Extraction ────────────────────────────────────────────────────

const EXTRACTION_PROMPT = `Du bist ein Memory-Extractor. Analysiere die folgende Assistenten-Nachricht und extrahiere Informationen die langfristig wissenswert sind.

Regeln:
- Nur NEUE Fakten, Entscheidungen, Präferenzen, Events extrahieren
- Keine trivialen Dinge ("ich hab gesucht", "hier ist das Ergebnis")
- Keine Wiederholungen von bereits bekanntem Wissen
- Technische Konfigurationen, Entscheidungen, Personen-Info = wichtig
- Smalltalk, Statusmeldungen, Zwischenschritte = unwichtig
- Wenn NICHTS wissenswert ist: leeres Array zurückgeben

Antwort als JSON-Array (NUR das Array, kein Markdown):
[
  {
    "content": "Kurze, prägnante Beschreibung des Fakts",
    "type": "fact|preference|decision|identity|event|observation|goal|task",
    "importance": 1-10,
    "tags": ["tag1", "tag2"],
    "category": "personal|professional|technical|project|health|preferences"
  }
]

Wenn nichts extrahiert werden soll: []`;

async function llmExtract(cfg: MemTapConfig, content: string): Promise<any[]> {
  const llmUrl = cfg.llmUrl || 'http://127.0.0.1:18789/v1/chat/completions';
  const model = cfg.llmModel || 'anthropic/claude-sonnet-4-20250514';

  // Timeout so a busy/blocked local gateway LLM can never hang the whole
  // agent run (capture hooks call this against 127.0.0.1:18789 — the agent's
  // own gateway; without a timeout a stuck tool-loop deadlocks until the
  // hard lane-timeout).
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 20_000);
  let res: Response;
  try {
    res = await fetch(llmUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      signal: controller.signal,
      body: JSON.stringify({
        model,
        max_tokens: 1000,
        messages: [
          { role: 'system', content: EXTRACTION_PROMPT },
          { role: 'user', content: `Assistenten-Nachricht:\n\n${content}` },
        ],
      }),
    });
  } finally {
    clearTimeout(timeout);
  }

  if (!res.ok) throw new Error(`LLM ${res.status}`);
  const data = await res.json();
  const text = data.choices?.[0]?.message?.content?.trim() || '[]';

  const cleaned = text.replace(/^```json?\n?/m, '').replace(/\n?```$/m, '').trim();

  try {
    const parsed = JSON.parse(cleaned);
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    return [];
  }
}

// ── Plugin Entry ─────────────────────────────────────────────────────────────

// The plugin-sdk entry module is only resolvable inside the host install
// context at runtime, not during standalone `tsc` compilation. Load it via
// require so the type checker does not try to resolve it; runtime behaviour
// (CommonJS require of the same path) is unchanged.
const { definePluginEntry } = require('openclaw/plugin-sdk/plugin-entry') as {
  definePluginEntry: (def: any) => any;
};

export default definePluginEntry({
  id: 'memtap',
  name: 'xmem',
  description: 'Graph-based long-term memory for AI agents',
  register(api: any) {
  const logger = api.logger ?? console;

  // ── Tool: memtap_scan ─────────────────────────────────────────────────────

  api.registerTool({
    name: 'memtap_scan',
    description: 'Workspace file scanner for automatic ingestion of workspace files (deep scan with SHA256 cache)',
    parameters: {
      type: 'object',
      additionalProperties: false,
      properties: {
        action: {
          type: 'string',
          enum: ['workspace', 'ingest', 'ingest-all', 'status', 'diff'],
          description: 'Action to perform: workspace (scan for .md files), ingest (single file), ingest-all (batch), status (tracking info), diff (hash comparison)',
        },
        force: {
          type: 'boolean',
          description: 'Force re-ingest all files, bypassing the SHA256 cache (default: false)',
        },
        filename: {
          type: 'string',
          description: 'Filename for ingest action',
        },
        content: {
          type: 'string',
          description: 'File content for ingest action',
        },
        files: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              filename: { type: 'string' },
              content: { type: 'string' },
            },
          },
          description: 'Array of files for ingest-all action',
        },
      },
      required: ['action'],
    },
    execute: async (params: any) => {
      try {
        const cfg = getConfig(api);
        const currentAgentId = agentId(cfg, api);
        const tracker = getWorkspaceFileTracker(currentAgentId);

        switch (params.action) {
          case 'workspace': {
            // [v7.4] Deep scan with SHA256 cache + .xmemignore support.
            const workspaceRoot = api?.context?.workspace || process.cwd();
            const force = params.force === true;
            const result = await captureWorkspaceFiles(cfg, currentAgentId, workspaceRoot, { force });
            return {
              content: [{
                type: 'text',
                text: `📁 Deep workspace scan complete (${workspaceRoot})${force ? ' [FORCE]' : ''}\n\n` +
                      `✅ Captured: ${result.captured}\n` +
                      `💾 Cache hits: ${result.cacheHits} (unchanged files skipped)\n` +
                      `⏭️  Skipped: ${result.skipped} (too small, binary, or secret-like)\n` +
                      `⚠️  Errors: ${result.errors}\n\n` +
                      `All capturable files (.md, .json, .yaml, .ts, .py, .go, Dockerfile, etc.) were sent to:\n` +
                      `  • /v1/artifacts (chunked RAG index)\n` +
                      `  • /v1/attachments (raw blob storage, SHA-256 dedup)\n` +
                      `  • /v1/capture (semantic graph extraction — docs only)\n\n` +
                      `Cache: .xmem-hashes.json | Ignore: .xmemignore`
              }],
            };
          }

          case 'ingest':
            if (!params.filename || !params.content) {
              return {
                content: [{ type: 'text', text: 'Error: filename and content are required for ingest action.' }],
                isError: true,
              };
            }

            const newHash = simpleHash(params.content);
            const existing = tracker.get(params.filename);

            // Skip if unchanged
            if (existing && existing.lastHash === newHash) {
              return {
                content: [{
                  type: 'text',
                  text: `📄 ${params.filename} - No changes detected (hash: ${newHash})`
                }],
              };
            }

            // Send to /capture
            try {
              await bbFetch(cfg, `${baseUrl(cfg)}/capture`, {
                method: 'POST',
                body: JSON.stringify({
                  content: params.content,
                  agent: currentAgentId,
                  metadata: {
                    source: 'workspace-file',
                    filename: params.filename,
                    fileHash: newHash,
                    priority: 'medium',
                  },
                }),
              });

              // Update tracker
              tracker.set(params.filename, {
                filename: params.filename,
                lastHash: newHash,
                lastSeen: Date.now(),
              });

              return {
                content: [{
                  type: 'text',
                  text: `✅ ${params.filename} ingested successfully (hash: ${newHash})`
                }],
              };
            } catch (err: any) {
              return {
                content: [{ type: 'text', text: `❌ Failed to ingest ${params.filename}: ${err.message}` }],
                isError: true,
              };
            }

          case 'ingest-all':
            if (!Array.isArray(params.files)) {
              return {
                content: [{ type: 'text', text: 'Error: files array is required for ingest-all action.' }],
                isError: true,
              };
            }

            const results: string[] = [];
            let ingestedCount = 0;
            let skippedCount = 0;

            for (const file of params.files) {
              if (!file.filename || !file.content) {
                results.push(`❌ Skipped invalid file entry (missing filename or content)`);
                continue;
              }

              const hash = simpleHash(file.content);
              const existingFile = tracker.get(file.filename);

              // Skip if unchanged
              if (existingFile && existingFile.lastHash === hash) {
                results.push(`📄 ${file.filename} - No changes`);
                skippedCount++;
                continue;
              }

              try {
                await bbFetch(cfg, `${baseUrl(cfg)}/capture`, {
                  method: 'POST',
                  body: JSON.stringify({
                    content: file.content,
                    agent: currentAgentId,
                    metadata: {
                      source: 'workspace-file',
                      filename: file.filename,
                      fileHash: hash,
                      priority: 'medium',
                    },
                  }),
                });

                tracker.set(file.filename, {
                  filename: file.filename,
                  lastHash: hash,
                  lastSeen: Date.now(),
                });

                results.push(`✅ ${file.filename} ingested (hash: ${hash})`);
                ingestedCount++;
              } catch (err: any) {
                results.push(`❌ ${file.filename} failed: ${err.message}`);
              }
            }

            return {
              content: [{
                type: 'text',
                text: `📦 Batch Ingestion Complete\n\n${results.join('\n')}\n\n📊 Summary: ${ingestedCount} ingested, ${skippedCount} skipped`
              }],
            };

          case 'status':
            if (tracker.size === 0) {
              return {
                content: [{
                  type: 'text',
                  text: '📁 No workspace files tracked yet. Use "workspace" action to see scanning instructions.'
                }],
              };
            }

            const statusLines: string[] = ['📁 Workspace File Tracking Status\n'];
            const sortedFiles = Array.from(tracker.values()).sort((a, b) => b.lastSeen - a.lastSeen);

            for (const file of sortedFiles) {
              const age = Math.floor((Date.now() - file.lastSeen) / (1000 * 60 * 60 * 24));
              const ageStr = age === 0 ? 'today' : `${age}d ago`;
              statusLines.push(`📄 ${file.filename} - last seen ${ageStr} (hash: ${file.lastHash})`);
            }

            return {
              content: [{
                type: 'text',
                text: statusLines.join('\n')
              }],
            };

          case 'diff':
            if (!params.filename || !params.content) {
              return {
                content: [{ type: 'text', text: 'Error: filename and content are required for diff action.' }],
                isError: true,
              };
            }

            const currentHash = simpleHash(params.content);
            const trackedFile = tracker.get(params.filename);

            if (!trackedFile) {
              return {
                content: [{
                  type: 'text',
                  text: `📄 ${params.filename} - NEW FILE (hash: ${currentHash})`
                }],
              };
            }

            const changed = trackedFile.lastHash !== currentHash;
            const age = Math.floor((Date.now() - trackedFile.lastSeen) / (1000 * 60 * 60 * 24));
            const ageStr = age === 0 ? 'today' : `${age}d ago`;

            return {
              content: [{
                type: 'text',
                text: `📄 ${params.filename} - ${changed ? 'CHANGED' : 'UNCHANGED'}\n` +
                      `Current hash: ${currentHash}\n` +
                      `Last hash: ${trackedFile.lastHash}\n` +
                      `Last seen: ${ageStr}`
              }],
            };

          default:
            return {
              content: [{ type: 'text', text: 'Error: Invalid action. Use workspace, ingest, ingest-all, status, or diff.' }],
              isError: true,
            };
        }
      } catch (err: any) {
        return { content: [{ type: 'text', text: `Workspace scanner error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_recall ──────────────────────────────────────────────────

  api.registerTool({
    name: 'memtap_recall',
    description:
      'Search the xmem knowledge graph for relevant memories. ' +
      'Returns semantically matched memories with entities and relationships. ' +
      'Use this before answering questions about prior decisions, people, projects, or preferences.',
    parameters: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Natural language search query' },
        types: {
          type: 'array',
          items: { type: 'string', enum: MEMORY_TYPES },
          description: 'Filter by memory types (optional)',
        },
        limit: { type: 'number', description: 'Max results (default 10)' },
      },
      required: ['query'],
    },
    async execute(_id: string, params: { query: string; types?: string[]; limit?: number }) {
      const cfg = getConfig(api);
      const url = new URL('/recall', baseUrl(cfg));
      url.searchParams.set('q', params.query);
      url.searchParams.set('agent', agentId(cfg, api));
      if (params.limit) url.searchParams.set('limit', String(params.limit));
      if (params.types?.length) url.searchParams.set('types', params.types.join(','));

      try {
        const data = await bbFetch(cfg, url.toString());
        const memories = data.results || data.memories || [];

        if (memories.length === 0) {
          return { content: [{ type: 'text', text: 'No matching memories found in xmem.' }] };
        }

        const formatted = memories.map((m: any, i: number) => {
          const entities = (m.entities || []).map((e: any) => e.name).join(', ');
          const meta = [`Type: ${m.type}`, `Importance: ${displayImportance(m.importance)}/10`];
          if (entities) meta.push(`Entities: ${entities}`);
          if (m.created) meta.push(`Created: ${m.created.split('T')[0]}`);
          return `${i + 1}. [${m.id || m._key}] ${m.content}\n   ${meta.join(' | ')}`;
        }).join('\n\n');

        return { content: [{ type: 'text', text: `Found ${memories.length} memories:\n\n${formatted}` }] };
      } catch (err: any) {
        return { content: [{ type: 'text', text: `xmem recall error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_remember ────────────────────────────────────────────────

  api.registerTool({
    name: 'memtap_remember',
    description:
      'Store a memory in the xmem knowledge graph. ' +
      'Use for important facts, decisions, preferences, events, goals, or observations worth remembering long-term. ' +
      'Entities mentioned are automatically extracted and linked.',
    parameters: {
      type: 'object',
      properties: {
        content: { type: 'string', description: 'The memory content to store' },
        type: { type: 'string', enum: MEMORY_TYPES, description: 'Memory type (default: fact)' },
        importance: { type: 'number', description: 'Importance 1-10 (default: 5)' },
        tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags for categorization' },
        immutable: { type: 'boolean', description: 'Mark memory as immutable (cannot be auto-decayed or auto-archived)' },
      },
      required: ['content'],
    },
    async execute(_id: string, params: { content: string; type?: string; importance?: number; tags?: string[]; immutable?: boolean }) {
      const cfg = getConfig(api);
      const importance = params.importance ?? 5;
      const body: Record<string, any> = {
        content: params.content,
        type: params.type || 'fact',
        agent: agentId(cfg, api),
        importance: storeImportance(importance),
        tags: params.tags || [],
      };
      if (params.immutable) body.immutable = true;

      try {
        const data = await bbFetch(cfg, `${baseUrl(cfg)}/memories`, {
          method: 'POST',
          body: JSON.stringify(body),
        });

        const entities = (data.entities || []).map((e: any) => e.name).join(', ');
        let response = `Memory stored [${data.id || data._key}] (type: ${body.type}, importance: ${importance}/10)`;
        if (entities) response += `\n  Linked entities: ${entities}`;

        return { content: [{ type: 'text', text: response }] };
      } catch (err: any) {
        return { content: [{ type: 'text', text: `xmem store error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_analyze (consolidated analysis tools) ──────────────────

  api.registerTool({
    name: 'memtap_analyze',
    description: 'Analysis tools: bulletin, graphrag, infer, profile, export',
    parameters: {
      type: 'object',
      properties: {
        action: {
          type: 'string',
          enum: ['bulletin', 'graphrag', 'infer', 'profile', 'export'],
          description: 'Action: bulletin (context bulletin), graphrag (GraphRAG search), infer (implicit knowledge), profile (agent profile), export (graph export)',
        },
        // Bulletin parameters
        topics: { type: 'array', items: { type: 'string' }, description: 'Topics for bulletin' },
        limit: { type: 'number', description: 'Max results/memories per topic' },
        // GraphRAG parameters
        query: { type: 'string', description: 'Search query for graphrag/infer' },
        depth: { type: 'number', description: 'Graph traversal depth for graphrag (default 2, max 4)' },
        topK: { type: 'number', description: 'Number of seed results for graphrag (default 5)' },
        // Infer parameters
        maxInferences: { type: 'number', description: 'Max inferences for infer (default 3)' },
        // Export parameters
        format: { type: 'string', enum: ['json', 'graphml', 'markdown'], description: 'Export format' },
        includeDeleted: { type: 'boolean', description: 'Include deleted memories in export' },
      },
      required: ['action'],
    },
    async execute(_id: string, params: any) {
      const cfg = getConfig(api);

      try {
        switch (params.action) {
          case 'bulletin': {
            if (!params.topics) {
              return { content: [{ type: 'text', text: 'Topics required for bulletin action' }], isError: true };
            }

            const data = await bbFetch(cfg, `${baseUrl(cfg)}/bulletin`, {
              method: 'POST',
              body: JSON.stringify({
                topics: params.topics,
                agent: agentId(cfg, api),
                limit: params.limit || 3,
              }),
            });

            const sections = (data.sections || []).map((s: any) => {
              const items = (s.memories || []).map((m: any) =>
                `  - [${m.type}] ${m.summary} (importance: ${m.importance}/10)`
              ).join('\n');

              const graphItems = (s.graphContext || []).map((m: any) =>
                `  - [${m.type}] ${m.summary} (importance: ${Math.round(m.importance)}/10)\n    Found via: ${m.foundVia}`
              ).join('\n');

              const connItems = (s.connections || []).slice(0, 5).map((c: any) =>
                `  - ${c.fromSummary} \u2192 ${c.edgeType} \u2192 ${c.toSummary}`
              ).join('\n');

              let section = `**${s.topic}** (${s.memories?.length || 0} direct + ${s.graphContext?.length || 0} via graph)`;
              section += `\n${items || '  (no direct matches)'}`;
              if (graphItems) section += `\n\nRelated (via graph):\n${graphItems}`;
              if (connItems) section += `\n\nConnections:\n${connItems}`;
              return section;
            }).join('\n\n---\n\n');

            return { content: [{ type: 'text', text: sections || 'No relevant memories found.' }] };
          }

          case 'graphrag': {
            if (!params.query) {
              return { content: [{ type: 'text', text: 'Query required for graphrag action' }], isError: true };
            }

            const data = await bbFetch(cfg, `${baseUrl(cfg)}/graphrag/query`, {
              method: 'POST',
              body: JSON.stringify({
                query: params.query,
                agent: agentId(cfg, api),
                graphDepth: params.depth ?? 2,
                embeddingTopK: params.topK ?? 5,
              }),
            });

            const method = data.method || 'unknown';
            const seeds = data.seeds || [];
            const graphResults = data.results || data.graphResults || [];

            if (seeds.length === 0 && graphResults.length === 0) {
              return { content: [{ type: 'text', text: 'No results found via GraphRAG.' }] };
            }

            let output = `GraphRAG results (method: ${method}, ${seeds.length} seeds, ${graphResults.length} graph results):\n\n`;

            if (seeds.length > 0) {
              output += 'Direct matches:\n';
              output += seeds.map((s: any, i: number) =>
                `  ${i + 1}. [${s.type}] ${s.summary || s.content} (importance: ${Math.round(s.importance)}/10)`
              ).join('\n');
              output += '\n\n';
            }

            if (graphResults.length > 0) {
              output += 'Discovered via graph traversal:\n';
              output += graphResults.map((r: any, i: number) => {
                const imp = Math.round(r.importance);
                let line = `  ${i + 1}. [${r.type}] ${r.summary || r.content} (importance: ${imp}/10, ${r.hopDistance ?? r.depth ?? '?'}-hop)`;
                if (r.path && r.path.length > 0) {
                  const pathStr = r.path.map((p: any) => `${p.id} \u2192 ${p.edgeType}`).join(' \u2192 ');
                  line += `\n     Path: ${pathStr} \u2192 ${r.id}`;
                } else if (r.foundVia) {
                  line += `\n     Found via: ${r.foundVia}`;
                }
                return line;
              }).join('\n');
            }

            return { content: [{ type: 'text', text: output }] };
          }

          case 'infer': {
            if (!params.query) {
              return { content: [{ type: 'text', text: 'Query required for infer action' }], isError: true };
            }

            const data = await bbFetch(cfg, `${baseUrl(cfg)}/inference`, {
              method: 'POST',
              body: JSON.stringify({
                query: params.query,
                agent: agentId(cfg, api),
                maxInferences: params.maxInferences || 3,
              }),
            });

            if (!data.inferences || data.inferences.length === 0) {
              return { content: [{ type: 'text', text: 'No implicit knowledge inferred from the query.' }] };
            }

            const formatted = data.inferences.map((inf: any, i: number) => {
              let output = `${i + 1}. **${inf.claim}** (confidence: ${Math.round(inf.confidence * 100)}%)`;
              if (inf.reasoning) output += `\n   Reasoning: ${inf.reasoning}`;
              if (inf.sources && inf.sources.length > 0) {
                const srcList = inf.sources.slice(0, 3).map((s: any) => `[${s.type}] ${s.summary || s.content}`).join(', ');
                output += `\n   Sources: ${srcList}${inf.sources.length > 3 ? ` (+${inf.sources.length - 3} more)` : ''}`;
              }
              return output;
            }).join('\n\n');

            return { content: [{ type: 'text', text: `Inference results:\n\n${formatted}` }] };
          }

          case 'profile': {
            const data = await bbFetch(cfg, `${baseUrl(cfg)}/profile/${agentId(cfg, api)}`);

            const stats = data.stats || {};
            const entities = data.topEntities || [];
            const topics = data.topTopics || [];

            let output = `Agent Memory Profile:\n`;
            output += `Memory count: ${stats.totalMemories || 0}\n`;
            output += `Entity count: ${stats.totalEntities || 0}\n`;
            output += `Edge count: ${stats.totalEdges || 0}\n`;
            if (stats.avgImportance) output += `Avg importance: ${stats.avgImportance.toFixed(1)}/10\n`;

            if (entities.length > 0) {
              output += `\nTop entities:\n`;
              output += entities.slice(0, 10).map((e: any) =>
                `  - ${e.name} (${e.count} memories, importance: ${e.importance.toFixed(1)}/10)`
              ).join('\n');
            }

            if (topics.length > 0) {
              output += `\nTop topics:\n`;
              output += topics.slice(0, 10).map((t: any) =>
                `  - ${t.topic} (${t.count} memories)`
              ).join('\n');
            }

            return { content: [{ type: 'text', text: output }] };
          }

          case 'export': {
            const format = params.format || 'json';
            const url = new URL(`/export/${agentId(cfg, api)}`, baseUrl(cfg));
            url.searchParams.set('format', format);
            if (params.includeDeleted) url.searchParams.set('includeDeleted', 'true');

            const data = await bbFetch(cfg, url.toString());

            if (format === 'json') {
              return { content: [{ type: 'text', text: `Memory Graph Export (JSON):\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\`` }] };
            } else if (format === 'graphml') {
              return { content: [{ type: 'text', text: `Memory Graph Export (GraphML):\n\`\`\`xml\n${data.graphml}\n\`\`\`` }] };
            } else if (format === 'markdown') {
              return { content: [{ type: 'text', text: data.markdown || 'Export completed' }] };
            }

            return { content: [{ type: 'text', text: 'Export completed' }] };
          }

          default:
            return { content: [{ type: 'text', text: `Unknown action: ${params.action}` }], isError: true };
        }
      } catch (err: any) {
        if (err.message.includes('404') || err.message.includes('ECONNREFUSED')) {
          return { content: [{ type: 'text', text: `${params.action} not available. Check server configuration.` }] };
        }
        return { content: [{ type: 'text', text: `xmem ${params.action} error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_manage (consolidated management tools) ──────────────────

  api.registerTool({
    name: 'memtap_manage',
    description: 'Memory management: get|update|delete|list-entities|merge-entities|entity-memories|create-edge|graph-overview|graph-gaps|graph-clusters|graph-connections|graph-traverse|decay-report|contradictions|dedup-scan|resolve-contradictions|run-all|attach',
    parameters: {
      type: 'object',
      properties: {
        action: {
          type: 'string',
          enum: [
            'get', 'update', 'delete',
            'list-entities', 'merge-entities', 'entity-memories',
            'create-edge',
            'graph-overview', 'graph-gaps', 'graph-clusters', 'graph-connections', 'graph-traverse',
            'decay-report', 'contradictions', 'dedup-scan', 'resolve-contradictions', 'run-all',
            'attach'
          ],
          description: 'Management action to perform',
        },
        // Memory actions
        id: { type: 'string', description: 'Memory ID for get/update/delete' },
        content: { type: 'string', description: 'New content for update' },
        type: { type: 'string', enum: MEMORY_TYPES, description: 'Memory type for update' },
        importance: { type: 'number', description: 'Importance 1-10 for update' },
        tags: { type: 'array', items: { type: 'string' }, description: 'Tags for update' },
        // Entity actions
        entityId: { type: 'string', description: 'Entity ID for entity operations' },
        targetEntityId: { type: 'string', description: 'Target entity ID for merge' },
        // Edge actions
        fromId: { type: 'string', description: 'Source memory ID for create-edge' },
        toId: { type: 'string', description: 'Target memory ID for create-edge' },
        relationship: { type: 'string', description: 'Relationship type for create-edge' },
        // Graph actions
        from: { type: 'string', description: 'Source memory ID for connections' },
        to: { type: 'string', description: 'Target memory ID for connections' },
        start: { type: 'string', description: 'Start node ID for traverse' },
        depth: { type: 'number', description: 'Traversal depth' },
        // Attach actions
        filename: { type: 'string', description: 'Filename for attach' },
        fileContent: { type: 'string', description: 'File content for attach (base64 or text)' },
        mimeType: { type: 'string', description: 'MIME type for attach' },
        entityNames: { type: 'array', items: { type: 'string' }, description: 'Entity names to link for attach' },
      },
      required: ['action'],
    },
    async execute(_id: string, params: any) {
      const cfg = getConfig(api);
      const base = baseUrl(cfg);

      try {
        let data: any;

        switch (params.action) {
          // Memory management
          case 'get': {
            if (!params.id) {
              return { content: [{ type: 'text', text: 'Memory ID required for get action' }], isError: true };
            }
            data = await bbFetch(cfg, `${base}/memories/${params.id}`);
            const entities = (data.entities || []).map((e: any) => e.name).join(', ');
            let output = `Memory [${data.id || data._key}]: ${data.content}\n`;
            output += `Type: ${data.type}, Importance: ${displayImportance(data.importance)}/10`;
            if (entities) output += `\nEntities: ${entities}`;
            if (data.tags?.length) output += `\nTags: ${data.tags.join(', ')}`;
            if (data.created) output += `\nCreated: ${data.created.split('T')[0]}`;
            return { content: [{ type: 'text', text: output }] };
          }

          case 'update': {
            if (!params.id) {
              return { content: [{ type: 'text', text: 'Memory ID required for update action' }], isError: true };
            }
            const body: any = {};
            if (params.content) body.content = params.content;
            if (params.type) body.type = params.type;
            if (params.importance) body.importance = storeImportance(params.importance);
            if (params.tags) body.tags = params.tags;

            data = await bbFetch(cfg, `${base}/memories/${params.id}`, {
              method: 'PUT',
              body: JSON.stringify(body),
            });

            return { content: [{ type: 'text', text: `Memory [${params.id}] updated successfully` }] };
          }

          case 'delete': {
            if (!params.id) {
              return { content: [{ type: 'text', text: 'Memory ID required for delete action' }], isError: true };
            }

            await bbFetch(cfg, `${base}/memories/${params.id}`, { method: 'DELETE' });
            return { content: [{ type: 'text', text: `Memory [${params.id}] deleted successfully` }] };
          }

          // Entity management
          case 'list-entities': {
            data = await bbFetch(cfg, `${base}/entities/${agentId(cfg, api)}`);
            const entities = data.entities || data.results || [];
            if (entities.length === 0) {
              return { content: [{ type: 'text', text: 'No entities found.' }] };
            }

            const formatted = entities.slice(0, 50).map((e: any, i: number) => {
              const name = e.name || e._key;
              const memCount = e.memoryCount || e.memories || 0;
              const imp = e.importance ? Math.round(e.importance) : '?';
              return `${i + 1}. ${name} (${memCount} memories, importance: ${imp}/10)`;
            }).join('\n');

            return { content: [{ type: 'text', text: `Found ${entities.length} entities:\n\n${formatted}` }] };
          }

          case 'entity-memories': {
            if (!params.entityId) {
              return { content: [{ type: 'text', text: 'Entity ID required for entity-memories action' }], isError: true };
            }

            data = await bbFetch(cfg, `${base}/entities/${agentId(cfg, api)}/${params.entityId}`);
            const memories = data.memories || [];
            if (memories.length === 0) {
              return { content: [{ type: 'text', text: `No memories found for entity [${params.entityId}].` }] };
            }

            const formatted = memories.map((m: any, i: number) => {
              const imp = displayImportance(m.importance);
              return `${i + 1}. [${m.id}] ${m.content} (${m.type}, ${imp}/10)`;
            }).join('\n');

            return { content: [{ type: 'text', text: `Memories for entity [${params.entityId}]:\n\n${formatted}` }] };
          }

          case 'merge-entities': {
            if (!params.entityId || !params.targetEntityId) {
              return { content: [{ type: 'text', text: 'Both entityId and targetEntityId required for merge-entities' }], isError: true };
            }

            await bbFetch(cfg, `${base}/entities/${agentId(cfg, api)}/merge`, {
              method: 'POST',
              body: JSON.stringify({
                sourceEntity: params.entityId,
                targetEntity: params.targetEntityId,
              }),
            });

            return { content: [{ type: 'text', text: `Entity [${params.entityId}] merged into [${params.targetEntityId}]` }] };
          }

          // Edge management
          case 'create-edge': {
            if (!params.fromId || !params.toId || !params.relationship) {
              return { content: [{ type: 'text', text: 'fromId, toId, and relationship required for create-edge' }], isError: true };
            }

            await bbFetch(cfg, `${base}/edges`, {
              method: 'POST',
              body: JSON.stringify({
                from: params.fromId,
                to: params.toId,
                type: params.relationship,
                agent: agentId(cfg, api),
              }),
            });

            return { content: [{ type: 'text', text: `Edge created: [${params.fromId}] --${params.relationship}--> [${params.toId}]` }] };
          }

          // Graph analysis (moved from memtap_graph)
          case 'graph-overview': {
            data = await bbFetch(cfg, `${base}/graph/overview`);
            let output = `Graph Overview:\n`;
            output += `Memories: ${data.totalMemories || '?'}\n`;
            output += `Entities: ${data.totalEntities || '?'}\n`;
            output += `Edges: ${data.totalEdges || '?'}\n`;

            if (data.topEntities?.length) {
              output += `\nMost connected entities:\n`;
              output += data.topEntities.slice(0, 10).map((e: any, i: number) =>
                `  ${i + 1}. ${e.name} (${e.degree} connections)`
              ).join('\n');
            }

            if (data.edgeTypes?.length) {
              output += `\nEdge type distribution:\n`;
              output += data.edgeTypes.slice(0, 10).map((et: any) =>
                `  - ${et.type}: ${et.count} edges`
              ).join('\n');
            }

            return { content: [{ type: 'text', text: output }] };
          }

          case 'graph-gaps': {
            data = await bbFetch(cfg, `${base}/graph/gaps`);
            const orphans = data.orphans || [];
            const weaklyConnected = data.weaklyConnected || [];

            let output = `Graph Gaps Analysis:\n`;
            output += `Orphan memories: ${orphans.length}\n`;
            output += `Weakly connected: ${weaklyConnected.length}\n\n`;

            if (orphans.length > 0) {
              output += `Orphan memories (no connections):\n`;
              output += orphans.slice(0, 10).map((m: any, i: number) =>
                `  ${i + 1}. [${m.id}] ${m.summary || m.content} (${m.type})`
              ).join('\n');
            }

            if (weaklyConnected.length > 0) {
              output += `\nWeakly connected (<=1 connection):\n`;
              output += weaklyConnected.slice(0, 10).map((m: any, i: number) =>
                `  ${i + 1}. [${m.id}] ${m.summary || m.content} (${m.connections || 1} connections)`
              ).join('\n');
            }

            return { content: [{ type: 'text', text: output }] };
          }

          case 'graph-clusters': {
            data = await bbFetch(cfg, `${base}/graph/clusters`);
            const clusters = data.clusters || [];

            if (clusters.length === 0) {
              return { content: [{ type: 'text', text: 'No clusters detected in the graph.' }] };
            }

            let output = `Graph Clusters (${clusters.length} found):\n\n`;
            output += clusters.slice(0, 10).map((c: any, i: number) => {
              const entities = (c.entities || []).slice(0, 5).join(', ');
              const moreEntities = c.entities?.length > 5 ? ` (+${c.entities.length - 5} more)` : '';
              return `${i + 1}. Cluster ${c.id || i + 1} (${c.size || c.memories?.length || '?'} memories)\n   Topics: ${entities}${moreEntities}`;
            }).join('\n\n');

            return { content: [{ type: 'text', text: output }] };
          }

          case 'graph-connections': {
            if (!params.from || !params.to) {
              return { content: [{ type: 'text', text: 'Both from and to memory IDs required for graph-connections' }], isError: true };
            }

            data = await bbFetch(cfg, `${base}/graph/connections?from=${params.from}&to=${params.to}`);
            const paths = data.paths || [];

            if (paths.length === 0) {
              return { content: [{ type: 'text', text: `No paths found between [${params.from}] and [${params.to}]` }] };
            }

            let output = `Paths between [${params.from}] and [${params.to}] (${paths.length} found):\n\n`;
            output += paths.slice(0, 5).map((path: any, i: number) => {
              const steps = (path.steps || path.nodes || []).map((step: any) =>
                `[${step.id}] --${step.edgeType || 'connected'}--> `
              ).join('');
              return `${i + 1}. ${steps}[${params.to}] (${path.length || path.hops || '?'} hops)`;
            }).join('\n');

            return { content: [{ type: 'text', text: output }] };
          }

          case 'graph-traverse': {
            if (!params.start) {
              return { content: [{ type: 'text', text: 'Start memory ID required for graph-traverse' }], isError: true };
            }

            const traverseUrl = `${base}/graph/traverse?start=${params.start}${params.depth ? `&depth=${params.depth}` : ''}`;
            data = await bbFetch(cfg, traverseUrl);
            const nodes = data.nodes || data.memories || [];

            if (nodes.length === 0) {
              return { content: [{ type: 'text', text: `No nodes found starting from [${params.start}]` }] };
            }

            let output = `Graph traversal from [${params.start}] (${nodes.length} nodes found):\n\n`;
            output += nodes.map((node: any, i: number) => {
              const hop = node.distance || node.depth || node.hop || 0;
              return `${i + 1}. [${node.id}] ${node.summary || node.content} (${hop}-hop${node.via ? `, via: ${node.via}` : ''})`;
            }).join('\n');

            return { content: [{ type: 'text', text: output }] };
          }

          // Maintenance actions (moved from memtap_maintenance)
          case 'decay-report': {
            data = await bbFetch(cfg, `${base}/maintenance/decay-report`);
            const results = data.results || data.memories || [];
            if (results.length === 0) {
              return { content: [{ type: 'text', text: 'All memories above decay threshold.' }] };
            }
            let output = `Decay Report: ${results.length} memories decayed\n\n`;
            output += results.slice(0, 20).map((m: any, i: number) => {
              const eff = m.effectiveImportance ?? '?';
              const days = m.daysSinceAccess ?? '?';
              return `  ${i + 1}. [${m.id}] ${m.summary} (effective: ${eff}/10, ${days} days)`;
            }).join('\n');
            return { content: [{ type: 'text', text: output }] };
          }

          case 'contradictions': {
            data = await bbFetch(cfg, `${base}/maintenance/contradictions`);
            const results = data.results || data.contradictions || [];
            if (results.length === 0) {
              return { content: [{ type: 'text', text: 'No contradictions found.' }] };
            }
            let output = `Contradictions: ${results.length} pair(s) found\n\n`;
            output += results.map((c: any, i: number) => {
              const m1 = c.memory1 || c;
              const m2 = c.memory2 || c;
              return `  ${i + 1}. "${m1.summary || m1.from}" CONTRADICTS "${m2.summary || m2.to}"`;
            }).join('\n');
            return { content: [{ type: 'text', text: output }] };
          }

          case 'dedup-scan': {
            data = await bbFetch(cfg, `${base}/maintenance/dedup-scan`, { method: 'POST', body: '{}' });
            const dupes = data.duplicates || [];
            if (dupes.length === 0) {
              return { content: [{ type: 'text', text: 'No duplicates detected.' }] };
            }
            let output = `Dedup Scan: ${dupes.length} potential duplicate(s)\n\n`;
            output += dupes.slice(0, 20).map((d: any, i: number) => {
              const m1 = d.memory1 || d;
              const m2 = d.memory2 || d;
              const kind = d.type === 'update_chain' ? 'Update chain' : 'Similar content';
              return `  ${i + 1}. [${kind}] "${m1.summary || m1.older}" vs "${m2.summary || m2.newer}"`;
            }).join('\n');
            return { content: [{ type: 'text', text: output }] };
          }

          case 'resolve-contradictions': {
            data = await bbFetch(cfg, `${base}/maintenance/contradictions`);
            const contradictions = data.results || data.contradictions || [];
            if (contradictions.length === 0) {
              return { content: [{ type: 'text', text: 'No contradictions to resolve.' }] };
            }

            let resolved = 0;
            const resolutions: string[] = [];

            for (const c of contradictions.slice(0, 10)) {
              const m1 = c.memory1 || c;
              const m2 = c.memory2 || c;
              const m1Summary = m1.summary || m1.content || m1.from || '';
              const m2Summary = m2.summary || m2.content || m2.to || '';

              try {
                const llmUrl = cfg.llmUrl || 'http://127.0.0.1:18789/v1/chat/completions';
                const model = cfg.llmModel || 'anthropic/claude-sonnet-4-20250514';
                // Timeout: a busy/blocked local gateway LLM must never hang
                // the maintenance loop (same protection as bbFetch/llmExtract).
                const llmController = new AbortController();
                const llmTimeout = setTimeout(() => llmController.abort(), 20_000);
                let llmRes: Response;
                try {
                  llmRes = await fetch(llmUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    signal: llmController.signal,
                    body: JSON.stringify({
                      model,
                      max_tokens: 500,
                      messages: [
                        { role: 'system', content: 'You resolve contradictions between two memories. Decide which is more current/accurate. Respond with JSON: {"keep": 1 or 2, "reason": "short reason"}' },
                        { role: 'user', content: `Memory 1: ${m1Summary}\nMemory 2: ${m2Summary}` },
                      ],
                    }),
                  });
                } finally {
                  clearTimeout(llmTimeout);
                }
                if (!llmRes.ok) continue;
                const llmData = await llmRes.json();
                const text = llmData.choices?.[0]?.message?.content?.trim() || '';
                const cleaned = text.replace(/^```json?\n?/m, '').replace(/\n?```$/m, '').trim();
                const verdict = JSON.parse(cleaned);

                const keepId = verdict.keep === 1 ? (m1.id || m1._key) : (m2.id || m2._key);
                const archiveId = verdict.keep === 1 ? (m2.id || m2._key) : (m1.id || m1._key);

                if (keepId && archiveId) {
                  await bbFetch(cfg, `${base}/maintenance/resolve-contradiction`, {
                    method: 'POST',
                    body: JSON.stringify({ keep: keepId, archive: archiveId, reason: verdict.reason }),
                  });
                  resolved++;
                  resolutions.push(`Kept [${keepId}], archived [${archiveId}]: ${verdict.reason}`);
                }
              } catch { /* skip failures */ }
            }

            let output = `Resolved: ${resolved}/${contradictions.length}\n\n`;
            output += resolutions.map((r, i) => `  ${i + 1}. ${r}`).join('\n');
            return { content: [{ type: 'text', text: output }] };
          }

          case 'run-all': {
            data = await bbFetch(cfg, `${base}/maintenance/run-all`, { method: 'POST', body: '{}' });
            const decay = data.decay || {};
            const contras = data.contradictions || {};
            const dedup = data.duplicates || {};

            let output = 'Maintenance Report\n\n';
            const decayCount = decay.count ?? (decay.results || []).length;
            output += `Decay: ${decayCount} memories below threshold\n`;
            const contraCount = contras.count ?? (contras.results || []).length;
            output += `Contradictions: ${contraCount} pair(s)\n`;
            const dedupCount = dedup.count ?? (dedup.results || []).length;
            output += `Duplicates: ${dedupCount} potential duplicate(s)\n`;

            return { content: [{ type: 'text', text: output }] };
          }

          // Attachment management (moved from memtap_attach)
          case 'attach': {
            if (!params.filename || !params.fileContent) {
              return { content: [{ type: 'text', text: 'filename and fileContent required for attach action' }], isError: true };
            }

            const body: any = {
              filename: params.filename,
              content: params.fileContent,
              agent: agentId(cfg, api),
            };
            if (params.mimeType) body.mimeType = params.mimeType;
            if (params.entityNames?.length) body.entityNames = params.entityNames;

            data = await bbFetch(cfg, `${base}/attachments`, {
              method: 'POST',
              body: JSON.stringify(body),
            });

            const entities = (data.linkedEntities || []).join(', ');
            let response = `File attached [${data.id}]: ${params.filename}`;
            if (entities) response += `\nLinked to entities: ${entities}`;

            return { content: [{ type: 'text', text: response }] };
          }

          default:
            return { content: [{ type: 'text', text: `Unknown management action: ${params.action}` }], isError: true };
        }
      } catch (err: any) {
        return { content: [{ type: 'text', text: `xmem ${params.action} error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── OLD TOOLS REMOVED - Replaced by memtap_manage, memtap_analyze, memtap_track ───

  // ── Tool: memtap_graph ───────────────────────────────────────────────────

  // ── Tool: memtap_decide ──────────────────────────────────────────────────

  // ── Tool: memtap_memory ──────────────────────────────────────────────────

  // ── Tool: memtap_entities ─────────────────────────────────────────────────

  // ── Tool: memtap_edges ────────────────────────────────────────────────────

  // ── Tool: memtap_consolidate ──────────────────────────────────────────────

  // ── Tool: memtap_profile ──────────────────────────────────────────────────

  // ── Tool: memtap_export ───────────────────────────────────────────────────

  // ── Tool: memtap_attach (File/Attachment Support) ───────────────────────────

  // ── Tool: memtap_health (Enhanced Neural Monitoring) ────────────────────────

  api.registerTool({
    name: 'memtap_health',
    description:
      'Check xmem server health and get neural system statistics. Actions:\n' +
      '- health: server health check (version, counts)\n' +
      '- stats: detailed statistics (by type, by agent, entity/edge counts)\n' +
      '- neural: neuromimetic system status (working memory, attention, consolidation)\n' +
      '- performance: system performance metrics (cache hits, response times, memory usage)\n' +
      '- analytics: user behavior analytics (engagement, success rates, patterns)',
    parameters: {
      type: 'object',
      properties: {
        action: {
          type: 'string',
          enum: ['health', 'stats', 'neural', 'performance', 'analytics'],
          description: 'Which check to run (default: health)',
        },
        agent: { type: 'string', description: 'Specific agent for neural/performance analysis (optional)' },
        timeRange: { type: 'string', description: 'Time range for analytics (1h, 24h, 7d) default: 24h' }
      },
    },
    async execute(_id: string, params: { action?: string; agent?: string; timeRange?: string }) {
      const cfg = getConfig(api);
      const base = baseUrl(cfg);
      const action = params.action || 'health';

      try {
        let data: any;

        switch (action) {
          case 'health':
            data = await bbFetch(cfg, `${base}/health`);
            const counts = data.counts || {};
            return { content: [{ type: 'text', text: `xmem: ${data.status}\nServer: ${data.arango || data.version || 'unknown'}\nMemories: ${counts.memories ?? '?'} | Entities: ${counts.entities ?? '?'} | Edges: ${counts.edges ?? '?'}` }] };

          case 'stats':
            data = await bbFetch(cfg, `${base}/stats`);
            const byType = (data.byType || []).map((t: any) => `  ${t.type}: ${t.count}`).join('\n');
            const byAgent = (data.byAgent || []).map((a: any) => `  ${a.agent}: ${a.count}`).join('\n');
            return { content: [{ type: 'text', text: `xmem Stats:\nTotal: ${data.total ?? '?'} memories | ${data.entityCount ?? '?'} entities | ${data.edgeCount ?? '?'} edges\n\nBy type:\n${byType || '  (none)'}\n\nBy agent:\n${byAgent || '  (none)'}` }] };

          case 'neural':
            return { content: [{ type: 'text', text: generateNeuralReport(params.agent) }] };

          case 'performance':
            return { content: [{ type: 'text', text: generatePerformanceReport(params.agent) }] };

          case 'analytics':
            return { content: [{ type: 'text', text: generateAnalyticsReport(params.agent, params.timeRange || '24h') }] };

          default:
            return { content: [{ type: 'text', text: `Unknown health action: ${action}` }], isError: true };
        }
      } catch (err: any) {
        return { content: [{ type: 'text', text: `xmem health error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_find_in_code (Semantic Artifact Search) ────────────────
  api.registerTool({
    name: 'memtap_find_in_code',
    description:
      'Semantic search across files the agent has created or edited. Returns matching code/prose chunks ranked by relevance. Use for questions like "wo hab ich X gebaut", "welche Datei parsed Y", "haben wir schon Z implementiert".',
    parameters: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Natural-language query (e.g. "function that parses markdown tables")' },
        limit: { type: 'number', description: 'Max results (default 8, max 50)' },
        kind: { type: 'string', enum: ['code', 'markdown', 'text'], description: 'Restrict to file kind (optional)' },
        pathGlob: { type: 'string', description: 'Glob-like path filter (e.g. %frontend%), optional' },
      },
      required: ['query'],
    },
    async execute(_id: string, params: { query: string; limit?: number; kind?: string; pathGlob?: string }) {
      const cfg = getConfig(api);
      const base = baseUrl(cfg);
      try {
        const data = await bbFetch(cfg, `${base}/artifacts/search`, {
          method: 'POST',
          body: JSON.stringify({
            query: params.query,
            limit: Math.min(params.limit || 8, 50),
            kind: params.kind,
            pathGlob: params.pathGlob,
          }),
        });
        const results = (data.results || []) as Array<{ path: string; kind: string; chunkIndex: number; score: number; preview: string; updated: string }>;
        if (results.length === 0) {
          return { content: [{ type: 'text', text: `Keine Treffer für "${params.query}" (${data.searched || 0} chunks durchsucht).` }] };
        }
        const lines = results.map((r, i) => {
          const scorePct = Math.round(r.score * 100);
          return `${i + 1}. [${scorePct}%] ${r.path} (chunk ${r.chunkIndex}, ${r.kind})\n    ${r.preview.replace(/\n/g, '\n    ')}`;
        });
        return { content: [{ type: 'text', text: `🔍 "${params.query}" — ${results.length}/${data.searched} matches:\n\n${lines.join('\n\n')}` }] };
      } catch (err: any) {
        return { content: [{ type: 'text', text: `memtap_find_in_code error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_files (List / retrieve attached file blobs) ──────────

  api.registerTool({
    name: 'memtap_files',
    description:
      'List or retrieve raw file blobs stored in the xmem database (attachments). Unlike memtap_find_in_code (which searches chunk-embeddings), this returns the actual byte-for-byte content of files the agent has seen. Actions: list (show all stored files), get (retrieve one by id), delete (remove).',
    parameters: {
      type: 'object',
      properties: {
        action: {
          type: 'string',
          enum: ['list', 'get', 'delete'],
          description: 'What to do. Default: list.',
        },
        id: {
          type: 'string',
          description: 'Attachment id (required for get/delete)',
        },
        limit: { type: 'number', description: 'Max results for list (default 20, max 100)' },
        offset: { type: 'number', description: 'Pagination offset for list (default 0)' },
      },
    },
    async execute(_id: string, params: { action?: string; id?: string; limit?: number; offset?: number }) {
      const cfg = getConfig(api);
      const base = baseUrl(cfg);
      const action = params.action || 'list';
      try {
        if (action === 'list') {
          const limit = Math.min(params.limit || 20, 100);
          const offset = params.offset || 0;
          const currentAgent = agentId(cfg, api);
          const data = await bbFetch(
            cfg,
            `${base}/attachments?limit=${limit}&offset=${offset}&agent=${encodeURIComponent(currentAgent)}`,
          );
          const atts = (data.attachments || []) as Array<{
            id: string; filename: string; mimetype: string; size_bytes: number;
            description?: string; tags: string[]; created_at: string;
          }>;
          if (atts.length === 0) {
            return { content: [{ type: 'text', text: 'No stored files yet.' }] };
          }
          const lines = atts.map(a => {
            const size = a.size_bytes < 1024 ? `${a.size_bytes}B`
              : a.size_bytes < 1024 * 1024 ? `${(a.size_bytes / 1024).toFixed(1)}KB`
              : `${(a.size_bytes / 1024 / 1024).toFixed(1)}MB`;
            return `• ${a.filename} [${a.mimetype}, ${size}] id=${a.id}`;
          });
          return {
            content: [{
              type: 'text',
              text: `Stored files (${atts.length} of ${data.pagination?.returned || atts.length}):\n${lines.join('\n')}`,
            }],
          };
        }

        if (action === 'get') {
          if (!params.id) {
            return { content: [{ type: 'text', text: 'id is required for get' }], isError: true };
          }
          const data = await bbFetch(cfg, `${base}/attachments/${encodeURIComponent(params.id)}`);
          let content = '';
          if (data.data_base64) {
            try {
              content = Buffer.from(data.data_base64, 'base64').toString('utf-8');
            } catch {
              content = '(binary data — cannot render as text)';
            }
          }
          return {
            content: [{
              type: 'text',
              text: `File: ${data.filename}\nMime: ${data.mimetype}\nSize: ${data.size_bytes} bytes\nChecksum: ${data.checksum_sha256}\nCreated: ${data.created_at}\n\n--- content ---\n${content.slice(0, 4000)}${content.length > 4000 ? '\n\n... (truncated)' : ''}`,
            }],
          };
        }

        if (action === 'delete') {
          if (!params.id) {
            return { content: [{ type: 'text', text: 'id is required for delete' }], isError: true };
          }
          await bbFetch(cfg, `${base}/attachments/${encodeURIComponent(params.id)}`, { method: 'DELETE' });
          return { content: [{ type: 'text', text: `Deleted attachment ${params.id}` }] };
        }

        return { content: [{ type: 'text', text: `Unknown action: ${action}` }], isError: true };
      } catch (err: any) {
        return { content: [{ type: 'text', text: `memtap_files error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_monitor (Real-time Neural Activity) ─────────────────────

  // ── Tool: memtap_onboard ─────────────────────────────────────────────────

  // preMessage hook removed in v5.4.0 — replaced by before_prompt_build (Mem0-style)
  // See git history for the original Neuromimetic Tiered Memory Recall implementation


  // ── Hook: Server-Side Capture (message_completed) ──────────────────────────
  // v4.0.1: Extraction moved to server. Plugin posts user+assistant conversations to /capture.
  // We buffer user messages per conversation, then capture the pair when the assistant replies.

  const conversationBuffers = new Map<string, Array<{
    role: 'user' | 'assistant';
    content: string;
    timestamp: string;
    memoryIntent?: boolean;
    intentType?: 'explicit' | 'preference' | 'fact' | 'instruction';
    intentPattern?: string;
  }>>();

  const MAX_BUFFER_MESSAGES = 10; // 5 pairs
  const BUFFER_TTL_MS = 30 * 60 * 1000; // 30 minutes

  function getBuffer(conversationId: string): Array<{ role: 'user' | 'assistant'; content: string; timestamp: string; memoryIntent?: boolean; intentType?: 'explicit' | 'preference' | 'fact' | 'instruction'; intentPattern?: string }> {
    let buf = conversationBuffers.get(conversationId);
    if (!buf) {
      buf = [];
      conversationBuffers.set(conversationId, buf);
    }
    // Trim old messages
    const cutoff = Date.now() - BUFFER_TTL_MS;
    while (buf.length > 0 && new Date(buf[0].timestamp).getTime() < cutoff) {
      buf.shift();
    }
    // Trim to max size
    while (buf.length > MAX_BUFFER_MESSAGES) {
      buf.shift();
    }
    return buf;
  }

  // Capture user's inbound message (store for pairing with assistant reply)
  // Track onboarding status in-memory (checked once per gateway lifecycle)
  let onboardingChecked = false;

  api.registerHook(
    'message:received',
    async (event: any) => {
      console.log('[memtap] message:received hook FIRED, ctx keys:', Object.keys(event?.context || {}).join(','));
      const cfg = getConfig(api);

      // [v6.0] Onboarding moved to agent:bootstrap (single canonical location).
      // Previously ran in two places which caused duplicate captures on startup.

      const ctx = event?.context || {};
      const content = ctx.content || '';
      if (!content || typeof content !== 'string') return;
      if (content === 'NO_REPLY' || content === 'HEARTBEAT_OK') return;

      // [v6.1] Filter system noise that should never be captured.
      // These are scheduler/cron/heartbeat injections, not real conversation.
      if (isSystemNoise(content, ctx)) {
        if (cfg.debug) console.log('[xmem] skipping system-noise message:', content.substring(0, 60));
        return;
      }

      const conversationId = ctx.conversationId || ctx.from || 'default';
      const buf = getBuffer(conversationId);

      // Detect memory intent
      const intentResult = detectMemoryIntent(content);

      if (cfg.debug && intentResult.hasIntent) {
        console.log(`[memtap] Memory intent detected: ${intentResult.intentType} (${intentResult.matchedPattern})`);
      }

      // Create buffer entry with intent information
      const bufferEntry: any = {
        role: 'user' as const,
        content,
        timestamp: new Date().toISOString()
      };

      if (intentResult.hasIntent) {
        bufferEntry.memoryIntent = true;
        bufferEntry.intentType = intentResult.intentType;
        bufferEntry.intentPattern = intentResult.matchedPattern;
      }

      // Always buffer messages with detected intent, even if capture is disabled
      if (!cfg.captureEnabled && !cfg.autoCapture && !intentResult.hasIntent) return;

      buf.push(bufferEntry);
    },
    {
      name: 'memtap.capture-inbound',
      description: 'Buffer inbound user messages for capture pairing',
    }
  );

  // Capture assistant's outbound reply (pair with buffered user message, then POST to /capture)
  api.registerHook(
    'message:sent',
    async (event: any) => {
      console.log('[memtap] message:sent hook FIRED, ctx keys:', Object.keys(event?.context || {}).join(','));
      const cfg = getConfig(api);

      const ctx = event?.context || {};
      const assistantContent = ctx.content || '';
      console.log('[memtap] assistantContent length:', assistantContent.length, 'preview:', String(assistantContent).substring(0, 60));
      if (!assistantContent || typeof assistantContent !== 'string') return;
      if (assistantContent === 'NO_REPLY' || assistantContent === 'HEARTBEAT_OK') return;
      if (ctx.success === false) return;
      if (isSystemNoise(assistantContent, ctx)) {
        if (cfg.debug) console.log('[xmem] skipping system-noise assistant message');
        return;
      }

      const conversationId = ctx.conversationId || ctx.to || ctx.groupId || 'default';
      const buf = getBuffer(conversationId);
      buf.push({ role: 'assistant', content: assistantContent, timestamp: new Date().toISOString() });

      // Get last user message for backward compat and check for memory intent
      const lastUserMsg = [...buf].reverse().find(m => m.role === 'user');
      const hasMemoryIntent = lastUserMsg?.memoryIntent || false;

      // Skip length check if message has memory intent (explicit user request)
      if (!hasMemoryIntent) {
        if (!cfg.captureEnabled && !cfg.autoCapture) {
          console.log('[memtap] capture disabled, skipping');
          return;
        }
        if (assistantContent.length < (cfg.captureMinLength || 80)) return;
      }

      try {
        const capturePayload: any = {
          agent: agentId(cfg, api),
          conversation: {
            // Multi-turn (new)
            messages: buf.map(m => ({
              role: m.role,
              content: m.content,
              timestamp: m.timestamp,
              ...(m.memoryIntent && {
                memoryIntent: m.memoryIntent,
                intentType: m.intentType,
                intentPattern: m.intentPattern
              })
            })),
            // Backward compat (old)
            userMessage: lastUserMsg?.content,
            assistantMessage: assistantContent,
            context: {
              channel: ctx.channelId,
              conversationId,
              timestamp: new Date().toISOString(),
            },
          },
        };

        // Add memory intent metadata at top level if present
        if (hasMemoryIntent) {
          capturePayload.memoryIntent = true;
          capturePayload.intentType = lastUserMsg?.intentType;
          capturePayload.priority = 'high'; // Always capture memory intent messages
        }

        await bbFetch(cfg, `${baseUrl(cfg)}/capture`, {
          method: 'POST',
          body: JSON.stringify(capturePayload),
        });
        if (cfg.debug) console.log('[memtap] capture posted');
      } catch (err) {
        // Silent fail — capture is non-critical
        if (cfg.debug) console.error('[memtap] capture failed:', err);
      }
    },
    {
      name: 'memtap.server-capture',
      description: 'Server-side memory extraction via /v1/capture',
    }
  );

  // [v6.0] Removed: duplicate message:sent and session_end capture hooks.
  // The single message:sent hook above (memtap.server-capture) is the canonical
  // capture path. session_end flushing is no longer needed because every
  // message:sent already posts to /capture with the full multi-turn buffer.

  // ── [v7.0] LLM Call Deep-Capture (llm_input → llm_output pair) ───────────
  //
  // Every LLM call the agent makes flows through these hooks. We pair
  // input + output and forward high-value calls to the server as
  // `llm_interaction` captures so the graph learns what the agent is
  // reasoning about (not just its final chat replies).
  //
  // Sampling budget: max daily captures per UTC day (config-driven, lazy-read).
  const MIN_OUTPUT_LEN = 80;
  const llmInputBuffer = new Map<string, { prompt: string; timestamp: string; model?: string }>();
  let llmCaptureDailyCount = 0;
  let llmCaptureDayAnchor = new Date().toISOString().slice(0, 10);

  function getConversationIdFromCtx(ctx: any): string {
    return ctx?.conversationId || ctx?.sessionId || ctx?.chatId || 'default';
  }

  api.on('llm_input', async (event: any, ctx: any) => {
    try {
      const baseCfg = getConfig(api);
      const c = ctx || event?.context || {};
      // [v7.2] Multi-agent support — route to the correct Space/Key.
      const currentAgentForInput = resolveAgentId(event, baseCfg, api) ||
        (c.agentId as string) || (c.agent_id as string) || agentId(baseCfg, api);
      const cfg = resolveConfigForAgent(baseCfg, currentAgentForInput);
      const convId = getConversationIdFromCtx(c);
      const messages = c.messages || event?.messages || [];
      const model = c.model || event?.model;
      if (!Array.isArray(messages) || messages.length === 0) return;
      // Summarize prompt: system + last user + short tail
      const lastUser = [...messages].reverse().find((m: any) => m?.role === 'user');
      const lastUserText = typeof lastUser?.content === 'string'
        ? lastUser.content
        : Array.isArray(lastUser?.content)
          ? lastUser.content.map((p: any) => p?.text || '').join(' ')
          : '';
      if (!lastUserText || lastUserText.length < 20) return;
      llmInputBuffer.set(convId, {
        prompt: lastUserText.substring(0, 2000),
        timestamp: new Date().toISOString(),
        model,
      });
    } catch (err: any) {
      if (getConfig(api).debug) console.warn('[memtap] llm_input error:', err.message);
    }
  });

  api.on('llm_output', async (event: any, ctx: any) => {
    try {
      const baseCfg = getConfig(api);
      const LLM_CAPTURE_DAILY_CAP = Number((baseCfg as any).llmCaptureDailyCap ?? 30);
      const c = ctx || event?.context || {};
      // [v7.2] Multi-agent support — derive agent from event so each agent writes
      // to its own Space/Key (if configured) instead of the shared default.
      const currentAgentFromEvent = resolveAgentId(event, baseCfg, api) ||
        (c.agentId as string) || (c.agent_id as string) || agentId(baseCfg, api);
      const cfg = resolveConfigForAgent(baseCfg, currentAgentFromEvent);
      const convId = getConversationIdFromCtx(c);
      const output = typeof c.output === 'string'
        ? c.output
        : (c.text || c.content || event?.output || '');
      if (!output || typeof output !== 'string' || output.length < MIN_OUTPUT_LEN) return;
      if (isSystemNoise(output, c)) return;

      // Daily cap reset
      const today = new Date().toISOString().slice(0, 10);
      if (today !== llmCaptureDayAnchor) {
        llmCaptureDayAnchor = today;
        llmCaptureDailyCount = 0;
      }
      if (llmCaptureDailyCount >= LLM_CAPTURE_DAILY_CAP) {
        if (cfg.debug) console.log('[xmem] llm_output: daily cap reached, skipping');
        return;
      }

      const input = llmInputBuffer.get(convId);
      llmInputBuffer.delete(convId);

      // Fire and forget capture — llm_interaction source type
      llmCaptureDailyCount++;
      const currentAgent = currentAgentFromEvent;
      bbFetch(cfg, `${baseUrl(cfg)}/capture`, {
        method: 'POST',
        body: JSON.stringify({
          agent: currentAgent,
          conversation: {
            userMessage: input?.prompt || '',
            assistantMessage: output.substring(0, 8000),
            context: {
              channel: 'llm_interaction',
              conversationId: convId,
              model: input?.model || c.model,
              timestamp: new Date().toISOString(),
            },
          },
        }),
      }).catch((err: any) => {
        if (cfg.debug) console.warn('[xmem] llm_output capture failed:', err.message);
      });
    } catch (err: any) {
      if (getConfig(api).debug) console.warn('[memtap] llm_output error:', err.message);
    }
  });

  // ── [v7.0] File-artifact capture (tool_execution_end for write/edit) ──────
  //
  // Capture files the agent creates or modifies. These become `artifact`
  // documents on the server, chunked and embedded for semantic recall.
  const ARTIFACT_TOOLS = new Set(['write', 'edit', 'Write', 'Edit', 'str_replace', 'create_file']);
  const ARTIFACT_BLOCK_PATTERNS = [
    /\.env(\.|$)/i, /id_rsa/i, /\.pem$/i, /\.key$/i,
    /\bnode_modules\//, /\b\.git\//, /\bdist\//, /\.lock$/,
    /^\/tmp\//, /password/i, /secret/i, /credential/i,
  ];
  const MAX_ARTIFACT_BYTES = 256 * 1024; // 256KB per file — chunk-able

  function isArtifactBlocked(path: string): boolean {
    if (!path || typeof path !== 'string') return true;
    return ARTIFACT_BLOCK_PATTERNS.some(re => re.test(path));
  }

  api.on('tool_execution_end', async (event: any, ctx: any) => {
    try {
      const baseCfg = getConfig(api);
      const c = ctx || event?.context || {};
      const toolName = c.toolName || c.tool_name || event?.toolName || '';
      if (!ARTIFACT_TOOLS.has(toolName)) return;

      const args = c.args || c.input || event?.args || {};
      const path = args.path || args.file_path || args.filename || args.target_file;
      if (!path || isArtifactBlocked(path)) return;

      // Try to read the content from the tool args (for write) or from disk
      let content: string = args.content || args.text || args.new_str || '';
      if (!content) {
        try {
          const fs = await import('fs/promises');
          const buf = await fs.readFile(path);
          if (buf.length > MAX_ARTIFACT_BYTES) return; // too large
          content = buf.toString('utf8');
        } catch { return; }
      }
      if (!content || content.length < 20) return;
      if (content.length > MAX_ARTIFACT_BYTES) content = content.substring(0, MAX_ARTIFACT_BYTES);

      // [v7.2] Route to the correct Space based on which agent made the edit.
      //  Hook context carries agentId per event; each agent can have its own
      //  apiKey/serverUrl in cfg.agents[agentId].
      const currentAgent = resolveAgentId(event, baseCfg, api) ||
        (c.agentId as string) || (c.agent_id as string) || agentId(baseCfg, api);
      const cfg = resolveConfigForAgent(baseCfg, currentAgent);
      if (!cfg.apiKey) return; // no key for this agent — silently skip, do not leak
      if (baseCfg.agents && !baseCfg.agents[currentAgent] && !baseCfg.apiKey) return;
      bbFetch(cfg, `${baseUrl(cfg)}/artifacts`, {
        method: 'POST',
        body: JSON.stringify({
          agent: currentAgent,
          path,
          tool: toolName,
          content,
          timestamp: new Date().toISOString(),
        }),
      }).catch((err: any) => {
        if (cfg.debug) console.warn('[xmem] artifact upload failed:', err.message);
      });

      // [v7.3] Additionally store the raw file blob in /attachments.
      // Uses central detectMimeType helper so all known extensions get the right mime.
      // Dedup by SHA-256 happens server-side (409 on duplicate is fine).
      try {
        const buf = Buffer.from(content, 'utf-8');
        if (buf.length <= 5 * 1024 * 1024) {
          const mimeType = detectMimeType(path);
          const classification = classifyFile(path);
          bbFetch(cfg, `${baseUrl(cfg)}/attachments`, {
            method: 'POST',
            body: JSON.stringify({
              agent: currentAgent,
              filename: path,
              mimetype: mimeType,
              data_base64: buf.toString('base64'),
              description: `File ${toolName || 'captured'} by agent: ${path}`,
              tags: ['auto-capture', toolName || 'unknown', ...classification.tags].filter(Boolean),
            }),
          }).catch((err: any) => {
            const msg = String(err?.message || '');
            if (cfg.debug && !msg.includes('409') && !msg.toLowerCase().includes('duplicate')) {
              console.warn('[xmem] attachment upload failed:', msg);
            }
          });
        }
      } catch (e: any) {
        if (cfg.debug) console.warn('[xmem] attachment prep failed:', e.message);
      }

      // [v8.0] Auto-transcribe: if the captured file is audio/video, send to /v1/transcribe.
      // Transcription text is stored as an additional memory for searchability.
      const MEDIA_EXTS = new Set(['mp3', 'm4a', 'wav', 'flac', 'ogg', 'webm', 'mp4', 'mov', 'avi', 'mkv']);
      const fileExt = (path.split('.').pop() || '').toLowerCase();
      if (MEDIA_EXTS.has(fileExt)) {
        try {
          const fsPromises = await import('fs/promises');
          const mediaBuf = await fsPromises.readFile(path);
          if (mediaBuf.length <= 25 * 1024 * 1024) {
            bbFetch(cfg, `${baseUrl(cfg)}/transcribe`, {
              method: 'POST',
              body: JSON.stringify({
                data_base64: mediaBuf.toString('base64'),
                filename: path.split('/').pop() || path,
              }),
            }).then((result: any) => {
              if (result?.text) {
                // Store the transcription as a memory
                bbFetch(cfg, `${baseUrl(cfg)}/capture`, {
                  method: 'POST',
                  body: JSON.stringify({
                    agent: currentAgent,
                    conversation: {
                      userMessage: `[Auto-transcribed from ${path.split('/').pop()}]`,
                      assistantMessage: result.text,
                      context: { source: 'auto-transcribe', file: path, timestamp: new Date().toISOString() },
                    },
                  }),
                }).catch(() => {});
                if (cfg.debug) console.log(`[xmem] auto-transcribed ${path} (${result.durationSeconds || '?'}s)`);
              }
            }).catch((err: any) => {
              if (cfg.debug) console.warn('[xmem] auto-transcribe failed:', err.message);
            });
          }
        } catch (e: any) {
          if (cfg.debug) console.warn('[xmem] auto-transcribe read failed:', e.message);
        }
      }

      // [v8.0] AST-aware capture: for code files, additionally extract symbols via /v1/code/symbols.
      // Falls back silently to regex-only extraction if server endpoint is unavailable.
      const CODE_EXTS = new Set(['ts', 'tsx', 'js', 'jsx', 'py', 'go', 'rs', 'java', 'rb', 'cpp', 'c', 'cs', 'swift', 'kt']);
      if (CODE_EXTS.has(fileExt) && content.length > 0) {
        bbFetch(cfg, `${baseUrl(cfg)}/code/symbols`, {
          method: 'POST',
          body: JSON.stringify({ content, language: fileExt }),
        }).then((result: any) => {
          const symbols = result?.symbols || [];
          if (symbols.length > 0 && cfg.debug) {
            console.log(`[xmem] AST extracted ${symbols.length} symbols from ${path}`);
          }
          // Symbols are stored server-side as part of the code index — no additional capture needed.
        }).catch(() => {
          // Graceful degradation: regex fallback for extraction still works via /artifacts.
        });
      }
    } catch (err: any) {
      if (getConfig(api).debug) console.warn('[memtap] tool_execution_end error:', err.message);
    }
  });

  // ── Auto-Recall via before_prompt_build (like Mem0) ────────────────────────
  // Injects relevant memories as system-context, NOT as assistant prefill.
  // This avoids the "assistant message prefill" error on Opus via OpenRouter.

  const RECALL_TIMEOUT_MS = 8000;
  // [v8.1] Relevance gate: keep only memories whose fused score is at least this
  // fraction of the top hit. Prevents forcing weak/irrelevant recalls (aha-killers).
  const RECALL_REL_GATE = parseFloat(process.env.XMEM_RECALL_REL_GATE || '0.55');
  // [v8.1] Hard cap on how many memories we inject per turn.
  const RECALL_MAX_INJECT = parseInt(process.env.XMEM_RECALL_MAX_INJECT || '5', 10);

  // [v8.1] Human-friendly "how long ago" label from an ISO timestamp. Powers the
  // visible temporal recall ("~2 months ago") that is xmem's UX differentiator.
  function formatAge(iso?: string): string {
    if (!iso) return '';
    const then = new Date(iso).getTime();
    if (isNaN(then)) return '';
    const secs = Math.max(0, (Date.now() - then) / 1000);
    const days = secs / 86400;
    if (secs < 90) return 'just now';
    if (secs < 3600) return `~${Math.round(secs / 60)} minutes ago`;
    if (secs < 86400) return `~${Math.round(secs / 3600)} hours ago`;
    if (days < 14) return `~${Math.round(days)} days ago`;
    if (days < 60) return `~${Math.round(days / 7)} weeks ago`;
    if (days < 365) return `~${Math.round(days / 30)} months ago`;
    const years = days / 365;
    return years < 2 ? '~1 year ago' : `~${Math.round(years)} years ago`;
  }

  api.on('before_prompt_build', async (event: any, ctx: any) => {
    const baseCfg = getConfig(api);
    const prompt = event?.prompt || '';
    if (!prompt || prompt.length < 5) return;

    // Skip system/bootstrap prompts
    const promptLower = prompt.toLowerCase();
    if (promptLower.includes('a new session was started') ||
        promptLower.includes('session startup sequence') ||
        promptLower.startsWith('run your session')) {
      return;
    }

    // [v7.2] Pull the per-event agent so recall hits the right Space.
    const eventCtx = ctx || event?.context || {};
    const currentAgent = resolveAgentId(event, baseCfg, api) ||
      (eventCtx.agentId as string) || (eventCtx.agent_id as string) || agentId(baseCfg, api);
    const cfg = resolveConfigForAgent(baseCfg, currentAgent);
    const recallWork = async () => {
      try {
        const query = prompt.slice(0, 500); // Limit query length
        // [v8.1] Use the superior multi-signal fused endpoint (BM25 + importance +
        // recency + GRAPH-PROXIMITY) instead of plain /recall. Superseded facts are
        // filtered server-side (P2.4) so we never surface outdated memories.
        let res: any;
        try {
          res = await bbFetch(cfg, `${baseUrl(cfg)}/recall/fused`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ q: query, agent: currentAgent, limit: 6, maxDepth: 2 }),
          });
        } catch {
          // Fallback to classic recall if /fused is unavailable (older server).
          res = await bbFetch(cfg, `${baseUrl(cfg)}/recall?q=${encodeURIComponent(query)}&agent=${currentAgent}&limit=6`);
        }
        const memories = res.results || res.memories || res || [];
        let validMemories = Array.isArray(memories) ? memories.filter((m: any) => m.content || m.summary) : [];

        // [v8.1] Relevance gate: only keep memories that clearly matched. `/recall/fused`
        // returns a fused score in [0,1]; classic /recall returns a raw BM25*importance
        // score. Gate relative to the top hit so we adapt to either scale, and never
        // inject weak matches that would produce forced/aha-killing references.
        if (validMemories.length > 0 && typeof validMemories[0].score === 'number') {
          const top = Math.max(...validMemories.map((m: any) => m.score || 0), 0.0001);
          validMemories = validMemories.filter((m: any) => (m.score || 0) >= top * RECALL_REL_GATE);
        }
        validMemories = validMemories.slice(0, RECALL_MAX_INJECT);

        if (validMemories.length === 0) return undefined;

        const memoryContext = validMemories.map((m: any) => {
          const age = formatAge(m.created || m.created_at);
          const label = m.type || 'memory';
          const text = m.summary || m.content;
          const tags = m.tags?.length ? ` [${m.tags.join(', ')}]` : '';
          return `- [${label}${age ? ` · ${age}` : ''}] ${text}${tags}`;
        }).join('\n');

        console.log(`[memtap] before_prompt_build: injecting ${validMemories.length} memories as system context`);

        return {
          prependContext:
            `<xmem-memories>\n` +
            `The following are relevant memories from YOUR long-term memory (via xmem), with how long ago each was recorded.\n` +
            `If any directly informs the user's question, acknowledge it naturally in your reply — e.g. "Based on what we worked out ~2 months ago, ...". ` +
            `Only do this when a memory genuinely informs the answer; never force it or invent connections.\n\n` +
            `${memoryContext}\n` +
            `</xmem-memories>`
        };
      } catch (err: any) {
        console.warn(`[memtap] before_prompt_build recall failed: ${err.message}`);
        return undefined;
      }
    };

    try {
      const timeout = new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), RECALL_TIMEOUT_MS));
      return await Promise.race([recallWork(), timeout]);
    } catch {
      return undefined;
    }
  });

  // [v6.0] Removed: agent_end capture hook.
  // It sent a malformed payload (no `conversation` wrapper) that the server
  // silently rejected as validation error. The message:sent hook above
  // already covers every assistant reply.

  /* ── LEGACY: Supplements disabled — cause assistant prefill on Opus/OpenRouter ──
  // Replaced by before_prompt_build + agent_end hooks above (Mem0-style).
  // registerMemoryCorpusSupplement and registerMemoryPromptSupplement both inject
  // as assistant-role content which OpenRouter rejects for Claude Opus.
  // Keep commented for reference until OpenClaw fixes supplement injection.
  */

  // ── Hook: Auto-Onboarding & Instructions (agent:bootstrap) ────────────────

  api.registerHook(
    'agent:bootstrap',
    async (event: any) => {
      console.log('[memtap] agent:bootstrap hook FIRED');
      const baseCfg = getConfig(api);
      // [v7.2] Use event-provided agentId so each agent on a multi-agent instance
      // gets its own Space/Key (if configured) and its own memory graph.
      const currentAgent = resolveAgentId(event, baseCfg, api);
      const cfg = resolveConfigForAgent(baseCfg, currentAgent);
      const bootstrapFiles = event.context?.bootstrapFiles || [];
      console.log(`[memtap] bootstrap: agent=${currentAgent}, bootstrapFiles=${bootstrapFiles.length}, cfg.serverUrl=${cfg.serverUrl}, key=${cfg.apiKey ? cfg.apiKey.slice(0, 12) + '…' : 'none'}`);

      // [v7.2] Safety: if a per-agent key is declared but missing, abort early.
      // Better to skip onboarding than leak this agent's memories into the default Space.
      if (baseCfg.agents && !baseCfg.agents[currentAgent] && !baseCfg.apiKey) {
        console.warn(`[memtap] agent=${currentAgent} has no apiKey in plugin config (neither per-agent nor top-level) — skipping to avoid cross-agent memory leak`);
        bootstrapFiles.push({
          name: 'XMEM_STATUS.md', path: 'XMEM_STATUS.md', filePath: 'XMEM_STATUS.md', missing: false, source: 'xmem-status',
          content: `# xmem: NOT CONNECTED\n\nNo API key configured for agent "${currentAgent}". Memory is disabled.\n\nTo fix: add your API key in plugin config. Get one at https://xmem.space/dashboard`,
        });
        return;
      }
      if (!cfg.apiKey) {
        console.warn(`[memtap] agent=${currentAgent}: resolved config has no apiKey — skipping bootstrap/onboarding`);
        bootstrapFiles.push({
          name: 'XMEM_STATUS.md', path: 'XMEM_STATUS.md', filePath: 'XMEM_STATUS.md', missing: false, source: 'xmem-status',
          content: `# xmem: NOT CONNECTED\n\nNo API key configured. Memory tools are disabled.\n\nTo fix: set plugins.entries.memtap.config.apiKey to your key.\nGet one at https://xmem.space/dashboard`,
        });
        return;
      }

      // ── Auto-onboarding: scrape workspace .md files on first run ──
      try {
        // [v7.2] Re-onboard once per agent to pick up:
        //   • attachments (blob storage) for each workspace md
        //   • multi-agent Space routing (each agent writes to its own key)
        const ONBOARD_MARKER = 'system:onboarded-v7.3';
        console.log(`[memtap] bootstrap: checking onboarded status (${ONBOARD_MARKER})`);

        // [v8.1] Non-blocking onboarding check with a short 8s timeout. A slow or
        // hanging onboarding endpoint must NEVER stall an agent bootstrap turn.
        // On timeout/error we treat the agent as "already onboarded" (skip the
        // expensive deep-scan) rather than re-running the full workspace scan on
        // every single bootstrap — that was the root cause of the multi-minute
        // cold-start hangs reported on psiBot/psiphone.
        let isOnboarded = false;
        try {
          const searchRes = await bbFetch(
            cfg,
            `${baseUrl(cfg)}/memories?tags=${ONBOARD_MARKER}&agent=${currentAgent}&limit=1`,
            { timeoutMs: 8_000 },
          );
          const memories = searchRes.memories || searchRes.results || searchRes || [];
          isOnboarded = Array.isArray(memories) && memories.length > 0 &&
            (memories[0].tags || []).includes(ONBOARD_MARKER);
        } catch (checkErr: any) {
          console.warn(`[memtap] onboarding status check failed (${checkErr?.message || checkErr}) — skipping deep-scan for this bootstrap to avoid stalling the turn`);
          isOnboarded = true; // fail-open: do not re-run the full workspace scan
        }

        if (!isOnboarded) {
          logger.info?.(`[memtap] v7.3 deep-onboarding (agent=${currentAgent}) — full workspace scan`) ??
            console.log(`[memtap] v7.3 deep-onboarding (agent=${currentAgent}) — full workspace scan`);

          const workspaceRoot = api?.context?.workspace || process.cwd();
          console.log(`[memtap] onboarding workspace root: ${workspaceRoot}`);
          const result = await captureWorkspaceFiles(cfg, currentAgent, workspaceRoot);

          await bbFetch(cfg, `${baseUrl(cfg)}/memories`, {
            method: 'POST',
            body: JSON.stringify({
              content: `Onboarded v7.3 on ${new Date().toISOString()} — agent=${currentAgent}, deep-scan captured=${result.captured}, skipped=${result.skipped}, errors=${result.errors}`,
              agent: currentAgent,
              type: 'fact',
              importance: 0.1,
              tags: [ONBOARD_MARKER],
              source: 'plugin:deep-scan',
            }),
          });

          logger.info?.(`[memtap] v7.3 deep-onboarding complete agent=${currentAgent}: ${result.captured} captured, ${result.skipped} skipped`) ??
            console.log(`[memtap] v7.3 deep-onboarding complete agent=${currentAgent}: ${result.captured} captured, ${result.skipped} skipped`);

          // NOTE: We intentionally do NOT push a success XMEM_STATUS.md into bootstrapFiles —
          // that risks triggering assistant-prefill errors on some LLMs (see DISABLED block below).
          // The success state is recorded via the /memories POST above (onboard marker) so the
          // dashboard and subsequent runs can see it. Error/missing-key paths still push status
          // because those are failure modes the user *must* see.
        }
      } catch (err: any) {
        logger.warn?.(`[memtap] Auto-onboarding failed: ${err.message}`) ??
          console.warn(`[memtap] Auto-onboarding failed: ${err.message}`);
        // User-visible error feedback
        bootstrapFiles.push({
          name: 'XMEM_STATUS.md', path: 'XMEM_STATUS.md', filePath: 'XMEM_STATUS.md', missing: false, source: 'xmem-status',
          content: `# xmem: Connection Error\n\nCould not connect to xmem server: ${err.message}\n\nMemory tools may not work. Check your serverUrl and API key configuration.\nDashboard: https://xmem.space/dashboard`,
        });
      }

      // ── xmem Agent Instructions ──
      // DISABLED: bootstrapFiles.push may cause assistant prefill error
      // TODO: investigate if push to bootstrapFiles triggers prefill on Opus/OpenRouter
      /*
      bootstrapFiles.push({
        name: 'MEMTAP_INSTRUCTIONS.md',
        path: 'MEMTAP_INSTRUCTIONS.md',
        filePath: 'MEMTAP_INSTRUCTIONS.md',
        content: `# xmem — Your Long-Term Memory System

You have a persistent knowledge graph powered by xmem. Use it actively!

## Core Tools (use these frequently)

### Remembering
- **memtap_remember** — Store important facts, decisions, preferences, instructions
  - Use when: user shares personal info, makes a decision, states a preference, gives you an instruction
  - Always set appropriate type (fact, preference, decision, identity, event, goal, task)

### Recalling
- **memtap_recall** — Search your memory before answering questions
  - Use when: user asks about something you might have discussed before
  - Use when: you need context about a project, person, or decision

## When to Remember (automatically detect these)
- User says "merk dir", "remember this", "vergiss nicht", "note this down"
- User shares: name, address, preferences, work info, project details
- Important decisions are made
- User gives persistent instructions ("ab jetzt immer...", "from now on...")

## When to Recall
- Before answering any question that could benefit from prior context
- When user references something from a previous conversation

## Advanced Tools
- **memtap_manage** — Memory management: get/update/delete memories, entities, edges, graph analysis
- **memtap_analyze** — Deep analysis: GraphRAG, bulletins, inference, profiles, export
- **memtap_track** — Decision tracking: create/resolve decisions, record outcomes
- **memtap_scan** — Ingest workspace .md files into memory
- **memtap_intent** — Configure memory intent detection patterns

## Rules
- Recall BEFORE you say "I don't remember" or "I'm not sure"
- Store important things IMMEDIATELY, don't wait
- Use tags and types for better organization
`,
        missing: false,
        source: 'memtap-instructions',
      });
    */
    },
    {
      name: 'memtap.bootstrap-onboard',
      description: 'Auto-onboarding with workspace MD scraping and instructions injection',
    }
  );

  // ── Hook: Persistent Dream-Cycle & Neural Maintenance (periodic) ──────────

  // [v7.3] Track last deep-scan timestamp per agent in-memory (re-scanned every
  // RESCAN_INTERVAL_MS). Per-agent so each Space gets its own rescan cadence.
  const lastRescanAt = new Map<string, number>();
  const RESCAN_INTERVAL_MS = 2 * 60 * 60 * 1000; // 2 hours

  api.registerHook(
    'periodic',
    async (event: any) => {
      const baseCfg = getConfig(api);
      // [v7.3] Use event-provided agent if present (same pattern as other hooks).
      const currentAgentId = resolveAgentId(event, baseCfg, api);
      const cfg = resolveConfigForAgent(baseCfg, currentAgentId);

      if (!cfg.apiKey) return; // no key — skip silently

      // ── Periodic deep re-scan every 2h per agent ─────────────────
      // Catches newly-added files that the one-shot onboarding missed.
      const now = Date.now();
      const last = lastRescanAt.get(currentAgentId) || 0;
      if (now - last > RESCAN_INTERVAL_MS) {
        lastRescanAt.set(currentAgentId, now);
        try {
          const workspaceRoot = api?.context?.workspace || process.cwd();
          const r = await captureWorkspaceFiles(cfg, currentAgentId, workspaceRoot);
          if (cfg.debug) {
            console.log(`[memtap] periodic rescan agent=${currentAgentId}: ${r.captured} captured, ${r.skipped} skipped`);
          }
        } catch (e: any) {
          if (cfg.debug) console.warn(`[memtap] periodic rescan failed agent=${currentAgentId}: ${e?.message || e}`);
        }
      }

      // Dream-cycle with API-persisted schedule (survives gateway restarts)
      if (await shouldRunDreamMode(cfg, currentAgentId)) {
        logger.info?.('[memtap] Starting persistent dream cycle...') ??
          console.log('[memtap] Starting persistent dream cycle...');

        // In-memory consolidation (chunks, episodics, patterns)
        await dreamModeConsolidation(cfg);

        // Persist completion date to API
        await markDreamCompleted(cfg, currentAgentId);

        logger.info?.('[memtap] Dream cycle completed for today') ??
          console.log('[memtap] Dream cycle completed for today');
      }

      // Neural maintenance (low probability)
      if (Math.random() < 0.05) {
        await neuralMaintenance();
      }
    },
    {
      name: 'memtap.dream-consolidation',
      description: 'Periodic deep workspace rescan (2h) + dream-cycle + neural maintenance. Multi-agent aware.',
    }
  );

// ── Neural Monitoring & Analytics Functions ──────────────────────────────────

function generateNeuralReport(agentFilter?: string): string {
  const now = Date.now();
  let report = `# 🧠 xmem Neural System Report\n\n`;

  // Working Memory Analysis
  report += `## Working Memory States:\n`;
  let wmCount = 0;
  for (const [agentId, wm] of workingMemoryState.entries()) {
    if (agentFilter && agentId !== agentFilter) continue;
    wmCount++;
    
    const age = Math.round((now - wm.lastUpdate) / (1000 * 60));
    const loadStatus = wm.cognitiveLoad > 0.8 ? '🔴 HIGH' : 
                      wm.cognitiveLoad > 0.5 ? '🟡 MEDIUM' : '🟢 LOW';
    
    report += `  **${agentId}:**\n`;
    report += `    Focus: [${wm.currentFocus.join(', ')}]\n`;
    report += `    Spotlight: "${wm.attentionSpotlight}"\n`;
    report += `    Load: ${Math.round(wm.cognitiveLoad * 100)}% ${loadStatus}\n`;
    report += `    Active Memories: ${wm.activeMemories.length}\n`;
    report += `    Last Update: ${age}min ago\n\n`;
  }
  if (wmCount === 0) report += `  No active working memory states\n\n`;

  // Conversation Context Analysis
  report += `## Conversation Contexts:\n`;
  let ctxCount = 0;
  for (const [agentId, ctx] of conversationState.entries()) {
    if (agentFilter && agentId !== agentFilter) continue;
    ctxCount++;
    
    const lastAccess = ctx.lastMemoryAccess ? 
      Math.round((now - ctx.lastMemoryAccess) / (1000 * 60)) + 'min ago' : 
      'never';
    
    report += `  **${agentId}:**\n`;
    report += `    Topics: [${ctx.recentTopics.join(', ')}]\n`;
    report += `    Dominant: "${ctx.dominantTopic || 'none'}"\n`;
    report += `    Attention: ${ctx.attentionLevel} | Emotion: ${ctx.emotionalContext}\n`;
    report += `    Engagement: ${ctx.userEngagement} | Queries: ${ctx.memoryQueryCount}\n`;
    report += `    Last Memory Access: ${lastAccess}\n\n`;
  }
  if (ctxCount === 0) report += `  No active conversation contexts\n\n`;

  // Episodic Memory Summary
  report += `## Episodic Memories:\n`;
  let episodicCount = 0;
  for (const [agentId, episodes] of episodicMemories.entries()) {
    if (agentFilter && agentId !== agentFilter) continue;
    episodicCount += episodes.length;
    
    const avgEmotion = episodes.length > 0 ? 
      episodes.reduce((sum, e) => sum + e.emotionalIntensity, 0) / episodes.length : 0;
    const avgConsolidation = episodes.length > 0 ? 
      episodes.reduce((sum, e) => sum + e.consolidationScore, 0) / episodes.length : 0;
    
    report += `  **${agentId}:** ${episodes.length} episodes\n`;
    report += `    Avg Emotional Intensity: ${Math.round(avgEmotion * 100)}%\n`;
    report += `    Avg Consolidation: ${Math.round(avgConsolidation * 100)}%\n`;
  }
  report += `  Total Episodic Memories: ${episodicCount}\n\n`;

  // Memory Chunks Analysis
  report += `## Memory Chunks:\n`;
  let chunkCount = 0;
  for (const [agentId, chunks] of memoryChunks.entries()) {
    if (agentFilter && agentId !== agentFilter) continue;
    chunkCount += chunks.length;
    
    const strongChunks = chunks.filter(c => c.strength > 0.8).length;
    const concepts = chunks.map(c => c.abstractConcept).slice(0, 3).join(', ');
    
    report += `  **${agentId}:** ${chunks.length} chunks (${strongChunks} strong)\n`;
    report += `    Top Concepts: ${concepts}\n`;
  }
  report += `  Total Memory Chunks: ${chunkCount}\n\n`;

  return report;
}

function generatePerformanceReport(agentFilter?: string): string {
  let report = `# ⚡ xmem Performance Report\n\n`;

  // Cache Performance
  report += `## Cache Performance:\n`;
  let totalCacheEntries = 0;
  let totalRetrievals = 0;
  let hitRate = 0;

  for (const [key, cached] of memoryCache.entries()) {
    if (agentFilter && !key.startsWith(agentFilter + ':')) continue;
    totalCacheEntries++;
    totalRetrievals += cached.retrievalCount || 0;
  }

  if (totalCacheEntries > 0) {
    hitRate = totalRetrievals / totalCacheEntries;
  }

  report += `  Cache Entries: ${totalCacheEntries}\n`;
  report += `  Total Retrievals: ${totalRetrievals}\n`;
  report += `  Avg Hit Rate: ${Math.round(hitRate * 100)}%\n`;
  report += `  Cache Efficiency: ${hitRate > 1.5 ? '🟢 EXCELLENT' : hitRate > 0.8 ? '🟡 GOOD' : '🔴 POOR'}\n\n`;

  // User Profile Performance
  report += `## User Profiles:\n`;
  let totalProfiles = 0;
  let totalQueries = 0;
  let totalSuccessful = 0;

  for (const [agentId, profile] of userProfiles.entries()) {
    if (agentFilter && agentId !== agentFilter) continue;
    totalProfiles++;
    totalQueries += profile.totalQueries;
    totalSuccessful += profile.successfulRecalls;

    if (!agentFilter) {
      const successRate = profile.totalQueries > 0 ? 
        Math.round((profile.successfulRecalls / profile.totalQueries) * 100) : 0;
      
      report += `  **${agentId}:**\n`;
      report += `    Queries: ${profile.totalQueries} | Success: ${successRate}%\n`;
      report += `    Sensitivity: ${profile.recallSensitivity} | Complexity: ${profile.averageQueryComplexity.toFixed(1)}\n`;
      report += `    Sleep Cycles: ${profile.sleepCycles} | Last Active: ${new Date(profile.lastActive).toLocaleString()}\n\n`;
    }
  }

  const overallSuccessRate = totalQueries > 0 ? 
    Math.round((totalSuccessful / totalQueries) * 100) : 0;

  report += `## Overall Performance:\n`;
  report += `  Active Profiles: ${totalProfiles}\n`;
  report += `  Total Queries: ${totalQueries}\n`;
  report += `  Success Rate: ${overallSuccessRate}%\n`;
  report += `  System Health: ${overallSuccessRate > 80 ? '🟢 EXCELLENT' : overallSuccessRate > 60 ? '🟡 GOOD' : '🔴 NEEDS ATTENTION'}\n\n`;

  return report;
}

function generateAnalyticsReport(agentFilter?: string, timeRange: string = '24h'): string {
  const now = Date.now();
  const ranges = {
    '1h': 60 * 60 * 1000,
    '24h': 24 * 60 * 60 * 1000,
    '7d': 7 * 24 * 60 * 60 * 1000
  };
  const cutoff = now - (ranges[timeRange as keyof typeof ranges] || ranges['24h']);

  let report = `# 📊 xmem Analytics Report (${timeRange})\n\n`;

  // Attention Patterns Analysis
  const recentAttention = attentionHistory.filter(a => 
    a.timestamp > cutoff && (!agentFilter || a.agent === agentFilter)
  );

  if (recentAttention.length > 0) {
    const attentionCounts = recentAttention.reduce((acc, a) => {
      acc[a.level] = (acc[a.level] || 0) + 1;
      return acc;
    }, {} as Record<string, number>);

    report += `## Attention Patterns (${recentAttention.length} events):\n`;
    for (const [level, count] of Object.entries(attentionCounts)) {
      const pct = Math.round((count / recentAttention.length) * 100);
      report += `  ${level}: ${count} events (${pct}%)\n`;
    }
    report += '\n';
  }

  // Engagement Analysis
  report += `## User Engagement:\n`;
  for (const [agentId, ctx] of conversationState.entries()) {
    if (agentFilter && agentId !== agentFilter) continue;
    
    const profile = userProfiles.get(agentId);
    if (!profile || profile.lastActive < cutoff) continue;

    report += `  **${agentId}:**\n`;
    report += `    Current Engagement: ${ctx.userEngagement}\n`;
    report += `    Memory Queries: ${ctx.memoryQueryCount} in session\n`;
    report += `    Dominant Topic: ${ctx.dominantTopic || 'none'}\n`;
    report += `    Emotional Context: ${ctx.emotionalContext}\n\n`;
  }

  // Memory Operation Trends
  const recentCacheOps = Array.from(memoryCache.entries()).filter(([key, cached]) => 
    cached.timestamp > cutoff && (!agentFilter || key.startsWith(agentFilter + ':'))
  );

  report += `## Memory Operations:\n`;
  report += `  Cache Operations: ${recentCacheOps.length}\n`;
  
  if (recentCacheOps.length > 0) {
    const avgRetrievals = recentCacheOps.reduce((sum, [, cached]) => 
      sum + (cached.retrievalCount || 0), 0) / recentCacheOps.length;
    report += `  Avg Retrievals per Entry: ${avgRetrievals.toFixed(1)}\n`;
  }

  // Query Complexity Distribution
  const complexities = Array.from(userProfiles.values())
    .filter(p => !agentFilter || agentFilter === 'all' || p.lastActive > cutoff)
    .map(p => p.averageQueryComplexity);

  if (complexities.length > 0) {
    const avgComplexity = complexities.reduce((sum, c) => sum + c, 0) / complexities.length;
    report += `  Avg Query Complexity: ${avgComplexity.toFixed(2)}\n`;
  }

  return report;
}

function generateLiveMonitoring(agentFilter?: string, duration: number = 30): string {
  let report = `# 🔴 LIVE Neural Monitoring (${duration}s)\n\n`;
  
  report += `## Current System State:\n`;
  report += `- Working Memory States: ${workingMemoryState.size}\n`;
  report += `- Active Conversations: ${conversationState.size}\n`;
  report += `- Cache Entries: ${memoryCache.size}\n`;
  report += `- User Profiles: ${userProfiles.size}\n`;
  report += `- Memory Chunks: ${Array.from(memoryChunks.values()).reduce((sum, chunks) => sum + chunks.length, 0)}\n`;
  report += `- Episodic Memories: ${Array.from(episodicMemories.values()).reduce((sum, eps) => sum + eps.length, 0)}\n\n`;

  if (agentFilter) {
    const wm = workingMemoryState.get(agentFilter);
    const ctx = conversationState.get(agentFilter);
    
    if (wm) {
      report += `## Live Working Memory (${agentFilter}):\n`;
      report += `- Focus: [${wm.currentFocus.join(', ')}]\n`;
      report += `- Cognitive Load: ${Math.round(wm.cognitiveLoad * 100)}%\n`;
      report += `- Active Memories: ${wm.activeMemories.length}\n\n`;
    }
    
    if (ctx) {
      report += `## Live Context (${agentFilter}):\n`;
      report += `- Attention: ${ctx.attentionLevel}\n`;
      report += `- Emotion: ${ctx.emotionalContext}\n`;
      report += `- Engagement: ${ctx.userEngagement}\n`;
      report += `- Recent Topics: [${ctx.recentTopics.join(', ')}]\n\n`;
    }
  }

  report += `*Monitoring started. Use memtap_monitor again to refresh.*\n`;
  
  return report;
}

function generateWorkingMemoryReport(agentFilter?: string): string {
  let report = `# 🧠 Working Memory Analysis\n\n`;
  
  for (const [agentId, wm] of workingMemoryState.entries()) {
    if (agentFilter && agentId !== agentFilter) continue;
    
    const age = Math.round((Date.now() - wm.lastUpdate) / 1000);
    
    report += `## Agent: ${agentId}\n`;
    report += `**Current Focus:** [${wm.currentFocus.join(', ')}]\n`;
    report += `**Attention Spotlight:** "${wm.attentionSpotlight}"\n`;
    report += `**Cognitive Load:** ${Math.round(wm.cognitiveLoad * 100)}% ${'▓'.repeat(Math.round(wm.cognitiveLoad * 10))}${'░'.repeat(10 - Math.round(wm.cognitiveLoad * 10))}\n`;
    report += `**Active Memories:** ${wm.activeMemories.length}\n`;
    report += `**Last Update:** ${age}s ago\n\n`;
    
    if (wm.activeMemories.length > 0) {
      report += `**Preloaded Memories:**\n`;
      wm.activeMemories.slice(0, 3).forEach((mem, i) => {
        report += `  ${i + 1}. [${mem.type}] ${mem.content?.substring(0, 60)}...\n`;
      });
      report += '\n';
    }
  }
  
  if (workingMemoryState.size === 0) {
    report += `No active working memory states found.\n`;
  }
  
  return report;
}

function generateAttentionReport(agentFilter?: string): string {
  const recentAttention = attentionHistory
    .filter(a => (!agentFilter || a.agent === agentFilter))
    .slice(-20);
  
  let report = `# 👁️ Attention Pattern Analysis\n\n`;
  
  if (recentAttention.length === 0) {
    return report + `No attention data available.\n`;
  }
  
  // Attention distribution
  const attentionCounts = recentAttention.reduce((acc, a) => {
    acc[a.level] = (acc[a.level] || 0) + 1;
    return acc;
  }, {} as Record<string, number>);
  
  report += `## Recent Attention Distribution (${recentAttention.length} events):\n`;
  for (const [level, count] of Object.entries(attentionCounts)) {
    const pct = Math.round((count / recentAttention.length) * 100);
    const bar = '█'.repeat(Math.round(pct / 5));
    report += `**${level}:** ${count} (${pct}%) ${bar}\n`;
  }
  report += '\n';
  
  // Recent events
  report += `## Recent Attention Events:\n`;
  recentAttention.slice(-10).reverse().forEach((event, i) => {
    const timeAgo = Math.round((Date.now() - event.timestamp) / 1000);
    report += `${i + 1}. **${event.level}** - "${event.trigger}" (${timeAgo}s ago)\n`;
  });
  
  return report;
}

function generateCacheReport(agentFilter?: string): string {
  let report = `# 💾 Memory Cache Analysis\n\n`;
  
  const relevantCache = Array.from(memoryCache.entries()).filter(([key]) => 
    !agentFilter || key.startsWith(agentFilter + ':')
  );
  
  if (relevantCache.length === 0) {
    return report + `No cache entries found.\n`;
  }
  
  // Cache statistics
  const totalRetrievals = relevantCache.reduce((sum, [, cached]) => sum + (cached.retrievalCount || 0), 0);
  const avgAge = relevantCache.reduce((sum, [, cached]) => sum + (Date.now() - cached.timestamp), 0) / relevantCache.length / 1000;
  const avgRetrievals = totalRetrievals / relevantCache.length;
  
  report += `## Cache Statistics:\n`;
  report += `**Total Entries:** ${relevantCache.length}\n`;
  report += `**Total Retrievals:** ${totalRetrievals}\n`;
  report += `**Avg Retrievals/Entry:** ${avgRetrievals.toFixed(2)}\n`;
  report += `**Avg Age:** ${Math.round(avgAge)}s\n`;
  report += `**Hit Rate:** ${avgRetrievals > 1 ? '🟢 Good' : '🟡 Low'}\n\n`;
  
  // Top cached queries
  const topCached = relevantCache
    .sort(([, a], [, b]) => (b.retrievalCount || 0) - (a.retrievalCount || 0))
    .slice(0, 5);
  
  report += `## Most Accessed Cache Entries:\n`;
  topCached.forEach(([key, cached], i) => {
    const query = cached.query.substring(0, 50);
    const age = Math.round((Date.now() - cached.timestamp) / 1000);
    report += `${i + 1}. "${query}..." (${cached.retrievalCount || 0} hits, ${age}s old)\n`;
  });
  
  return report;
}

function generateConsolidationReport(agentFilter?: string): string {
  let report = `# 🌙 Dream-Mode Consolidation Report\n\n`;
  
  // Memory chunks analysis
  const relevantChunks = Array.from(memoryChunks.entries()).filter(([agentId]) => 
    !agentFilter || agentId === agentFilter
  );
  
  if (relevantChunks.length === 0) {
    return report + `No consolidation data available.\n`;
  }
  
  const allChunks = relevantChunks.flatMap(([, chunks]) => chunks);
  const strongChunks = allChunks.filter(c => c.strength > 0.8);
  const avgStrength = allChunks.reduce((sum, c) => sum + c.strength, 0) / allChunks.length;
  
  report += `## Consolidation Statistics:\n`;
  report += `**Total Chunks:** ${allChunks.length}\n`;
  report += `**Strong Chunks:** ${strongChunks.length} (${Math.round(strongChunks.length / allChunks.length * 100)}%)\n`;
  report += `**Avg Strength:** ${avgStrength.toFixed(2)}\n`;
  report += `**Consolidation Health:** ${avgStrength > 0.7 ? '🟢 Excellent' : avgStrength > 0.5 ? '🟡 Good' : '🔴 Poor'}\n\n`;
  
  // Top consolidated concepts
  report += `## Top Consolidated Concepts:\n`;
  strongChunks.slice(0, 5).forEach((chunk, i) => {
    const age = Math.round((Date.now() - chunk.lastActivation) / (1000 * 60));
    report += `${i + 1}. **${chunk.abstractConcept}** (strength: ${chunk.strength.toFixed(2)}, ${chunk.relatedMemories.length} memories, ${age}min ago)\n`;
  });
  
  // Episodic consolidation
  const episodicData = Array.from(episodicMemories.entries()).filter(([agentId]) => 
    !agentFilter || agentId === agentFilter
  );
  
  if (episodicData.length > 0) {
    const allEpisodics = episodicData.flatMap(([, eps]) => eps);
    const avgConsolidation = allEpisodics.reduce((sum, e) => sum + e.consolidationScore, 0) / allEpisodics.length;
    
    report += `\n## Episodic Memory Consolidation:\n`;
    report += `**Total Episodes:** ${allEpisodics.length}\n`;
    report += `**Avg Consolidation Score:** ${Math.round(avgConsolidation * 100)}%\n`;
  }
  
  return report;
}

// ── Alert System & Dashboard Functions ───────────────────────────────────────

interface Alert {
  id: string;
  severity: 'info' | 'warning' | 'critical';
  message: string;
  timestamp: number;
  agent?: string;
  metric?: string;
  value?: number;
  threshold?: number;
}

const activeAlerts: Alert[] = [];

function performAnomalyDetection(minSeverity: string = 'warning'): string {
  const now = Date.now();
  let report = `# 🚨 Neural Anomaly Detection\n\n`;
  let alertCount = 0;

  // Check working memory overload
  for (const [agentId, wm] of workingMemoryState.entries()) {
    if (wm.cognitiveLoad > 0.9) {
      const alert: Alert = {
        id: `cognitive_overload_${agentId}`,
        severity: 'warning',
        message: `High cognitive load detected for ${agentId}`,
        timestamp: now,
        agent: agentId,
        metric: 'cognitive_load',
        value: wm.cognitiveLoad,
        threshold: 0.9
      };
      addAlert(alert);
      alertCount++;
    }
  }

  // Check for memory cache bloat
  if (memoryCache.size > 80) {
    const alert: Alert = {
      id: 'cache_bloat',
      severity: 'warning',
      message: 'Memory cache size approaching limit',
      timestamp: now,
      metric: 'cache_size',
      value: memoryCache.size,
      threshold: 80
    };
    addAlert(alert);
    alertCount++;
  }

  // Check for inactive working memory states (memory leak detection)
  const oneHourAgo = now - 60 * 60 * 1000;
  for (const [agentId, wm] of workingMemoryState.entries()) {
    if (wm.lastUpdate < oneHourAgo) {
      const alert: Alert = {
        id: `stale_wm_${agentId}`,
        severity: 'info',
        message: `Stale working memory state for ${agentId}`,
        timestamp: now,
        agent: agentId,
        metric: 'last_update',
        value: (now - wm.lastUpdate) / (1000 * 60), // minutes
        threshold: 60
      };
      addAlert(alert);
      alertCount++;
    }
  }

  // Check attention pattern anomalies
  const recentAttention = attentionHistory.filter(a => a.timestamp > now - 24 * 60 * 60 * 1000);
  if (recentAttention.length > 0) {
    const distractedCount = recentAttention.filter(a => a.level === 'distracted').length;
    const distractedRatio = distractedCount / recentAttention.length;
    
    if (distractedRatio > 0.7) {
      const alert: Alert = {
        id: 'attention_degradation',
        severity: 'warning',
        message: 'High distraction rate detected across system',
        timestamp: now,
        metric: 'distracted_ratio',
        value: distractedRatio,
        threshold: 0.7
      };
      addAlert(alert);
      alertCount++;
    }
  }

  // Check user profile health
  for (const [agentId, profile] of userProfiles.entries()) {
    if (profile.totalQueries > 10) {
      const successRate = profile.successfulRecalls / profile.totalQueries;
      if (successRate < 0.6) {
        const alert: Alert = {
          id: `low_success_rate_${agentId}`,
          severity: 'warning',
          message: `Low memory recall success rate for ${agentId}`,
          timestamp: now,
          agent: agentId,
          metric: 'success_rate',
          value: successRate,
          threshold: 0.6
        };
        addAlert(alert);
        alertCount++;
      }
    }
  }

  // Filter alerts by severity
  const filteredAlerts = activeAlerts.filter(a => {
    const levels = { info: 0, warning: 1, critical: 2 };
    return levels[a.severity] >= levels[minSeverity as keyof typeof levels];
  });

  report += `**Anomaly Detection Complete**\n`;
  report += `- Total Checks: 5\n`;
  report += `- New Alerts: ${alertCount}\n`;
  report += `- Active Alerts (${minSeverity}+): ${filteredAlerts.length}\n\n`;

  if (filteredAlerts.length > 0) {
    report += `## Active Alerts:\n`;
    filteredAlerts.slice(-10).forEach((alert, i) => {
      const age = Math.round((now - alert.timestamp) / (1000 * 60));
      const icon = alert.severity === 'critical' ? '🔴' : 
                   alert.severity === 'warning' ? '🟡' : '🔵';
      
      report += `${i + 1}. ${icon} **${alert.severity.toUpperCase()}**: ${alert.message}\n`;
      if (alert.value && alert.threshold) {
        report += `   Value: ${alert.value.toFixed(2)} | Threshold: ${alert.threshold} | ${age}min ago\n`;
      }
      report += '\n';
    });
  } else {
    report += `✅ **System Status: HEALTHY**\nNo anomalies detected at ${minSeverity} level or above.\n`;
  }

  return report;
}

function addAlert(alert: Alert) {
  // Remove existing alert with same ID
  const existingIndex = activeAlerts.findIndex(a => a.id === alert.id);
  if (existingIndex >= 0) {
    activeAlerts[existingIndex] = alert;
  } else {
    activeAlerts.push(alert);
  }
  
  // Limit alert history
  if (activeAlerts.length > 100) {
    activeAlerts.splice(0, 50); // Keep most recent 50
  }
}

function listActiveAlerts(minSeverity: string = 'warning'): string {
  const levels = { info: 0, warning: 1, critical: 2 };
  const filtered = activeAlerts.filter(a => 
    levels[a.severity] >= levels[minSeverity as keyof typeof levels]
  );

  if (filtered.length === 0) {
    return `No active alerts at ${minSeverity} level or above.`;
  }

  let report = `# 🚨 Active Alerts (${filtered.length})\n\n`;
  
  const now = Date.now();
  filtered.slice(-20).reverse().forEach((alert, i) => {
    const age = Math.round((now - alert.timestamp) / (1000 * 60));
    const icon = alert.severity === 'critical' ? '🔴' : 
                 alert.severity === 'warning' ? '🟡' : '🔵';
    
    report += `## ${i + 1}. ${icon} ${alert.severity.toUpperCase()}\n`;
    report += `**Message:** ${alert.message}\n`;
    report += `**Time:** ${age} minutes ago\n`;
    if (alert.agent) report += `**Agent:** ${alert.agent}\n`;
    if (alert.value && alert.threshold) {
      report += `**Value:** ${alert.value.toFixed(2)} (threshold: ${alert.threshold})\n`;
    }
    report += '\n';
  });

  return report;
}

function clearAlerts() {
  activeAlerts.length = 0;
}

function generateDashboard(view: string = 'overview', agentFilter?: string): string {
  const now = Date.now();
  let dashboard = `# 🧠 xmem Neural System Dashboard\n`;
  dashboard += `*Updated: ${new Date().toLocaleString()}*\n\n`;

  // System Health Overview
  const totalProfiles = userProfiles.size;
  const activeWM = workingMemoryState.size;
  const activeConversations = conversationState.size;
  const cacheSize = memoryCache.size;
  const totalAlerts = activeAlerts.filter(a => a.severity !== 'info').length;

  dashboard += `## 🚦 System Health\n`;
  dashboard += `| Metric | Value | Status |\n`;
  dashboard += `|--------|-------|--------|\n`;
  dashboard += `| Active Users | ${totalProfiles} | ${totalProfiles > 0 ? '🟢' : '🟡'} |\n`;
  dashboard += `| Working Memory | ${activeWM} | ${activeWM > 0 ? '🟢' : '🟡'} |\n`;
  dashboard += `| Conversations | ${activeConversations} | ${activeConversations > 0 ? '🟢' : '🟡'} |\n`;
  dashboard += `| Cache Size | ${cacheSize}/100 | ${cacheSize < 80 ? '🟢' : cacheSize < 95 ? '🟡' : '🔴'} |\n`;
  dashboard += `| Alerts | ${totalAlerts} | ${totalAlerts === 0 ? '🟢' : totalAlerts < 5 ? '🟡' : '🔴'} |\n\n`;

  // Performance Metrics
  if (view !== 'compact') {
    const totalQueries = Array.from(userProfiles.values()).reduce((sum, p) => sum + p.totalQueries, 0);
    const successfulRecalls = Array.from(userProfiles.values()).reduce((sum, p) => sum + p.successfulRecalls, 0);
    const overallSuccessRate = totalQueries > 0 ? (successfulRecalls / totalQueries) * 100 : 0;

    dashboard += `## 📊 Performance Metrics\n`;
    dashboard += `- **Total Queries:** ${totalQueries}\n`;
    dashboard += `- **Success Rate:** ${overallSuccessRate.toFixed(1)}%\n`;
    dashboard += `- **Cache Hit Rate:** ${cacheSize > 0 ? 'Available' : 'N/A'}\n`;
    dashboard += `- **Memory Efficiency:** ${overallSuccessRate > 80 ? '🟢 Excellent' : overallSuccessRate > 60 ? '🟡 Good' : '🔴 Poor'}\n\n`;
  }

  // Neural Activity Summary
  if (view === 'detailed') {
    dashboard += `## 🧠 Neural Activity\n`;
    
    // Working Memory Summary
    if (workingMemoryState.size > 0) {
      const avgCognitiveLoad = Array.from(workingMemoryState.values())
        .reduce((sum, wm) => sum + wm.cognitiveLoad, 0) / workingMemoryState.size;
      
      dashboard += `### Working Memory\n`;
      dashboard += `- **Active States:** ${workingMemoryState.size}\n`;
      dashboard += `- **Avg Cognitive Load:** ${Math.round(avgCognitiveLoad * 100)}%\n`;
      
      // Show individual agents if filtered
      if (agentFilter) {
        const wm = workingMemoryState.get(agentFilter);
        if (wm) {
          dashboard += `- **Focus (${agentFilter}):** [${wm.currentFocus.join(', ')}]\n`;
          dashboard += `- **Spotlight:** "${wm.attentionSpotlight}"\n`;
        }
      }
      dashboard += '\n';
    }

    // Attention Patterns
    const recentAttention = attentionHistory.filter(a => 
      a.timestamp > now - 60 * 60 * 1000 && (!agentFilter || a.agent === agentFilter)
    );
    
    if (recentAttention.length > 0) {
      const attentionCounts = recentAttention.reduce((acc, a) => {
        acc[a.level] = (acc[a.level] || 0) + 1;
        return acc;
      }, {} as Record<string, number>);

      dashboard += `### Attention Patterns (Last Hour)\n`;
      for (const [level, count] of Object.entries(attentionCounts)) {
        const pct = Math.round((count / recentAttention.length) * 100);
        dashboard += `- **${level}:** ${count} (${pct}%)\n`;
      }
      dashboard += '\n';
    }

    // Memory Consolidation
    const totalChunks = Array.from(memoryChunks.values()).reduce((sum, chunks) => sum + chunks.length, 0);
    const totalEpisodics = Array.from(episodicMemories.values()).reduce((sum, eps) => sum + eps.length, 0);

    dashboard += `### Memory Consolidation\n`;
    dashboard += `- **Memory Chunks:** ${totalChunks}\n`;
    dashboard += `- **Episodic Memories:** ${totalEpisodics}\n`;
    dashboard += `- **Consolidation Status:** ${totalChunks > 0 ? '🟢 Active' : '🟡 Minimal'}\n\n`;
  }

  // Recent Alerts
  const recentAlerts = activeAlerts.filter(a => a.timestamp > now - 60 * 60 * 1000);
  if (recentAlerts.length > 0) {
    dashboard += `## 🚨 Recent Alerts (${recentAlerts.length})\n`;
    recentAlerts.slice(-3).forEach((alert, i) => {
      const age = Math.round((now - alert.timestamp) / (1000 * 60));
      const icon = alert.severity === 'critical' ? '🔴' : alert.severity === 'warning' ? '🟡' : '🔵';
      dashboard += `${i + 1}. ${icon} ${alert.message} (${age}min ago)\n`;
    });
    dashboard += '\n';
  }

  // Quick Actions
  dashboard += `## ⚡ Quick Actions\n`;
  dashboard += `- \`memtap_monitor live\` - Real-time neural monitoring\n`;
  dashboard += `- \`memtap_alerts check\` - Run anomaly detection\n`;
  dashboard += `- \`memtap_health neural\` - Detailed neural system report\n`;
  dashboard += `- \`memtap_maintenance run-all\` - Full system maintenance\n`;

  return dashboard;
}

// ── Dream-Mode Consolidation Function ────────────────────────────────────────

async function dreamModeConsolidation(cfg: MemTapConfig) {
  try {
    // Simulate sleep consolidation - strengthen important memories, weaken unused ones
    for (const [agentId, chunks] of memoryChunks.entries()) {
      
      // Strengthen frequently accessed chunks
      for (const chunk of chunks) {
        if (chunk.lastActivation > Date.now() - 86400000) { // Active in last 24h
          chunk.strength += FORGETTING_CURVE.consolidationBonus;
        } else {
          chunk.strength *= 0.98; // Slight decay for unused chunks
        }
      }
      
      // Remove very weak chunks (forgotten)
      const activeChunks = chunks.filter(c => c.strength > 0.1);
      memoryChunks.set(agentId, activeChunks);
      
      // Update consolidation scores for episodic memories
      const episodics = episodicMemories.get(agentId) || [];
      for (const episodic of episodics) {
        episodic.consolidationScore = Math.min(1.0, 
          episodic.consolidationScore + 
          (episodic.emotionalIntensity * FORGETTING_CURVE.consolidationBonus)
        );
      }
    }
    
    // Pattern recognition and abstraction (simplified)
    await backgroundPatternRecognition(cfg);
    
    console.log('[memtap] Dream-mode consolidation completed');
    
  } catch (err: any) {
    console.warn(`[memtap] Dream-mode consolidation failed: ${err.message}`);
  }
}

// dreamModeWorkspaceRescan removed in v5.4.0 — replaced by captureWorkspaceMdFiles

async function backgroundPatternRecognition(cfg: MemTapConfig) {
  // Analyze patterns in memory access and create abstract connections
  try {
    // Find frequently co-accessed memories
    const cachePatterns = new Map<string, string[]>();
    
    for (const [key, cached] of memoryCache.entries()) {
      if (cached.retrievalCount > 3) { // Frequently accessed
        const agent = key.split(':')[0];
        const patterns = cachePatterns.get(agent) || [];
        patterns.push(cached.query);
        cachePatterns.set(agent, patterns);
      }
    }
    
    // Create abstract pattern memories (very simplified)
    for (const [agent, patterns] of cachePatterns.entries()) {
      if (patterns.length > 5) {
        const abstractPattern = `Pattern detected: Frequent queries about ${extractCommonThemes(patterns)}`;
        
        // Store as a meta-memory
        await bbFetch(cfg, `${baseUrl(cfg)}/memories`, {
          method: 'POST',
          body: JSON.stringify({
            content: abstractPattern,
            type: 'pattern',
            agent: agent,
            importance: 0.6,
            tags: ['dream-extracted', 'pattern', 'meta-memory'],
            source: 'plugin:dream-mode-pattern-recognition'
          })
        }).catch(() => {}); // Silent fail
      }
    }
    
  } catch { /* Pattern recognition failed - not critical */ }
}

function extractCommonThemes(queries: string[]): string {
  const allText = queries.join(' ').toLowerCase();
  
  if (/memtap.*development/i.test(allText)) return 'xmem development patterns';
  if (/business.*strategy/i.test(allText)) return 'business strategy concerns';
  if (/infrastructure.*server/i.test(allText)) return 'infrastructure management';
  
  return 'general usage patterns';
}

async function neuralMaintenance() {
  try {
    const now = Date.now();
    const oneDayAgo = now - 24 * 60 * 60 * 1000;
    const oneWeekAgo = now - 7 * 24 * 60 * 60 * 1000;
    
    // Clean up old conversation state (older than 1 day)
    for (const [agentId, context] of conversationState.entries()) {
      if ((context.lastMemoryAccess || 0) < oneDayAgo) {
        conversationState.delete(agentId);
      }
    }
    
    // Clean up old cache entries and update retrieval counts
    for (const [key, cached] of memoryCache.entries()) {
      if (cached.timestamp < oneDayAgo) {
        memoryCache.delete(key);
      }
    }
    
    // Clean up old working memory states
    for (const [agentId, wm] of workingMemoryState.entries()) {
      if (wm.lastUpdate < oneDayAgo) {
        workingMemoryState.delete(agentId);
      }
    }
    
    // Clean up old episodic memories (keep only last 30 days)
    for (const [agentId, episodics] of episodicMemories.entries()) {
      const recentEpisodics = episodics.filter(e => e.timestamp > oneWeekAgo);
      episodicMemories.set(agentId, recentEpisodics);
    }
    
    // Update user profiles sleep cycles (for consolidation tracking)
    for (const [agentId, profile] of userProfiles.entries()) {
      if (profile.lastActive < oneDayAgo) {
        profile.sleepCycles++;
      }
    }
    
    // Clean up attention history
    attentionHistory = attentionHistory.filter(a => a.timestamp > oneWeekAgo);
    
    console.log('[memtap] Neural maintenance completed');
    
  } catch (err: any) {
    console.warn(`[memtap] Neural maintenance failed: ${err.message}`);
  }
}

  // [v6.0] Removed: duplicate session_end performance-monitor hook.
  // Stats can be retrieved on demand via memtap_health tool.

  // ── Tool: memtap_alerts (Anomaly Detection & Alerting) ──────────────────────

  // ── Tool: memtap_dashboard (Comprehensive System Overview) ─────────────────

  // ── Tool: memtap_outcome (Agent Learning Loop) ──────────────────────────────

  // ── Tool: memtap_infer (Inference Engine) ─────────────────────────────────

  // ── Tool: Memory Intent Management ──────────────────────────────────────────

  api.registerTool({
    name: 'memtap_intent',
    description:
      'Configure automatic memory intent detection patterns. ' +
      'Manage patterns that detect when users want the agent to remember something. ' +
      'Detected intents are automatically captured with high priority.',
    parameters: {
      type: 'object',
      properties: {
        action: {
          type: 'string',
          enum: ['list', 'add', 'remove', 'stats'],
          description: 'Action to perform: list patterns, add new pattern, remove pattern, or view statistics',
        },
        pattern: {
          type: 'string',
          description: 'Regex pattern string (for add action)',
        },
        type: {
          type: 'string',
          enum: ['explicit', 'preference', 'fact', 'instruction'],
          description: 'Intent type for the pattern (for add action)',
        },
        description: {
          type: 'string',
          description: 'Human-readable description of the pattern (for add action)',
        },
        id: {
          type: 'number',
          description: 'Pattern ID to remove (for remove action)',
        },
      },
      required: ['action'],
    },
    async execute(_id: string, params: { action: string; pattern?: string; type?: string; description?: string; id?: number }) {
      try {
        switch (params.action) {
          case 'list':
            let output = 'Memory Intent Detection Patterns\n\n';

            output += 'Default Patterns (builtin):\n';
            DEFAULT_MEMORY_PATTERNS.forEach((p, i) => {
              output += `${i + 1}. [${p.type}] ${p.regex.source}\n   ${p.description}\n\n`;
            });

            if (customMemoryPatterns.length > 0) {
              output += 'Custom Patterns:\n';
              customMemoryPatterns.forEach((p, i) => {
                output += `${DEFAULT_MEMORY_PATTERNS.length + i + 1}. [${p.type}] ${p.regex.source}\n   ${p.description}\n\n`;
              });
            } else {
              output += 'No custom patterns defined.\n';
            }

            output += `Total patterns: ${DEFAULT_MEMORY_PATTERNS.length + customMemoryPatterns.length}`;
            return { content: [{ type: 'text', text: output }] };

          case 'add':
            if (!params.pattern || !params.type || !params.description) {
              return {
                content: [{ type: 'text', text: 'Error: pattern, type, and description are required for add action.' }],
                isError: true,
              };
            }

            try {
              const regex = new RegExp(params.pattern, 'i');
              const newPattern: MemoryIntentPattern = {
                regex,
                type: params.type as any,
                description: params.description,
              };

              customMemoryPatterns.push(newPattern);
              return {
                content: [{ type: 'text', text: `Added custom memory intent pattern:\n[${params.type}] ${params.pattern}\n${params.description}` }],
              };
            } catch (err: any) {
              return {
                content: [{ type: 'text', text: `Error: Invalid regex pattern: ${err.message}` }],
                isError: true,
              };
            }

          case 'remove':
            if (typeof params.id !== 'number') {
              return {
                content: [{ type: 'text', text: 'Error: id is required for remove action.' }],
                isError: true,
              };
            }

            const index = params.id - 1;
            if (index < DEFAULT_MEMORY_PATTERNS.length) {
              return {
                content: [{ type: 'text', text: 'Error: Cannot remove default patterns. Only custom patterns can be removed.' }],
                isError: true,
              };
            }

            const customIndex = index - DEFAULT_MEMORY_PATTERNS.length;
            if (customIndex < 0 || customIndex >= customMemoryPatterns.length) {
              return {
                content: [{ type: 'text', text: 'Error: Invalid pattern ID.' }],
                isError: true,
              };
            }

            const removed = customMemoryPatterns.splice(customIndex, 1)[0];
            return {
              content: [{ type: 'text', text: `Removed pattern: [${removed.type}] ${removed.description}` }],
            };

          case 'stats':
            let statsOutput = 'Memory Intent Detection Statistics\n\n';

            if (memoryIntentStats.size === 0) {
              statsOutput += 'No memory intents have been detected yet.';
            } else {
              const sorted = Array.from(memoryIntentStats.entries())
                .sort((a, b) => b[1].count - a[1].count);

              sorted.forEach(([pattern, stats]) => {
                const lastTriggered = new Date(stats.lastTriggered).toLocaleString();
                statsOutput += `${pattern}:\n`;
                statsOutput += `  Triggered: ${stats.count} times\n`;
                statsOutput += `  Last triggered: ${lastTriggered}\n\n`;
              });

              const totalTriggers = Array.from(memoryIntentStats.values())
                .reduce((sum, stats) => sum + stats.count, 0);
              statsOutput += `Total intent detections: ${totalTriggers}`;
            }

            return { content: [{ type: 'text', text: statsOutput }] };

          default:
            return {
              content: [{ type: 'text', text: 'Error: Invalid action. Use list, add, remove, or stats.' }],
              isError: true,
            };
        }
      } catch (err: any) {
        return { content: [{ type: 'text', text: `Memory intent tool error: ${err.message}` }], isError: true };
      }
    },
  });

  // [v8.1.1] Read version dynamically from package.json so the banner never
  // drifts from the published version again (was hardcoded to v7.0.0).
  let __pkgVersion = 'unknown';
  try {
    // Resolve package.json relative to the compiled module dir (dist/), so the
    // version resolves correctly regardless of cwd. Fall back through parents.
    const candidates = [
      path.join(__dirname, 'package.json'),
      path.join(__dirname, '..', 'package.json'),
    ];
    for (const c of candidates) {
      try { __pkgVersion = JSON.parse(fs.readFileSync(c, 'utf-8')).version || __pkgVersion; break; } catch { /* try next */ }
    }
  } catch { /* ignore */ }
  const __banner = `[memtap] Plugin v${__pkgVersion} "Deep Capture" registered: 15 tools + 6 hooks (capture, llm_io, artifact, onboard, dream, recall)`;
  logger.info?.(__banner) ?? console.log(__banner);

  // Test memory intent detection on startup
  const testMessages = [
    'merk dir das ich JavaScript mag',
    'remember that I prefer coffee over tea',
    'meine Adresse ist Musterstraße 123',
    'ab jetzt immer TypeScript verwenden',
    'das ist nur normaler text'
  ];

  if (getConfig(api).debug) {
    console.log('[memtap] Testing memory intent detection:');
    testMessages.forEach(msg => {
      const result = detectMemoryIntent(msg);
      console.log(`  "${msg}" → ${result.hasIntent ? `${result.intentType} (${result.matchedPattern})` : 'no intent'}`);
    });
  }

  // ── Tool: memtap_code (AST Code Analysis) ──────────────────────────────────
  // v8.0: Wraps /v1/code/* endpoints for AST-based symbol and call extraction.

  api.registerTool({
    name: 'memtap_code',
    description:
      'AST-based code analysis: list supported languages, get file stats, extract symbols, or trace call edges.',
    parameters: {
      type: 'object',
      additionalProperties: false,
      properties: {
        action: {
          type: 'string',
          enum: ['languages', 'stats', 'symbols', 'calls'],
          description: 'Action: languages (list supported), stats (file metrics), symbols (extract functions/classes/vars), calls (trace call graph edges)',
        },
        filePath: {
          type: 'string',
          description: 'Path to a local source file (for symbols/calls actions). Reads and sends content automatically.',
        },
        content: {
          type: 'string',
          description: 'Raw source code (alternative to filePath)',
        },
        language: {
          type: 'string',
          description: 'Language hint when using content instead of filePath (e.g. "typescript", "python")',
        },
      },
      required: ['action'],
    },
    async execute(_id: string, params: { action: string; filePath?: string; content?: string; language?: string }) {
      const cfg = getConfig(api);
      const base = baseUrl(cfg);

      try {
        switch (params.action) {
          case 'languages': {
            const data = await bbFetch(cfg, `${base}/code/languages`);
            const langs = data.languages || data;
            return { content: [{ type: 'text', text: `Supported languages:\n${Array.isArray(langs) ? langs.join(', ') : JSON.stringify(langs, null, 2)}` }] };
          }

          case 'stats': {
            const data = await bbFetch(cfg, `${base}/code/stats`);
            return { content: [{ type: 'text', text: `Code stats:\n${JSON.stringify(data, null, 2)}` }] };
          }

          case 'symbols': {
            let code = params.content || '';
            let lang = params.language || '';
            if (!code && params.filePath) {
              try {
                code = fs.readFileSync(params.filePath, 'utf-8');
                lang = lang || path.extname(params.filePath).replace('.', '');
              } catch (err: any) {
                return { content: [{ type: 'text', text: `Cannot read file: ${err.message}` }], isError: true };
              }
            }
            if (!code) {
              return { content: [{ type: 'text', text: 'Error: provide filePath or content for symbols extraction.' }], isError: true };
            }
            const data = await bbFetch(cfg, `${base}/code/symbols`, {
              method: 'POST',
              body: JSON.stringify({ content: code, language: lang }),
            });
            const symbols = data.symbols || data;
            if (Array.isArray(symbols) && symbols.length === 0) {
              return { content: [{ type: 'text', text: `No symbols found${params.filePath ? ` in ${params.filePath}` : ''}.` }] };
            }
            const lines = (Array.isArray(symbols) ? symbols : []).map((s: any) =>
              `  ${s.kind || 'symbol'} ${s.name}${s.line ? ` (line ${s.line})` : ''}${s.params ? `(${s.params})` : ''}`
            );
            return { content: [{ type: 'text', text: `Symbols${params.filePath ? ` in ${params.filePath}` : ''}:\n${lines.join('\n') || JSON.stringify(symbols, null, 2)}` }] };
          }

          case 'calls': {
            let code = params.content || '';
            let lang = params.language || '';
            if (!code && params.filePath) {
              try {
                code = fs.readFileSync(params.filePath, 'utf-8');
                lang = lang || path.extname(params.filePath).replace('.', '');
              } catch (err: any) {
                return { content: [{ type: 'text', text: `Cannot read file: ${err.message}` }], isError: true };
              }
            }
            if (!code) {
              return { content: [{ type: 'text', text: 'Error: provide filePath or content for call graph extraction.' }], isError: true };
            }
            const data = await bbFetch(cfg, `${base}/code/calls`, {
              method: 'POST',
              body: JSON.stringify({ content: code, language: lang }),
            });
            const edges = data.edges || data.calls || data;
            if (Array.isArray(edges) && edges.length === 0) {
              return { content: [{ type: 'text', text: `No call edges found${params.filePath ? ` in ${params.filePath}` : ''}.` }] };
            }
            const lines = (Array.isArray(edges) ? edges : []).map((e: any) =>
              `  ${e.caller || e.from} → ${e.callee || e.to}${e.line ? ` (line ${e.line})` : ''}`
            );
            return { content: [{ type: 'text', text: `Call graph${params.filePath ? ` for ${params.filePath}` : ''}:\n${lines.join('\n') || JSON.stringify(edges, null, 2)}` }] };
          }

          default:
            return { content: [{ type: 'text', text: `Unknown code action: ${params.action}. Use: languages, stats, symbols, calls` }], isError: true };
        }
      } catch (err: any) {
        return { content: [{ type: 'text', text: `memtap_code error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_transcribe (Audio/Video Transcription) ───────────────────
  // v8.0: Wraps POST /v1/transcribe — sends audio/video to server for transcription.

  const TRANSCRIBE_SUPPORTED_EXTS = new Set(['mp3', 'm4a', 'wav', 'flac', 'ogg', 'webm', 'mp4', 'mov', 'avi', 'mkv']);
  const TRANSCRIBE_MAX_BYTES = 25 * 1024 * 1024; // 25 MB

  api.registerTool({
    name: 'memtap_transcribe',
    description:
      'Transcribe an audio or video file to text. Supports mp3, m4a, wav, flac, ogg, webm, mp4, mov, avi, mkv (max 25 MB).',
    parameters: {
      type: 'object',
      additionalProperties: false,
      properties: {
        filePath: {
          type: 'string',
          description: 'Path to a local audio/video file to transcribe',
        },
        base64Data: {
          type: 'string',
          description: 'Base64-encoded audio/video data (alternative to filePath)',
        },
        filename: {
          type: 'string',
          description: 'Filename when using base64Data (needed for format detection)',
        },
        language: {
          type: 'string',
          description: 'Language hint (ISO 639-1, e.g. "en", "de") — optional',
        },
        prompt: {
          type: 'string',
          description: 'Optional prompt to guide transcription (e.g. proper nouns, context)',
        },
      },
    },
    async execute(_id: string, params: { filePath?: string; base64Data?: string; filename?: string; language?: string; prompt?: string }) {
      const cfg = getConfig(api);
      const base = baseUrl(cfg);

      try {
        let b64: string;
        let fname: string;

        if (params.filePath) {
          // Read from disk
          const ext = path.extname(params.filePath).replace('.', '').toLowerCase();
          if (!TRANSCRIBE_SUPPORTED_EXTS.has(ext)) {
            return { content: [{ type: 'text', text: `Unsupported format ".${ext}". Supported: ${[...TRANSCRIBE_SUPPORTED_EXTS].join(', ')}` }], isError: true };
          }
          let stat: fs.Stats;
          try {
            stat = fs.statSync(params.filePath);
          } catch (err: any) {
            return { content: [{ type: 'text', text: `Cannot access file: ${err.message}` }], isError: true };
          }
          if (stat.size > TRANSCRIBE_MAX_BYTES) {
            return { content: [{ type: 'text', text: `File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Max is 25 MB.` }], isError: true };
          }
          const buf = fs.readFileSync(params.filePath);
          b64 = buf.toString('base64');
          fname = path.basename(params.filePath);
        } else if (params.base64Data && params.filename) {
          b64 = params.base64Data;
          fname = params.filename;
          const ext = path.extname(fname).replace('.', '').toLowerCase();
          if (!TRANSCRIBE_SUPPORTED_EXTS.has(ext)) {
            return { content: [{ type: 'text', text: `Unsupported format ".${ext}". Supported: ${[...TRANSCRIBE_SUPPORTED_EXTS].join(', ')}` }], isError: true };
          }
        } else {
          return { content: [{ type: 'text', text: 'Error: provide either filePath or both base64Data + filename.' }], isError: true };
        }

        const body: Record<string, string> = { data_base64: b64, filename: fname };
        if (params.language) body.language = params.language;
        if (params.prompt) body.prompt = params.prompt;

        const data = await bbFetch(cfg, `${base}/transcribe`, {
          method: 'POST',
          body: JSON.stringify(body),
        });

        const text = data.text || '';
        const lang = data.language || params.language || '?';
        const dur = data.durationSeconds != null ? `${data.durationSeconds}s` : '?';
        const cost = data.costUsd != null ? `$${data.costUsd.toFixed(4)}` : '';

        return { content: [{ type: 'text', text: `🎙️ Transcription of ${fname} (${lang}, ${dur}${cost ? ', ' + cost : ''}):\n\n${text}` }] };
      } catch (err: any) {
        return { content: [{ type: 'text', text: `memtap_transcribe error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_communities (Louvain Community Detection) ────────────────
  // v8.0: Wraps /v1/graph/communities endpoints.

  api.registerTool({
    name: 'memtap_communities',
    description:
      'Detect and browse memory graph communities (clusters of related entities). Actions: list, compute, get.',
    parameters: {
      type: 'object',
      additionalProperties: false,
      properties: {
        action: {
          type: 'string',
          enum: ['list', 'compute', 'get'],
          description: 'Action: list (show communities), compute (trigger Louvain recompute), get (specific community by id)',
        },
        communityId: {
          type: 'string',
          description: 'Community ID (for get action)',
        },
      },
      required: ['action'],
    },
    async execute(_id: string, params: { action: string; communityId?: string }) {
      const cfg = getConfig(api);
      const base = baseUrl(cfg);

      try {
        switch (params.action) {
          case 'list': {
            const data = await bbFetch(cfg, `${base}/graph/communities`);
            const communities = data.communities || data;
            if (Array.isArray(communities) && communities.length === 0) {
              return { content: [{ type: 'text', text: 'No communities detected yet. Run compute first.' }] };
            }
            const lines = (Array.isArray(communities) ? communities : []).map((c: any) =>
              `  #${c.id}: ${c.memberCount || c.members?.length || '?'} members${c.label ? ` — ${c.label}` : ''}`
            );
            return { content: [{ type: 'text', text: `Graph communities (${lines.length}):\n${lines.join('\n') || JSON.stringify(communities, null, 2)}` }] };
          }

          case 'compute': {
            const data = await bbFetch(cfg, `${base}/graph/communities/compute`, { method: 'POST' });
            const count = data.communityCount ?? data.communities?.length ?? '?';
            return { content: [{ type: 'text', text: `✅ Community detection complete. Found ${count} communities.${data.elapsed ? ` (${data.elapsed}ms)` : ''}` }] };
          }

          case 'get': {
            if (!params.communityId) {
              return { content: [{ type: 'text', text: 'Error: communityId required for get action.' }], isError: true };
            }
            const data = await bbFetch(cfg, `${base}/graph/communities/${encodeURIComponent(params.communityId)}`);
            const members = data.members || [];
            const memberList = members.map((m: any) => `  • ${m.name || m.id || m}`).join('\n');
            return { content: [{ type: 'text', text: `Community ${params.communityId}${data.label ? ` — ${data.label}` : ''}:\nMembers (${members.length}):\n${memberList || '(none)'}` }] };
          }

          default:
            return { content: [{ type: 'text', text: `Unknown communities action: ${params.action}. Use: list, compute, get` }], isError: true };
        }
      } catch (err: any) {
        return { content: [{ type: 'text', text: `memtap_communities error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_graph_hygiene (God-Node Detection) ───────────────────────
  // v8.0: Wraps GET /v1/graph/god-nodes for detecting graph pollution.

  api.registerTool({
    name: 'memtap_graph_hygiene',
    description:
      'Detect god-nodes (entities with suspiciously high connectivity) that may signal graph pollution.',
    parameters: {
      type: 'object',
      additionalProperties: false,
      properties: {
        threshold: {
          type: 'number',
          description: 'Minimum edge count to flag as god-node (default: server-defined)',
        },
        limit: {
          type: 'number',
          description: 'Max results to return (default: server-defined)',
        },
      },
    },
    async execute(_id: string, params: { threshold?: number; limit?: number }) {
      const cfg = getConfig(api);
      const base = baseUrl(cfg);

      try {
        const qs = new URLSearchParams();
        if (params.threshold != null) qs.set('threshold', String(params.threshold));
        if (params.limit != null) qs.set('limit', String(params.limit));
        const qsStr = qs.toString();
        const data = await bbFetch(cfg, `${base}/graph/god-nodes${qsStr ? '?' + qsStr : ''}`);
        const nodes = data.nodes || data.godNodes || data;

        if (Array.isArray(nodes) && nodes.length === 0) {
          return { content: [{ type: 'text', text: '✅ No god-nodes detected — graph looks healthy.' }] };
        }
        const lines = (Array.isArray(nodes) ? nodes : []).map((n: any) =>
          `  ⚠️ ${n.name || n.entity || n.id} — ${n.degree || n.edgeCount || '?'} edges${n.types ? ` (${n.types.join(', ')})` : ''}`
        );
        return { content: [{ type: 'text', text: `God-nodes detected (${lines.length}):\n${lines.join('\n') || JSON.stringify(nodes, null, 2)}\n\nThese over-connected entities may add noise. Consider merging or splitting them.` }] };
      } catch (err: any) {
        return { content: [{ type: 'text', text: `memtap_graph_hygiene error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_surprising (Surprising Connections) ──────────────────────
  // v8.0: Wraps GET /v1/graph/surprising-connections — embedding-close but graph-distant pairs.

  api.registerTool({
    name: 'memtap_surprising',
    description:
      'Discover surprising connections: entity pairs that are semantically similar but not connected in the graph.',
    parameters: {
      type: 'object',
      additionalProperties: false,
      properties: {
        limit: {
          type: 'number',
          description: 'Max pairs to return (default: server-defined)',
        },
      },
    },
    async execute(_id: string, params: { limit?: number }) {
      const cfg = getConfig(api);
      const base = baseUrl(cfg);

      try {
        const qs = params.limit ? `?limit=${params.limit}` : '';
        const data = await bbFetch(cfg, `${base}/graph/surprising-connections${qs}`);
        const pairs = data.pairs || data.connections || data;

        if (Array.isArray(pairs) && pairs.length === 0) {
          return { content: [{ type: 'text', text: 'No surprising connections found. Your graph is well-connected!' }] };
        }
        const lines = (Array.isArray(pairs) ? pairs : []).map((p: any) =>
          `  🔗 ${p.entityA || p.from} ↔ ${p.entityB || p.to} (similarity: ${p.similarity != null ? (p.similarity * 100).toFixed(0) + '%' : '?'})`
        );
        return { content: [{ type: 'text', text: `Surprising connections (${lines.length}):\n${lines.join('\n') || JSON.stringify(pairs, null, 2)}\n\nThese entities are semantically close but have no graph edge. Consider linking them.` }] };
      } catch (err: any) {
        return { content: [{ type: 'text', text: `memtap_surprising error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── Tool: memtap_graph_report (Full Graph Health Report) ──────────────────
  // v8.0: Wraps GET /v1/graph/report and /v1/graph/report/markdown.

  api.registerTool({
    name: 'memtap_graph_report',
    description:
      'Generate a comprehensive graph health report: entity/edge counts, community structure, density, god-nodes.',
    parameters: {
      type: 'object',
      additionalProperties: false,
      properties: {
        format: {
          type: 'string',
          enum: ['json', 'markdown'],
          description: 'Output format (default: markdown)',
        },
      },
    },
    async execute(_id: string, params: { format?: string }) {
      const cfg = getConfig(api);
      const base = baseUrl(cfg);
      const fmt = params.format || 'markdown';

      try {
        if (fmt === 'markdown') {
          const data = await bbFetch(cfg, `${base}/graph/report/markdown`);
          const md = data.markdown || data.report || (typeof data === 'string' ? data : JSON.stringify(data, null, 2));
          return { content: [{ type: 'text', text: md }] };
        } else {
          const data = await bbFetch(cfg, `${base}/graph/report`);
          return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
        }
      } catch (err: any) {
        return { content: [{ type: 'text', text: `memtap_graph_report error: ${err.message}` }], isError: true };
      }
    },
  });

  // ── OpenClaw Standard Memory Tool Aliases ──────────────────────────────────
  // OpenClaw expects memory_recall, memory_store, memory_forget for the memory slot.
  // These alias tools delegate to the existing memtap tools for compatibility.

  api.registerTool({
    name: 'memory_recall',
    description: 'Search memories in the xmem knowledge graph. Alias for memtap_recall.',
    parameters: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Search query' },
        limit: { type: 'number', description: 'Max results (default 10)' },
      },
      required: ['query'],
    },
    async execute(_id: string, params: any) {
      const cfg = getConfig(api);
      try {
        const res = await bbFetch(cfg, `${baseUrl(cfg)}/recall?q=${encodeURIComponent(params.query)}&limit=${params.limit || 10}&agent=${agentId(cfg, api)}`);
        const data = await res.json();
        return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
      } catch (err: any) {
        return { content: [{ type: 'text', text: `Recall failed: ${err.message}` }], isError: true };
      }
    },
  });

  api.registerTool({
    name: 'memory_store',
    description: 'Store a memory in the xmem knowledge graph. Alias for memtap_remember.',
    parameters: {
      type: 'object',
      properties: {
        content: { type: 'string', description: 'Memory content to store' },
        tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
      },
      required: ['content'],
    },
    async execute(_id: string, params: any) {
      const cfg = getConfig(api);
      try {
        const res = await bbFetch(cfg, `${baseUrl(cfg)}/memories`, {
          method: 'POST',
          body: JSON.stringify({
            content: params.content,
            agent: agentId(cfg, api),
            type: 'fact',
            importance: 0.7,
            tags: params.tags || [],
            source: 'plugin:memory_store',
          }),
        });
        const data = await res.json();
        return { content: [{ type: 'text', text: `Memory stored: ${data.id || data._key || 'OK'}` }] };
      } catch (err: any) {
        return { content: [{ type: 'text', text: `Store failed: ${err.message}` }], isError: true };
      }
    },
  });

  api.registerTool({
    name: 'memory_forget',
    description: 'Delete a memory from the xmem knowledge graph.',
    parameters: {
      type: 'object',
      properties: {
        id: { type: 'string', description: 'Memory ID to delete' },
      },
      required: ['id'],
    },
    async execute(_id: string, params: any) {
      const cfg = getConfig(api);
      try {
        await bbFetch(cfg, `${baseUrl(cfg)}/memories/${params.id}`, { method: 'DELETE' });
        return { content: [{ type: 'text', text: `Memory ${params.id} deleted.` }] };
      } catch (err: any) {
        return { content: [{ type: 'text', text: `Delete failed: ${err.message}` }], isError: true };
      }
    },
  });

  api.registerTool({
    name: 'memory_search',
    description: 'Search memories by query. Alias for memory_recall.',
    parameters: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Search query' },
        limit: { type: 'number', description: 'Max results' },
      },
      required: ['query'],
    },
    async execute(_id: string, params: any) {
      const cfg = getConfig(api);
      try {
        const res = await bbFetch(cfg, `${baseUrl(cfg)}/recall?q=${encodeURIComponent(params.query)}&limit=${params.limit || 10}&agent=${agentId(cfg, api)}`);
        const data = await res.json();
        return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
      } catch (err: any) {
        return { content: [{ type: 'text', text: `Search failed: ${err.message}` }], isError: true };
      }
    },
  });
  }
});
