import {
  safeIngestMemoryById
} from "./chunk-QSJKGEID.js";
import {
  computeQualityReport,
  listQualitySnapshots,
  recordQualitySnapshot
} from "./chunk-YL23MKB3.js";
import {
  ensureDailyBackup,
  handlePromptHook,
  handleSessionEndHook,
  handleSessionStartHook,
  handleToolHook
} from "./chunk-MT552653.js";
import {
  createRecallMcpServer
} from "./chunk-G7YTYVRH.js";
import {
  graphQuery
} from "./chunk-AKODTKWM.js";
import {
  countEntities,
  countRelations,
  getEntity,
  listAllRelations,
  listEntities,
  listEntitiesForMemory,
  listMemoryIdsForEntity,
  neighborsOf
} from "./chunk-Z6RT5BUO.js";
import {
  bootstrapHistoryEmbeddings,
  compileContext,
  compileContextHybrid,
  computeMetrics,
  createActivityEvent,
  createHistorySnippet,
  createPolicy,
  endEvalSession,
  endSessionLifecycle,
  ensureRepoBootstrapped,
  evaluatePolicy,
  evaluateScannedMemory,
  findHistorySnippetByRepoKind,
  findHistorySnippetBySession,
  getSignalStats,
  incrementEvalCounter,
  inferRepoSlugFromPath,
  initDb,
  listActivityEvents,
  listActivitySessions,
  listHistorySnippets,
  listPendingApprovals,
  listPolicies,
  pruneMemories,
  recordSessionLifecycleEvent,
  recordSignal,
  recordTestSignals,
  removeHistoryFtsRow,
  removeHistoryVecRow,
  requestApproval,
  resolveApproval,
  runTests,
  scanAndStore,
  startEvalSession,
  startSessionLifecycle,
  syncHistoryFtsIndex,
  updateHistorySnippet,
  verifyHistoryEmbeddings,
  writeRepoContextArtifact
} from "./chunk-MCR3OGUV.js";
import {
  DEFAULT_ENQUEUE_CONFIG,
  autoResolveContradictions,
  computeAllHealthScores,
  computeHealthScore,
  detectContradictions,
  detectCorrections,
  dispatchPendingTasks,
  enqueueMaintenanceTasks,
  getAuditTrail,
  getRecentAudit,
  getRepoQualityProfile,
  listContradictions,
  processCorrection,
  processReviewFeedback,
  recordAudit,
  recordAuditWithSnapshot,
  resolveContradiction,
  rollbackMemory,
  runDeterministicCleanup
} from "./chunk-BMWLHPLS.js";
import {
  confirmMemory,
  getMemory,
  promoteMemory,
  queryMemories,
  recordFeedback,
  rejectMemory,
  statusFromConfidence
} from "./chunk-UBPGOOAN.js";
import "./chunk-37K4DPRI.js";
import {
  bootstrapEmbeddings,
  ensureEmbeddingProviderReady,
  getEmbeddingModelInfo,
  loadEmbeddingConfigFromEnv,
  queueMemoryEmbeddingSync,
  rebuildEmbeddingIndex,
  verifyEmbeddings
} from "./chunk-JRIM7VBY.js";
import {
  activityEvents,
  eq,
  feedbackEvents,
  historySnippets,
  implicitSignals,
  lt,
  memories
} from "./chunk-QDA5M3AJ.js";
import {
  hasProviderConfigured,
  init_keychain
} from "./chunk-DNFKAHS6.js";
import "./chunk-4CV4JOE5.js";

// src/daemon.ts
import { createServer as createServer2 } from "http";
import { createRequire } from "module";

// src/daemon/events.ts
var handlers = /* @__PURE__ */ new Map();
var anyHandlers = /* @__PURE__ */ new Set();
function onAny(handler) {
  anyHandlers.add(handler);
  return () => {
    anyHandlers.delete(handler);
  };
}
function emit(name, payload) {
  const envelope = {
    name,
    payload,
    ts: (/* @__PURE__ */ new Date()).toISOString()
  };
  const set = handlers.get(name);
  if (set) {
    for (const h of set) {
      try {
        h(envelope);
      } catch (err) {
        console.error(`[recall] event handler for ${name} threw:`, err);
      }
    }
  }
  for (const h of anyHandlers) {
    try {
      h(envelope);
    } catch (err) {
      console.error(`[recall] any-event handler threw:`, err);
    }
  }
}

// src/webui/server.ts
import { createServer } from "http";
import { createReadStream, existsSync, statSync } from "fs";
import { extname, join, normalize, resolve, sep } from "path";
import { fileURLToPath } from "url";

// src/webui/ws.ts
import { createHash } from "crypto";
var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
var clients = /* @__PURE__ */ new Set();
var pingTimer = null;
var busUnsubscribe = null;
function isLocalRequest(req) {
  const addr = req.socket.remoteAddress ?? "";
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1" || addr === "localhost";
}
function handleUpgrade(req, socket) {
  if (!isLocalRequest(req)) {
    socket.destroy();
    return;
  }
  const key = req.headers["sec-websocket-key"];
  if (typeof key !== "string") {
    socket.destroy();
    return;
  }
  const accept = createHash("sha1").update(key + WS_GUID).digest("base64");
  socket.write(
    [
      "HTTP/1.1 101 Switching Protocols",
      "Upgrade: websocket",
      "Connection: Upgrade",
      `Sec-WebSocket-Accept: ${accept}`,
      "\r\n"
    ].join("\r\n")
  );
  const client = { socket, alive: true };
  clients.add(client);
  socket.on("close", () => {
    clients.delete(client);
  });
  socket.on("error", () => {
    clients.delete(client);
    socket.destroy();
  });
  socket.on("data", (chunk) => {
    if (chunk.length < 2) return;
    const opcode = chunk[0] & 15;
    if (opcode === 8) {
      writeFrame(socket, 8, Buffer.alloc(0));
      socket.end();
      clients.delete(client);
    } else if (opcode === 10) {
      client.alive = true;
    }
  });
  writeText(
    socket,
    JSON.stringify({ name: "hello", payload: { version: 1 }, ts: (/* @__PURE__ */ new Date()).toISOString() })
  );
  ensurePingLoop();
  ensureBusBridge();
}
function ensureBusBridge() {
  if (busUnsubscribe) return;
  busUnsubscribe = onAny((envelope) => {
    if (clients.size === 0) return;
    const json = JSON.stringify(envelope);
    for (const c of clients) {
      writeText(c.socket, json);
    }
  });
}
function ensurePingLoop() {
  if (pingTimer) return;
  pingTimer = setInterval(() => {
    if (clients.size === 0) return;
    for (const c of clients) {
      if (!c.alive) {
        c.socket.destroy();
        clients.delete(c);
        continue;
      }
      c.alive = false;
      try {
        writeFrame(c.socket, 9, Buffer.alloc(0));
      } catch {
        clients.delete(c);
      }
    }
  }, 3e4);
  pingTimer.unref?.();
}
function writeText(socket, text) {
  try {
    writeFrame(socket, 1, Buffer.from(text, "utf8"));
  } catch {
  }
}
function writeFrame(socket, opcode, payload) {
  const len = payload.length;
  let header;
  if (len < 126) {
    header = Buffer.alloc(2);
    header[0] = 128 | opcode & 15;
    header[1] = len;
  } else if (len < 65536) {
    header = Buffer.alloc(4);
    header[0] = 128 | opcode & 15;
    header[1] = 126;
    header.writeUInt16BE(len, 2);
  } else {
    header = Buffer.alloc(10);
    header[0] = 128 | opcode & 15;
    header[1] = 127;
    header.writeBigUInt64BE(BigInt(len), 2);
  }
  socket.write(Buffer.concat([header, payload]));
}
function clientCount() {
  return clients.size;
}
function shutdown() {
  if (pingTimer) {
    clearInterval(pingTimer);
    pingTimer = null;
  }
  busUnsubscribe?.();
  busUnsubscribe = null;
  for (const c of clients) {
    try {
      writeFrame(c.socket, 8, Buffer.alloc(0));
      c.socket.end();
    } catch {
    }
  }
  clients.clear();
}

// src/webui/server.ts
var MIME = {
  ".html": "text/html; charset=utf-8",
  ".js": "text/javascript; charset=utf-8",
  ".mjs": "text/javascript; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".svg": "image/svg+xml",
  ".png": "image/png",
  ".jpg": "image/jpeg",
  ".jpeg": "image/jpeg",
  ".webp": "image/webp",
  ".ico": "image/x-icon",
  ".woff2": "font/woff2",
  ".woff": "font/woff",
  ".map": "application/json"
};
var active = null;
function defaultDistDir() {
  const here = fileURLToPath(new URL(".", import.meta.url));
  let dir = here;
  for (let i = 0; i < 8; i++) {
    if (existsSync(join(dir, "package.json"))) {
      return join(dir, "dist", "webui");
    }
    const parent = resolve(dir, "..");
    if (parent === dir) break;
    dir = parent;
  }
  return join(here, "..", "..", "dist", "webui");
}
function getStatus() {
  if (!active) {
    const distDir = defaultDistDir();
    return {
      running: false,
      port: null,
      host: null,
      url: null,
      client_count: 0,
      dist_dir: distDir,
      dist_exists: existsSync(distDir),
      started_at: null
    };
  }
  return {
    running: true,
    port: active.port,
    host: active.host,
    url: `http://${active.host}:${active.port}`,
    client_count: clientCount(),
    dist_dir: active.distDir,
    dist_exists: existsSync(active.distDir),
    started_at: active.startedAt
  };
}
async function start(options = {}) {
  if (active) return getStatus();
  const port = options.port ?? parseInt(process.env.RECALL_WEBUI_PORT ?? "7891", 10);
  const host = options.host ?? "127.0.0.1";
  const distDir = options.distDir ?? defaultDistDir();
  const server2 = createServer((req, res) => handleHttp(req, res, distDir));
  server2.on("upgrade", (req, socket) => {
    if (req.url === "/ws") {
      handleUpgrade(req, socket);
    } else {
      socket.destroy();
    }
  });
  await new Promise((resolve2, reject) => {
    server2.once("error", reject);
    server2.listen(port, host, () => {
      server2.off("error", reject);
      resolve2();
    });
  });
  active = { server: server2, port, host, distDir, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
  return getStatus();
}
async function stop() {
  if (!active) return getStatus();
  const a = active;
  active = null;
  shutdown();
  await new Promise((resolve2) => {
    a.server.close(() => resolve2());
  });
  return getStatus();
}
function isRunning() {
  return active !== null;
}
function handleHttp(req, res, distDir) {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Cache-Control", "no-cache");
  if (req.method === "GET" && req.url === "/__webui/status") {
    res.statusCode = 200;
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify(getStatus()));
    return;
  }
  if (!isLocalRequest(req)) {
    res.statusCode = 403;
    res.end();
    return;
  }
  if (req.method !== "GET" && req.method !== "HEAD") {
    res.statusCode = 405;
    res.end();
    return;
  }
  const rawPath = (req.url ?? "/").split("?")[0];
  const decoded = safeDecode(rawPath);
  if (decoded === null) {
    res.statusCode = 400;
    res.end();
    return;
  }
  const relative = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
  const fullPath = normalize(join(distDir, relative));
  if (!fullPath.startsWith(distDir + sep) && fullPath !== distDir) {
    res.statusCode = 403;
    res.end();
    return;
  }
  if (!existsSync(fullPath) || !statSync(fullPath).isFile()) {
    const indexPath = join(distDir, "index.html");
    if (existsSync(indexPath)) {
      serveFile(res, indexPath);
    } else {
      res.statusCode = 503;
      res.setHeader("Content-Type", "text/html; charset=utf-8");
      res.end(missingBundleHtml(distDir));
    }
    return;
  }
  serveFile(res, fullPath);
}
function safeDecode(p) {
  try {
    return decodeURIComponent(p);
  } catch {
    return null;
  }
}
function serveFile(res, file) {
  const mime = MIME[extname(file).toLowerCase()] ?? "application/octet-stream";
  res.statusCode = 200;
  res.setHeader("Content-Type", mime);
  createReadStream(file).pipe(res);
}
function missingBundleHtml(distDir) {
  return `<!doctype html>
<html><head><meta charset="utf-8"><title>Recall WebUI \u2014 bundle missing</title>
<style>body{font-family:system-ui;max-width:560px;margin:80px auto;color:#222;padding:0 16px}code{background:#f4f4f4;padding:2px 4px;border-radius:3px}</style>
</head><body>
<h1>WebUI bundle not built</h1>
<p>The Recall daemon is running and the WebUI port is open, but the static SPA bundle was not found at:</p>
<pre><code>${escapeHtml(distDir)}</code></pre>
<p>Run <code>npm run webui:build</code> (or the wrapping <code>npm run build</code>) and reload this page.</p>
</body></html>`;
}
function escapeHtml(s) {
  return s.replace(
    /[&<>"']/g,
    (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c] ?? c
  );
}

// src/daemon.ts
import { spawn } from "child_process";

// src/maintenance/lifecycle.ts
import { statSync as statSync2 } from "fs";
var DAY_MS = 864e5;
function loadMaintenanceConfigFromEnv() {
  return {
    enabled: process.env.RECALL_MAINTENANCE_ENABLED !== "false",
    interval_seconds: parseInt(process.env.RECALL_MAINTENANCE_INTERVAL_SECONDS ?? "300", 10),
    stale_days: parseInt(process.env.RECALL_MAINTENANCE_STALE_DAYS ?? "90", 10),
    min_health_score: parseFloat(process.env.RECALL_MAINTENANCE_MIN_HEALTH_SCORE ?? "0.2"),
    activity_retention_days: parseInt(process.env.RECALL_ACTIVITY_RETENTION_DAYS ?? "90", 10),
    feedback_retention_days: parseInt(process.env.RECALL_FEEDBACK_RETENTION_DAYS ?? "180", 10),
    signal_retention_days: parseInt(process.env.RECALL_SIGNAL_RETENTION_DAYS ?? "180", 10),
    rejected_retention_days: parseInt(process.env.RECALL_REJECTED_RETENTION_DAYS ?? "90", 10),
    transient_retention_days: parseInt(process.env.RECALL_TRANSIENT_RETENTION_DAYS ?? "7", 10),
    history_session_retention_days: parseInt(process.env.RECALL_HISTORY_SESSION_RETENTION_DAYS ?? "30", 10),
    sqlite_analyze_enabled: process.env.RECALL_SQLITE_ANALYZE_ENABLED !== "false",
    sqlite_optimize_enabled: process.env.RECALL_SQLITE_OPTIMIZE_ENABLED !== "false",
    sqlite_wal_checkpoint_enabled: process.env.RECALL_SQLITE_CHECKPOINT_ENABLED !== "false",
    sqlite_wal_truncate_bytes: parseInt(
      process.env.RECALL_SQLITE_WAL_TRUNCATE_BYTES ?? String(32 * 1024 * 1024),
      10
    ),
    sqlite_vacuum_enabled: process.env.RECALL_SQLITE_VACUUM_ENABLED === "true",
    sqlite_vacuum_min_free_pages: parseInt(process.env.RECALL_SQLITE_VACUUM_MIN_FREE_PAGES ?? "100", 10),
    sqlite_vacuum_min_free_ratio: parseFloat(process.env.RECALL_SQLITE_VACUUM_MIN_FREE_RATIO ?? "0.1"),
    llm_tasks_enabled: process.env.RECALL_MAINTENANCE_LLM_DISABLED !== "true",
    llm_task_config: {
      max_pending: parseInt(process.env.RECALL_MAINTENANCE_MAX_PENDING ?? String(DEFAULT_ENQUEUE_CONFIG.max_pending), 10),
      max_per_kind: parseInt(process.env.RECALL_MAINTENANCE_MAX_PER_KIND ?? String(DEFAULT_ENQUEUE_CONFIG.max_per_kind), 10),
      refine_min_repetition: parseInt(process.env.RECALL_MAINTENANCE_REFINE_MIN_REPETITION ?? String(DEFAULT_ENQUEUE_CONFIG.refine_min_repetition), 10),
      summary_max_age_days: parseInt(process.env.RECALL_MAINTENANCE_SUMMARY_MAX_AGE_DAYS ?? String(DEFAULT_ENQUEUE_CONFIG.summary_max_age_days), 10),
      merge_similarity_threshold: parseFloat(process.env.RECALL_MAINTENANCE_MERGE_SIMILARITY_THRESHOLD ?? String(DEFAULT_ENQUEUE_CONFIG.merge_similarity_threshold)),
      session_min_activity_events: parseInt(process.env.RECALL_MAINTENANCE_SESSION_MIN_EVENTS ?? String(DEFAULT_ENQUEUE_CONFIG.session_min_activity_events), 10),
      repo_synthesis_min_memories: parseInt(process.env.RECALL_MAINTENANCE_REPO_SYNTHESIS_MIN_MEMORIES ?? String(DEFAULT_ENQUEUE_CONFIG.repo_synthesis_min_memories), 10),
      repo_synthesis_refresh_days: parseInt(process.env.RECALL_MAINTENANCE_REPO_SYNTHESIS_REFRESH_DAYS ?? String(DEFAULT_ENQUEUE_CONFIG.repo_synthesis_refresh_days), 10)
    }
  };
}
async function runMaintenanceCycle(db2, config = loadMaintenanceConfigFromEnv()) {
  const prune = pruneMemories(db2, {
    stale_days: config.stale_days,
    min_health_score: config.min_health_score,
    rejected_retention_days: config.rejected_retention_days,
    transient_retention_days: config.transient_retention_days
  });
  const scannedMemoryCleanup = reconcileScannedMemories(db2);
  const candidates_promoted = promoteRepetitionCandidates(db2);
  const activity_pruned = pruneOldActivityEvents(db2, config.activity_retention_days);
  const feedback_pruned = pruneOldFeedbackEvents(db2, config.feedback_retention_days);
  const signals_pruned = pruneOldImplicitSignals(db2, config.signal_retention_days);
  const sqliteMaintenance = runSqliteMaintenance(db2, config);
  let embeddings_refreshed = 0;
  let vector_rows_rebuilt = 0;
  let lexical_rows_rebuilt = 0;
  let embedding_stale = 0;
  let vector_drift = 0;
  let lexical_drift = 0;
  const history_snippets_created = rollupSessionHistory(db2);
  const history_summaries_created = summarizeHistorySnippets(db2);
  const history_session_deleted = cleanupSessionHistory(db2, config.history_session_retention_days);
  let history_embeddings_refreshed = 0;
  let history_vector_drift = 0;
  let history_lexical_drift = 0;
  const embeddingConfig = loadEmbeddingConfigFromEnv();
  if (embeddingConfig) {
    const verify = verifyEmbeddings(db2, embeddingConfig);
    embedding_stale = verify.stale;
    vector_drift = verify.index_drift;
    lexical_drift = verify.lexical_drift;
    if (embedding_stale > 0) {
      embeddings_refreshed = await bootstrapEmbeddings(db2, embeddingConfig);
    }
    if (vector_drift !== 0 || lexical_drift !== 0) {
      const rebuilt = rebuildEmbeddingIndex(db2, embeddingConfig);
      vector_rows_rebuilt = rebuilt.vector_rows;
      lexical_rows_rebuilt = rebuilt.lexical_rows;
    }
    const historyVerify = verifyHistoryEmbeddings(db2, embeddingConfig);
    history_vector_drift = historyVerify.index_drift;
    history_lexical_drift = historyVerify.lexical_drift;
    if (historyVerify.stale > 0 || history_snippets_created > 0 || history_summaries_created > 0 || history_session_deleted > 0) {
      history_embeddings_refreshed = await bootstrapHistoryEmbeddings(db2, embeddingConfig);
    }
  }
  const tasks = config.llm_tasks_enabled ? await enqueueMaintenanceTasks(db2, config.llm_task_config) : { tasks_enqueued: 0, per_kind: {}, expired_leases_swept: 0, dropped_over_cap: 0, expired_pending_tasks: 0 };
  return {
    prune_total: prune.total,
    stale_rejected: prune.stale_rejected.length,
    rejected_pruned: prune.rejected_pruned.length,
    transient_pruned: prune.transient_pruned.length,
    unhealthy_demoted: prune.unhealthy_demoted.length,
    scanned_memories_normalized: scannedMemoryCleanup.normalized,
    scanned_memories_demoted: scannedMemoryCleanup.demoted,
    scanned_memories_rejected: scannedMemoryCleanup.rejected,
    activity_pruned,
    feedback_pruned,
    signals_pruned,
    embeddings_refreshed,
    vector_rows_rebuilt,
    lexical_rows_rebuilt,
    embedding_stale,
    vector_drift,
    lexical_drift,
    history_snippets_created,
    history_summaries_created,
    history_session_deleted,
    history_embeddings_refreshed,
    history_vector_drift,
    history_lexical_drift,
    candidates_promoted,
    sqlite_analyze_ran: sqliteMaintenance.analyze_ran,
    sqlite_optimize_ran: sqliteMaintenance.optimize_ran,
    sqlite_checkpoint_ran: sqliteMaintenance.checkpoint_ran,
    sqlite_vacuum_ran: sqliteMaintenance.vacuum_ran,
    sqlite_page_count: sqliteMaintenance.page_count,
    sqlite_freelist_count: sqliteMaintenance.freelist_count,
    maintenance_tasks_enqueued: tasks.tasks_enqueued,
    maintenance_leases_swept: tasks.expired_leases_swept,
    maintenance_tasks_dropped: tasks.dropped_over_cap,
    maintenance_tasks_expired: tasks.expired_pending_tasks
  };
}
function promoteRepetitionCandidates(db2) {
  const candidates = queryMemories(db2, { status: "candidate" });
  let promoted = 0;
  for (const candidate of candidates) {
    if (!candidate.repo) continue;
    const profile = getRepoQualityProfile(db2, candidate.repo);
    if (candidate.repetition_count < profile.repeat_sessions_required) continue;
    const before = getMemory(db2, candidate.id);
    if (!before || before.status !== "candidate") continue;
    const ok = promoteMemory(db2, candidate.id, "repeat_correction");
    if (!ok) continue;
    const after = getMemory(db2, candidate.id);
    recordAuditWithSnapshot(
      db2,
      candidate.id,
      "promoted",
      "system",
      `repetition:${candidate.repetition_count}`,
      before,
      after ?? null
    );
    promoted += 1;
  }
  return promoted;
}
function reconcileScannedMemories(db2) {
  const scanned = queryMemories(db2, {}).filter(
    (memory) => memory.status !== "rejected" && (memory.source === "repo_scan" || memory.source === "config_parse")
  );
  let normalized = 0;
  let demoted = 0;
  let rejected = 0;
  for (const memory of scanned) {
    const evaluated = evaluateScannedMemory({
      text: memory.text,
      type: memory.type,
      source: memory.source,
      confidence: memory.confidence
    });
    if (evaluated.action === "reject") {
      if (memory.status !== "rejected") {
        rejectMemory(db2, memory.id);
        rejected += 1;
      }
      continue;
    }
    const nextStatus = statusFromConfidence(evaluated.confidence);
    const updates = {};
    if (memory.text !== evaluated.text) {
      updates.text = evaluated.text;
      normalized += 1;
    }
    if (memory.confidence !== evaluated.confidence) {
      updates.confidence = evaluated.confidence;
    }
    if (memory.status !== nextStatus) {
      updates.status = nextStatus;
      if (memory.status === "active" && nextStatus === "candidate") {
        demoted += 1;
      }
    }
    if (Object.keys(updates).length === 0) continue;
    updates.updated_at = (/* @__PURE__ */ new Date()).toISOString();
    db2.update(memories).set(updates).where(eq(memories.id, memory.id)).run();
    queueMemoryEmbeddingSync(db2, memory.id);
  }
  return { normalized, demoted, rejected };
}
function runSqliteMaintenance(db2, config) {
  const sqlite = db2.$client;
  const pageCount = Number(sqlite.pragma("page_count", { simple: true }) ?? 0);
  const freelistCount = Number(sqlite.pragma("freelist_count", { simple: true }) ?? 0);
  const freeRatio = pageCount > 0 ? freelistCount / pageCount : 0;
  let analyzeRan = false;
  let optimizeRan = false;
  let checkpointRan = false;
  let vacuumRan = false;
  if (config.sqlite_analyze_enabled) {
    sqlite.exec("ANALYZE;");
    analyzeRan = true;
  }
  if (config.sqlite_wal_checkpoint_enabled) {
    const mode = shouldTruncateWal(sqlite, config.sqlite_wal_truncate_bytes) ? "TRUNCATE" : "PASSIVE";
    sqlite.pragma(`wal_checkpoint(${mode})`);
    checkpointRan = true;
  }
  if (config.sqlite_optimize_enabled) {
    sqlite.pragma("optimize");
    optimizeRan = true;
  }
  if (config.sqlite_vacuum_enabled && freelistCount >= config.sqlite_vacuum_min_free_pages && freeRatio >= config.sqlite_vacuum_min_free_ratio) {
    sqlite.exec("VACUUM;");
    vacuumRan = true;
  }
  return {
    analyze_ran: analyzeRan,
    optimize_ran: optimizeRan,
    checkpoint_ran: checkpointRan,
    vacuum_ran: vacuumRan,
    page_count: pageCount,
    freelist_count: freelistCount
  };
}
function shouldTruncateWal(sqlite, thresholdBytes) {
  if (!Number.isFinite(thresholdBytes) || thresholdBytes <= 0) return false;
  try {
    return statSync2(`${sqlite.name}-wal`).size >= thresholdBytes;
  } catch {
    return false;
  }
}
function pruneOldActivityEvents(db2, retentionDays) {
  const cutoff = new Date(Date.now() - retentionDays * DAY_MS).toISOString();
  return db2.delete(activityEvents).where(lt(activityEvents.created_at, cutoff)).run().changes;
}
function pruneOldFeedbackEvents(db2, retentionDays) {
  const cutoff = new Date(Date.now() - retentionDays * DAY_MS).toISOString();
  return db2.delete(feedbackEvents).where(lt(feedbackEvents.timestamp, cutoff)).run().changes;
}
function pruneOldImplicitSignals(db2, retentionDays) {
  const cutoff = new Date(Date.now() - retentionDays * DAY_MS).toISOString();
  return db2.delete(implicitSignals).where(lt(implicitSignals.timestamp, cutoff)).run().changes;
}
function rollupSessionHistory(db2) {
  const sessionEnds = listActivityEvents(db2, { event_type: "session_end", limit: 500 });
  let createdOrUpdated = 0;
  for (const end of sessionEnds) {
    if (!end.session_id) continue;
    const existing = findHistorySnippetBySession(db2, end.session_id, "session_summary");
    const events = listActivityEvents(db2, { session_id: end.session_id }).sort((a, b) => a.created_at.localeCompare(b.created_at));
    if (events.length === 0) continue;
    const repo = end.repo ?? events.find((event) => event.repo)?.repo ?? null;
    const summary = summarizeSessionEvents(events);
    const sourceActivityIds = events.map((event) => event.id);
    if (existing) {
      if (existing.text !== summary) {
        updateHistorySnippet(db2, existing.id, {
          text: summary,
          source_activity_ids: sourceActivityIds
        });
        syncHistoryFtsIndex(db2, existing.id);
        createdOrUpdated++;
      }
      continue;
    }
    const id = createHistorySnippet(db2, {
      repo,
      session_id: end.session_id,
      kind: "session_summary",
      text: summary,
      source_activity_ids: sourceActivityIds
    });
    syncHistoryFtsIndex(db2, id);
    createdOrUpdated++;
  }
  return createdOrUpdated;
}
function summarizeHistorySnippets(db2) {
  const sessionSnippets = listHistorySnippets(db2, {
    kind: "session_summary",
    limit: 1e3
  });
  const byRepo = /* @__PURE__ */ new Map();
  for (const snippet of sessionSnippets) {
    if (!snippet.repo) continue;
    const bucket = byRepo.get(snippet.repo) ?? [];
    bucket.push(snippet);
    byRepo.set(snippet.repo, bucket);
  }
  let createdOrUpdated = 0;
  for (const [repo, snippets] of byRepo.entries()) {
    const aggregated = aggregateRepoHistory(repo, snippets);
    for (const item of aggregated) {
      if (!item.text) continue;
      const existing = findHistorySnippetByRepoKind(db2, repo, item.kind);
      if (existing) {
        if (existing.text !== item.text) {
          updateHistorySnippet(db2, existing.id, {
            text: item.text,
            source_activity_ids: item.source_activity_ids
          });
          syncHistoryFtsIndex(db2, existing.id);
          createdOrUpdated++;
        }
        continue;
      }
      const id = createHistorySnippet(db2, {
        repo,
        kind: item.kind,
        text: item.text,
        source_activity_ids: item.source_activity_ids
      });
      syncHistoryFtsIndex(db2, id);
      createdOrUpdated++;
    }
  }
  return createdOrUpdated;
}
function cleanupSessionHistory(db2, retentionDays) {
  const cutoff = new Date(Date.now() - retentionDays * DAY_MS).toISOString();
  const sessionSnippets = listHistorySnippets(db2, {
    kind: "session_summary",
    limit: 1e3
  });
  let deleted = 0;
  for (const snippet of sessionSnippets) {
    if (!snippet.repo) continue;
    if (snippet.created_at >= cutoff) continue;
    const hasRepoSummary = findHistorySnippetByRepoKind(db2, snippet.repo, "correction_summary") || findHistorySnippetByRepoKind(db2, snippet.repo, "decision_summary") || findHistorySnippetByRepoKind(db2, snippet.repo, "review_summary") || findHistorySnippetByRepoKind(db2, snippet.repo, "compile_summary");
    if (!hasRepoSummary) continue;
    removeHistoryFtsRow(db2, snippet.id);
    removeHistoryVecRow(db2, snippet.id);
    db2.delete(historySnippets).where(eq(historySnippets.id, snippet.id)).run();
    deleted++;
  }
  return deleted;
}
function summarizeSessionEvents(events) {
  const repo = events.find((event) => event.repo)?.repo ?? "unknown";
  const eventTypes = [...new Set(events.map((event) => event.event_type))];
  const corrections = events.filter((event) => event.event_type === "correction").map((event) => String(event.request.text ?? "")).filter(Boolean);
  const reviews = events.filter((event) => event.event_type === "review").map((event) => String(event.request.feedback ?? "")).filter(Boolean);
  const decisions = extractPromptDecisions(events);
  const compileEvents = events.filter((event) => event.event_type === "compile");
  const lines = [
    `Repo: ${repo}`,
    `Event types: ${eventTypes.join(", ")}`
  ];
  if (compileEvents.length > 0) {
    const latestCompile = compileEvents.at(-1);
    const included = Array.isArray(latestCompile?.result.included) ? latestCompile.result.included.length : 0;
    lines.push(`Latest compile included ${included} memories.`);
  }
  if (corrections.length > 0) {
    lines.push(`Corrections: ${corrections.slice(0, 3).join(" | ")}`);
  }
  if (reviews.length > 0) {
    lines.push(`Reviews: ${reviews.slice(0, 3).join(" | ")}`);
  }
  if (decisions.length > 0) {
    lines.push(`Decisions: ${decisions.slice(0, 5).join(" | ")}`);
  }
  return lines.join("\n");
}
function aggregateRepoHistory(repo, snippets) {
  const corrections = /* @__PURE__ */ new Map();
  const decisions = /* @__PURE__ */ new Map();
  const reviews = /* @__PURE__ */ new Map();
  let compileObservations = 0;
  let compileIncludedTotal = 0;
  const sourceActivityIds = /* @__PURE__ */ new Set();
  for (const snippet of snippets) {
    for (const id of snippet.source_activity_ids) {
      sourceActivityIds.add(id);
    }
    const lines = snippet.text.split("\n");
    const correctionsLine = lines.find((line) => line.startsWith("Corrections: "));
    if (correctionsLine) {
      for (const item of correctionsLine.replace("Corrections: ", "").split(" | ").filter(Boolean)) {
        corrections.set(item, (corrections.get(item) ?? 0) + 1);
      }
    }
    const reviewsLine = lines.find((line) => line.startsWith("Reviews: "));
    if (reviewsLine) {
      for (const item of reviewsLine.replace("Reviews: ", "").split(" | ").filter(Boolean)) {
        reviews.set(item, (reviews.get(item) ?? 0) + 1);
      }
    }
    const decisionsLine = lines.find((line) => line.startsWith("Decisions: "));
    if (decisionsLine) {
      for (const item of decisionsLine.replace("Decisions: ", "").split(" | ").filter(Boolean)) {
        decisions.set(item, (decisions.get(item) ?? 0) + 1);
      }
    }
    const compileLine = lines.find((line) => line.startsWith("Latest compile included "));
    if (compileLine) {
      compileObservations++;
      const match = compileLine.match(/included (\d+) memories/);
      if (match) compileIncludedTotal += parseInt(match[1], 10);
    }
  }
  return [
    {
      kind: "correction_summary",
      text: renderSummary(repo, "Frequent corrections", corrections),
      source_activity_ids: [...sourceActivityIds]
    },
    {
      kind: "review_summary",
      text: renderSummary(repo, "Frequent review guidance", reviews),
      source_activity_ids: [...sourceActivityIds]
    },
    {
      kind: "decision_summary",
      text: renderSummary(repo, "Frequent user decisions", decisions),
      source_activity_ids: [...sourceActivityIds]
    },
    {
      kind: "compile_summary",
      text: compileObservations > 0 ? [
        `Repo: ${repo}`,
        `Compile observations: ${compileObservations}`,
        `Average included memories: ${(compileIncludedTotal / compileObservations).toFixed(1)}`
      ].join("\n") : "",
      source_activity_ids: [...sourceActivityIds]
    }
  ];
}
function renderSummary(repo, heading, counts) {
  if (counts.size === 0) return "";
  const top = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([text, count]) => `- (${count}) ${text}`);
  return [
    `Repo: ${repo}`,
    `${heading}:`,
    ...top
  ].join("\n");
}
function extractPromptDecisions(events) {
  const seen = /* @__PURE__ */ new Set();
  const decisions = [];
  for (const event of events) {
    if (event.event_type !== "session_event") continue;
    if (event.request.name !== "prompt_submitted") continue;
    const text = String(event.result.text ?? "").trim();
    if (!text) continue;
    const durable = detectCorrections(text).filter((match) => match.type === "decision").map((match) => match.text);
    for (const item of durable) {
      addUniqueDecision(seen, decisions, item);
    }
    const directive = extractDurablePromptDirective(text);
    if (directive) {
      addUniqueDecision(seen, decisions, directive);
    }
  }
  return decisions;
}
function addUniqueDecision(seen, decisions, text) {
  const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
  if (!normalized || seen.has(normalized)) return;
  seen.add(normalized);
  decisions.push(text);
}
function extractDurablePromptDirective(text) {
  const compact = text.replace(/\s+/g, " ").replace(/^[-*]\s+/, "").trim();
  const phaseDirective = /\b(?:do\s+phase|phase\s+\d+)\b/i.test(compact);
  if (!phaseDirective && compact.length < 14 || compact.length > 240) return null;
  if (!DURABLE_PROMPT_DIRECTIVE.test(compact)) return null;
  if (!DURABLE_PROMPT_DOMAIN.test(compact)) return null;
  if (/^(?:can|could|would|should|why|what|how)\b.*\?$/i.test(compact)) return null;
  return `User direction: ${compact.replace(/[.!?]+$/u, "")}.`;
}
var DURABLE_PROMPT_DIRECTIVE = /\b(?:let's|lets|let us|we should|we need to|make|improve|add|change|implement|ship|do phase|phase\s+\d+|production ready|open source|self[- ]healing|self healing)\b/i;
var DURABLE_PROMPT_DOMAIN = /\b(?:recall|memory|memories|dedupe|duplicate|question|prompt|capture|history|summary|quality|maintenance|cleanup|doctor|daemon|dispatcher|migration|test|phase|production|open source|self[- ]healing|self healing)\b/i;

// src/maintenance/logging.ts
function maintenanceChangeCount(result) {
  return result.prune_total + result.scanned_memories_normalized + result.scanned_memories_demoted + result.scanned_memories_rejected + result.activity_pruned + result.feedback_pruned + result.signals_pruned + result.embeddings_refreshed + result.vector_rows_rebuilt + result.lexical_rows_rebuilt + result.history_snippets_created + result.history_embeddings_refreshed;
}
function shouldLogMaintenance(result) {
  return maintenanceChangeCount(result) > 0 || result.vector_drift !== 0 || result.lexical_drift !== 0 || result.embedding_stale > 0 || result.history_vector_drift !== 0 || result.history_lexical_drift !== 0;
}
function formatMaintenanceSummary(result) {
  return `[recall] maintenance prune=${result.prune_total} scanned(normalized=${result.scanned_memories_normalized},demoted=${result.scanned_memories_demoted},rejected=${result.scanned_memories_rejected}) activity=${result.activity_pruned} feedback=${result.feedback_pruned} signals=${result.signals_pruned} refreshed=${result.embeddings_refreshed} rebuilt(vec=${result.vector_rows_rebuilt},fts=${result.lexical_rows_rebuilt}) drift(vec=${result.vector_drift},fts=${result.lexical_drift}) stale=${result.embedding_stale} history(created=${result.history_snippets_created},refreshed=${result.history_embeddings_refreshed},drift_vec=${result.history_vector_drift},drift_fts=${result.history_lexical_drift})`;
}

// src/daemon.ts
init_keychain();

// src/mcp/http.ts
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
async function handleRecallMcpHttpRequest(req, res, db2) {
  if (req.method !== "POST") {
    return sendJsonRpcError(res, 405, -32e3, "Method not allowed");
  }
  const mcpServer = createRecallMcpServer(db2);
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: void 0,
    enableJsonResponse: true
  });
  try {
    await mcpServer.connect(transport);
    await transport.handleRequest(req, res);
  } catch (error) {
    if (!res.headersSent) {
      sendJsonRpcError(
        res,
        500,
        -32603,
        error instanceof Error ? error.message : "Internal server error"
      );
    }
  } finally {
    await transport.close().catch(() => {
    });
    await mcpServer.close().catch(() => {
    });
  }
}
function sendJsonRpcError(res, status, code, message) {
  res.statusCode = status;
  res.setHeader("Content-Type", "application/json");
  res.end(JSON.stringify({
    jsonrpc: "2.0",
    error: { code, message },
    id: null
  }));
}

// src/daemon.ts
function safeIngestMemory(memoryId) {
  safeIngestMemoryById(db, memoryId);
}
var db;
var PORT = parseInt(process.env.RECALL_PORT ?? "7890", 10);
var requireFromHere = createRequire(import.meta.url);
var pkg = requireFromHere("../package.json");
var maintenanceConfig = loadMaintenanceConfigFromEnv();
var maintenanceRunning = false;
var dispatcherConfig = {
  enabled: process.env.RECALL_DISPATCHER_ENABLED !== "false",
  intervalSeconds: parseInt(process.env.RECALL_DISPATCHER_INTERVAL_SECONDS ?? "86400", 10),
  maxTasksPerRun: parseInt(process.env.RECALL_DISPATCHER_MAX_TASKS_PER_RUN ?? "5", 10)
};
var dispatcherRunning = false;
var cleanupConfig = {
  enabled: process.env.RECALL_CLEANUP_ENABLED !== "false",
  intervalSeconds: parseInt(process.env.RECALL_CLEANUP_INTERVAL_SECONDS ?? "86400", 10)
};
var cleanupRunning = false;
var qualitySnapshotConfig = {
  enabled: process.env.RECALL_QUALITY_SNAPSHOT_ENABLED !== "false",
  intervalSeconds: parseInt(process.env.RECALL_QUALITY_SNAPSHOT_INTERVAL_SECONDS ?? "604800", 10)
};
var qualitySnapshotRunning = false;
function parseBody(req) {
  return new Promise((resolve2, reject) => {
    const chunks = [];
    req.on("data", (c) => chunks.push(c));
    req.on("end", () => {
      try {
        resolve2(JSON.parse(Buffer.concat(chunks).toString()));
      } catch {
        resolve2({});
      }
    });
    req.on("error", reject);
  });
}
function resolveRepo(body) {
  return body.repo ?? inferRepoSlugFromPath(body.repo_path) ?? void 0;
}
function scheduleMaintenanceLoop() {
  if (!maintenanceConfig.enabled) return;
  const run = async () => {
    if (maintenanceRunning) return;
    maintenanceRunning = true;
    try {
      const result = await runMaintenanceCycle(db, maintenanceConfig);
      if (shouldLogMaintenance(result)) {
        console.log(formatMaintenanceSummary(result));
      }
    } catch (error) {
      const message = error instanceof Error ? error.stack ?? error.message : String(error);
      console.error(`[recall] maintenance failed: ${message}`);
    } finally {
      maintenanceRunning = false;
    }
  };
  const intervalMs = Math.max(30, maintenanceConfig.interval_seconds) * 1e3;
  setTimeout(() => void run(), intervalMs).unref?.();
  const timer = setInterval(() => {
    void run();
  }, intervalMs);
  timer.unref?.();
}
var dispatcherDormantLogged = false;
async function runDispatcherOnce() {
  if (dispatcherRunning) return;
  const hasKey = hasProviderConfigured("anthropic") || hasProviderConfigured("azure-openai") || hasProviderConfigured("openai");
  if (!hasKey) {
    if (!dispatcherDormantLogged) {
      console.log("[recall] dispatcher dormant: no LLM provider configured (set one via 'recall maintenance credentials set <provider> <key>'; preview prompts via 'recall maintenance dispatch --preview')");
      dispatcherDormantLogged = true;
    }
    return;
  }
  dispatcherDormantLogged = false;
  dispatcherRunning = true;
  try {
    const report = await dispatchPendingTasks(db, {
      maxTasks: dispatcherConfig.maxTasksPerRun
    });
    if (report.attempted > 0 || report.applied > 0) {
      console.log(
        `[recall] dispatcher ${report.provider}: attempted=${report.attempted} applied=${report.applied} rejected=${report.rejected} released=${report.released}`
      );
      emit("dispatcher.tick", {
        provider: report.provider ?? "unknown",
        attempted: report.attempted,
        applied: report.applied,
        rejected: report.rejected
      });
    }
  } catch (error) {
    const message = error instanceof Error ? error.stack ?? error.message : String(error);
    console.error(`[recall] dispatcher failed: ${message}`);
  } finally {
    dispatcherRunning = false;
  }
}
var dispatchWakeTimer = null;
var DISPATCH_WAKE_DEBOUNCE_MS = 3e3;
function wakeDispatcherDebounced() {
  if (!dispatcherConfig.enabled) return;
  if (dispatchWakeTimer) return;
  dispatchWakeTimer = setTimeout(() => {
    dispatchWakeTimer = null;
    void runDispatcherOnce();
  }, DISPATCH_WAKE_DEBOUNCE_MS);
  dispatchWakeTimer.unref?.();
}
function scheduleDispatcherLoop() {
  if (!dispatcherConfig.enabled) return;
  const timer = setInterval(() => {
    void runDispatcherOnce();
  }, Math.max(60, dispatcherConfig.intervalSeconds) * 1e3);
  timer.unref?.();
}
function scheduleCleanupLoop() {
  if (!cleanupConfig.enabled) return;
  const run = async () => {
    if (cleanupRunning) return;
    cleanupRunning = true;
    try {
      const report = runDeterministicCleanup(db, { dryRun: false });
      const c = report.counts;
      const total = c.dedupe_clusters + c.fragment_rejections + c.repeat_promotions + c.command_suppressions + c.globalizations;
      if (total > 0) {
        console.log(
          `[recall] cleanup run=${report.run_id.slice(0, 8)} merges=${c.dedupe_clusters}/${c.dedupe_losers} fragments=${c.fragment_rejections} promotions=${c.repeat_promotions} suppress=${c.command_suppressions} globalize=${c.globalizations}/${c.globalize_losers}`
        );
        emit("cleanup.tick", {
          run_id: report.run_id,
          merges: c.dedupe_clusters,
          promotions: c.repeat_promotions,
          suppressions: c.command_suppressions
        });
      }
      const newContradictions = detectContradictions(db);
      if (newContradictions.length > 0) {
        console.log(
          `[recall] contradictions detected: ${newContradictions.length} new pair(s)`
        );
        for (const c2 of newContradictions.slice(0, 5)) {
          console.log(`  [${c2.severity}] ${c2.contradiction_type}: ${c2.description.slice(0, 120)}`);
        }
        for (const c2 of newContradictions) {
          emit("contradiction.detected", {
            contradiction_id: c2.id,
            memory_a: c2.memory_a_id,
            memory_b: c2.memory_b_id,
            severity: c2.severity
          });
        }
      }
    } catch (error) {
      const message = error instanceof Error ? error.stack ?? error.message : String(error);
      console.error(`[recall] cleanup failed: ${message}`);
    } finally {
      cleanupRunning = false;
    }
  };
  setTimeout(() => void run(), 3e4).unref?.();
  const timer = setInterval(() => {
    void run();
  }, Math.max(60, cleanupConfig.intervalSeconds) * 1e3);
  timer.unref?.();
}
function scheduleQualitySnapshotLoop() {
  if (!qualitySnapshotConfig.enabled) return;
  const intervalMs = Math.max(60, qualitySnapshotConfig.intervalSeconds) * 1e3;
  const run = () => {
    if (qualitySnapshotRunning) return;
    qualitySnapshotRunning = true;
    try {
      const last = listQualitySnapshots(db, 1)[0];
      if (last) {
        const ageMs = Date.now() - new Date(last.taken_at).getTime();
        if (ageMs < intervalMs) return;
      }
      const report = computeQualityReport(db);
      const row = recordQualitySnapshot(db, report, "auto");
      console.log(
        `[recall] quality snapshot ${row.id.slice(0, 8)} followed=${row.followed_rate_resolved != null ? (row.followed_rate_resolved * 100).toFixed(1) + "%" : "n/a"} resolved=${row.injections_resolved} history=${row.history_injections_total} rules=${row.active_rule_count} cand=${row.candidate_correction_count}`
      );
    } catch (error) {
      const message = error instanceof Error ? error.stack ?? error.message : String(error);
      console.error(`[recall] quality snapshot failed: ${message}`);
    } finally {
      qualitySnapshotRunning = false;
    }
  };
  setTimeout(run, 6e4).unref?.();
  const timer = setInterval(run, 3600 * 1e3);
  timer.unref?.();
}
var server = createServer2(async (req, res) => {
  const url = new URL(req.url ?? "/", `http://localhost:${PORT}`);
  const path = url.pathname;
  const method = req.method ?? "GET";
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
  res.setHeader(
    "Access-Control-Allow-Headers",
    "Content-Type, Authorization, MCP-Protocol-Version, MCP-Session-Id, Last-Event-ID"
  );
  if (method === "OPTIONS") {
    res.statusCode = 204;
    res.end();
    return;
  }
  try {
    if (path === "/mcp") {
      return await handleRecallMcpHttpRequest(req, res, db);
    }
    res.setHeader("Content-Type", "application/json");
    if (path === "/health" && method === "GET") {
      return send(res, 200, {
        status: "ok",
        version: pkg.version,
        embeddings: getEmbeddingModelInfo()
      });
    }
    if (path === "/compile" && method === "POST") {
      const body = await parseBody(req);
      const repo = resolveRepo(body);
      if (!repo) return send(res, 400, { error: "repo or repo_path required" });
      const bootstrap = ensureRepoBootstrapped(db, {
        repo,
        repoPathHint: body.repo_path
      });
      if (bootstrap.status === "bootstrapped" || bootstrap.status === "scanned_empty") {
        createActivityEvent(db, {
          session_id: body.session_id ?? null,
          repo,
          source: "daemon",
          event_type: "scan",
          memory_ids: bootstrap.created_ids,
          request: {
            repo_path: bootstrap.repo_path,
            trigger: "compile_auto_bootstrap"
          },
          result: {
            created: bootstrap.created_ids.length,
            status: bootstrap.status
          }
        });
      }
      const result = body.query_text || body.config?.include_candidates ? await compileContextHybrid(db, {
        repo,
        path: body.path,
        session_id: body.session_id,
        query_text: body.query_text,
        config: body.config
      }) : compileContext(db, {
        repo,
        path: body.path,
        session_id: body.session_id,
        config: body.config
      });
      createActivityEvent(db, {
        session_id: body.session_id ?? null,
        repo,
        path: body.path ?? null,
        source: "daemon",
        event_type: "compile",
        memory_ids: result.memories_included,
        request: {
          config: body.config ?? {},
          query_text: body.query_text ?? null,
          bootstrap_status: bootstrap.status
        },
        result: {
          included: result.memories_included,
          dropped: result.memories_dropped,
          history_included: result.history_included,
          token_estimate: result.token_estimate,
          repo_path: bootstrap.repo_path
        }
      });
      return send(res, 200, {
        ...result,
        repo,
        repo_path: bootstrap.repo_path ?? body.repo_path ?? null,
        bootstrap_status: bootstrap.status
      });
    }
    if (path === "/session/start" && method === "POST") {
      const body = await parseBody(req);
      if (!body.session_id) {
        return send(res, 400, { error: "session_id required" });
      }
      const result = startSessionLifecycle(db, {
        session_id: body.session_id,
        client: body.client ?? null,
        repo: body.repo ?? null,
        repo_path: body.repo_path ?? null,
        path: body.path ?? null,
        meta: body.meta ?? {}
      });
      emit("session.started", {
        session_id: body.session_id,
        client: body.client ?? null,
        repo: body.repo ?? null
      });
      return send(res, 200, result);
    }
    if (path === "/session/event" && method === "POST") {
      const body = await parseBody(req);
      if (!body.session_id || !body.name) {
        return send(res, 400, { error: "session_id and name required" });
      }
      const result = recordSessionLifecycleEvent(db, {
        session_id: body.session_id,
        client: body.client ?? null,
        repo: body.repo ?? null,
        repo_path: body.repo_path ?? null,
        path: body.path ?? null,
        meta: body.meta ?? {},
        name: body.name,
        payload: body.payload ?? {}
      });
      return send(res, 200, result);
    }
    if (path === "/session/end" && method === "POST") {
      const body = await parseBody(req);
      if (!body.session_id) {
        return send(res, 400, { error: "session_id required" });
      }
      const result = endSessionLifecycle(db, {
        session_id: body.session_id,
        client: body.client ?? null,
        repo: body.repo ?? null,
        repo_path: body.repo_path ?? null,
        path: body.path ?? null,
        meta: body.meta ?? {},
        payload: body.payload ?? {}
      });
      emit("session.ended", {
        session_id: body.session_id,
        repo: body.repo ?? null
      });
      return send(res, 200, result);
    }
    if (path === "/graph/stats" && method === "GET") {
      return send(res, 200, {
        entities: countEntities(db),
        relations: countRelations(db)
      });
    }
    if (path === "/graph/relations" && method === "GET") {
      const repo = url.searchParams.get("repo") ?? void 0;
      const limit = parseInt(url.searchParams.get("limit") ?? "2000", 10);
      const rows = listAllRelations(db, { repo, limit });
      return send(res, 200, { count: rows.length, relations: rows });
    }
    if (path === "/graph/entities" && method === "GET") {
      const repo = url.searchParams.get("repo") ?? void 0;
      const kind = url.searchParams.get("kind");
      const search = url.searchParams.get("search") ?? void 0;
      const limit = parseInt(url.searchParams.get("limit") ?? "100", 10);
      const rows = listEntities(db, {
        repo,
        kind: kind ?? void 0,
        search,
        limit
      });
      return send(res, 200, {
        count: rows.length,
        entities: rows
      });
    }
    if (path === "/graph/query" && method === "POST") {
      const body = await parseBody(req);
      if (!body.query || typeof body.query !== "string") {
        return send(res, 400, { error: "query (string) required" });
      }
      const embeddingConfig = loadEmbeddingConfigFromEnv();
      const result = await graphQuery(db, body.query, embeddingConfig, {
        repo: body.repo,
        hops: typeof body.hops === "number" ? body.hops : void 0,
        limit: typeof body.limit === "number" ? body.limit : void 0,
        relationTypes: Array.isArray(body.relation_types) ? body.relation_types : void 0
      });
      return send(res, 200, {
        seed_count: result.seed_count,
        expanded_entities: result.expanded_entities,
        hits: result.hits.map((h) => ({
          memory_id: h.memory.id,
          text: h.memory.text,
          repo: h.memory.repo,
          scope: h.memory.scope,
          status: h.memory.status,
          via: h.via,
          score: h.score,
          hops: h.hops,
          shared_entities: h.shared_entities
        })),
        entities: result.entities
      });
    }
    if (path.startsWith("/graph/entity/") && method === "GET") {
      const id = path.slice("/graph/entity/".length);
      const ent = getEntity(db, id);
      if (!ent) return send(res, 404, { error: "entity not found" });
      return send(res, 200, {
        entity: ent,
        memories: listMemoryIdsForEntity(db, id)
      });
    }
    if (path === "/graph/neighbors" && method === "POST") {
      const body = await parseBody(req);
      if (!body.entity_id || typeof body.entity_id !== "string") {
        return send(res, 400, { error: "entity_id (string) required" });
      }
      const root = getEntity(db, body.entity_id);
      if (!root) return send(res, 404, { error: "entity not found" });
      const walk = neighborsOf(db, body.entity_id, {
        hops: typeof body.hops === "number" ? body.hops : void 0,
        relationTypes: Array.isArray(body.relation_types) ? body.relation_types : void 0
      });
      const includeMemories = body.include_memories !== false;
      const memoriesByEntity = includeMemories ? Object.fromEntries(walk.entities.map((e) => [e.id, listMemoryIdsForEntity(db, e.id)])) : {};
      return send(res, 200, {
        root,
        entities: walk.entities,
        relations: walk.relations,
        memories_by_entity: memoriesByEntity
      });
    }
    if (path.startsWith("/graph/memory/") && method === "GET") {
      const memId = path.slice("/graph/memory/".length);
      const ents = listEntitiesForMemory(db, memId);
      return send(res, 200, { memory_id: memId, entities: ents });
    }
    if (path === "/webui/status" && method === "GET") {
      return send(res, 200, getStatus());
    }
    if (path === "/webui/start" && method === "POST") {
      const body = await parseBody(req).catch(() => ({}));
      const status = await start({
        port: typeof body.port === "number" ? body.port : void 0
      });
      if (body.open !== false && status.url) {
        openInBrowser(status.url);
      }
      return send(res, 200, status);
    }
    if (path === "/webui/stop" && method === "POST") {
      const status = await stop();
      return send(res, 200, status);
    }
    if (path === "/dispatch/wake" && method === "POST") {
      wakeDispatcherDebounced();
      return send(res, 202, { status: "queued", debounce_ms: DISPATCH_WAKE_DEBOUNCE_MS });
    }
    if (path === "/hook/prompt" && method === "POST") {
      const body = await parseBody(req);
      if (!body.text) {
        return send(res, 400, { error: "text required" });
      }
      const result = await handlePromptHook(body, {
        db,
        source: "daemon"
      });
      return send(res, 200, { ...result, transport: "daemon" });
    }
    if (path === "/hook/tool" && method === "POST") {
      const body = await parseBody(req);
      if (!body.name || typeof body.exit_code !== "number") {
        return send(res, 400, { error: "name and numeric exit_code required" });
      }
      const result = await handleToolHook(body, {
        db,
        source: "daemon"
      });
      return send(res, 200, { ...result, transport: "daemon" });
    }
    if (path === "/hook/session-start" && method === "POST") {
      const body = await parseBody(req);
      if (!body.session_id || !body.agent) {
        return send(res, 400, { error: "session_id and agent required" });
      }
      const result = await handleSessionStartHook(body, {
        db,
        source: "daemon"
      });
      return send(res, 200, { ...result, transport: "daemon" });
    }
    if (path === "/hook/session-end" && method === "POST") {
      const body = await parseBody(req);
      if (!body.session_id) {
        return send(res, 400, { error: "session_id required" });
      }
      const result = await handleSessionEndHook(body, {
        db,
        source: "daemon"
      });
      return send(res, 200, { ...result, transport: "daemon" });
    }
    if (path === "/correct" && method === "POST") {
      const body = await parseBody(req);
      const repo = resolveRepo(body);
      const { ids, pendingTaskId } = await processCorrection(db, body.text, {
        sessionId: body.session_id ?? "hook",
        repo,
        path: body.path
      });
      createActivityEvent(db, {
        session_id: body.session_id ?? "hook",
        repo: repo ?? null,
        path: body.path ?? null,
        source: "daemon",
        event_type: "correction",
        memory_ids: ids,
        request: { text: body.text },
        result: { created: ids, pending_task_id: pendingTaskId ?? null }
      });
      for (const id of ids) {
        safeIngestMemory(id);
        emit("memory.created", {
          memory_id: id,
          repo: repo ?? null,
          source: "correction"
        });
      }
      return send(res, 200, { created: ids, pending_task_id: pendingTaskId ?? null });
    }
    if (path === "/review" && method === "POST") {
      const body = await parseBody(req);
      const repo = resolveRepo(body);
      const ids = await processReviewFeedback(db, body.feedback, {
        sessionId: body.session_id ?? "hook-review",
        repo,
        path: body.path,
        reviewer: body.reviewer
      });
      createActivityEvent(db, {
        session_id: body.session_id ?? "hook-review",
        repo: repo ?? null,
        path: body.path ?? null,
        source: "daemon",
        event_type: "review",
        memory_ids: ids,
        request: { feedback: body.feedback, reviewer: body.reviewer ?? null },
        result: { created: ids }
      });
      for (const id of ids) {
        safeIngestMemory(id);
        emit("memory.created", {
          memory_id: id,
          repo: repo ?? null,
          source: "review"
        });
      }
      return send(res, 200, { created: ids });
    }
    if (path === "/confirm" && method === "POST") {
      const body = await parseBody(req);
      const ok = confirmMemory(db, body.memory_id);
      if (ok) {
        const mem = getMemory(db, body.memory_id);
        emit("memory.confirmed", {
          memory_id: body.memory_id,
          repo: mem?.repo ?? null
        });
      }
      return send(res, ok ? 200 : 404, { success: ok });
    }
    if (path === "/reject" && method === "POST") {
      const body = await parseBody(req);
      const before = getMemory(db, body.memory_id);
      const ok = rejectMemory(db, body.memory_id);
      if (ok) {
        const after = getMemory(db, body.memory_id);
        recordAudit(
          db,
          body.memory_id,
          "rejected",
          "daemon:http",
          "manual reject",
          before ? JSON.stringify(before) : null,
          after ? JSON.stringify(after) : null
        );
        emit("memory.rejected", {
          memory_id: body.memory_id,
          repo: before?.repo ?? null,
          actor: "daemon:http"
        });
      }
      return send(res, ok ? 200 : 404, { success: ok });
    }
    if (path === "/feedback" && method === "POST") {
      const body = await parseBody(req);
      const id = recordFeedback(
        db,
        body.memory_id,
        body.session_id,
        body.injected,
        body.outcome
      );
      createActivityEvent(db, {
        session_id: body.session_id,
        source: "daemon",
        event_type: "feedback",
        memory_ids: [body.memory_id],
        request: { injected: body.injected, outcome: body.outcome },
        result: { feedback_id: id }
      });
      emit("feedback.recorded", {
        memory_id: body.memory_id,
        session_id: body.session_id,
        outcome: String(body.outcome ?? ""),
        injected: Boolean(body.injected)
      });
      return send(res, 200, { feedback_id: id });
    }
    if (path === "/memories" && method === "GET") {
      const repo = url.searchParams.get("repo") ?? void 0;
      const status = url.searchParams.get("status");
      const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
      const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
      const items = queryMemories(db, { repo, status, limit, offset });
      return send(res, 200, {
        memories: items,
        offset,
        limit,
        has_more: items.length === limit
      });
    }
    if (path.startsWith("/memory/") && method === "GET") {
      const id = path.slice("/memory/".length);
      const mem = getMemory(db, id);
      if (!mem) return send(res, 404, { error: "not found" });
      return send(res, 200, mem);
    }
    if (path === "/scan" && method === "POST") {
      const body = await parseBody(req);
      const ids = scanAndStore(db, body.repo_path);
      const mem = ids[0] ? getMemory(db, ids[0]) : void 0;
      const artifact = writeRepoContextArtifact(db, {
        repo: mem?.repo ?? null,
        repo_path: body.repo_path
      });
      createActivityEvent(db, {
        session_id: body.session_id ?? null,
        repo: mem?.repo ?? null,
        source: "daemon",
        event_type: "scan",
        memory_ids: ids,
        request: { repo_path: body.repo_path },
        result: {
          created: ids.length,
          artifact_path: artifact.output_path,
          artifact_written: artifact.written
        }
      });
      emit("scan.completed", {
        repo: mem?.repo ?? null,
        created: ids.length,
        repo_path: body.repo_path
      });
      for (const id of ids) {
        safeIngestMemory(id);
        const m = getMemory(db, id);
        emit("memory.created", {
          memory_id: id,
          repo: m?.repo ?? null,
          source: "scan",
          type: m?.type
        });
      }
      return send(res, 200, {
        created: ids,
        count: ids.length,
        artifact_path: artifact.output_path,
        artifact_written: artifact.written
      });
    }
    if (path === "/eval/metrics" && method === "GET") {
      const repo = url.searchParams.get("repo") ?? void 0;
      const since = url.searchParams.get("since") ?? void 0;
      const metrics = computeMetrics(db, { repo, since });
      return send(res, 200, metrics);
    }
    if (path === "/eval/start" && method === "POST") {
      const body = await parseBody(req);
      const id = startEvalSession(db, body.repo);
      return send(res, 200, { session_id: id });
    }
    if (path === "/eval/end" && method === "POST") {
      const body = await parseBody(req);
      endEvalSession(db, body.session_id);
      return send(res, 200, { success: true });
    }
    if (path === "/eval/increment" && method === "POST") {
      const body = await parseBody(req);
      incrementEvalCounter(db, body.session_id, body.field, body.amount ?? 1);
      return send(res, 200, { success: true });
    }
    if (path === "/signal" && method === "POST") {
      const body = await parseBody(req);
      const id = recordSignal(
        db,
        body.memory_id,
        body.session_id ?? "daemon",
        body.signal_type,
        body.context
      );
      const mem = getMemory(db, body.memory_id);
      createActivityEvent(db, {
        session_id: body.session_id ?? "daemon",
        repo: mem?.repo ?? null,
        path: mem?.path_scope ?? null,
        source: "daemon",
        event_type: "signal",
        memory_ids: [body.memory_id],
        request: { signal_type: body.signal_type, context: body.context ?? null },
        result: { signal_id: id }
      });
      return send(res, 200, { signal_id: id });
    }
    if (path.startsWith("/signal/stats/") && method === "GET") {
      const memId = path.slice("/signal/stats/".length);
      const stats = getSignalStats(db, memId);
      return send(res, 200, stats);
    }
    if (path === "/test" && method === "POST") {
      const body = await parseBody(req);
      const testResult = runTests(body.repo_path, body.command);
      const signalIds = recordTestSignals(
        db,
        body.session_id ?? "daemon",
        body.memory_ids ?? [],
        testResult
      );
      return send(res, 200, {
        passed: testResult.passed,
        signals: signalIds,
        output: testResult.output?.slice(0, 1e3)
      });
    }
    if (path === "/quality" && method === "GET") {
      const repo = url.searchParams.get("repo") ?? void 0;
      return send(res, 200, getRepoQualityProfile(db, repo));
    }
    if (path === "/activity" && method === "GET") {
      const repo = url.searchParams.get("repo") ?? void 0;
      const session_id = url.searchParams.get("session_id") ?? void 0;
      const source = url.searchParams.get("source");
      const event_type = url.searchParams.get("event_type");
      const since = url.searchParams.get("since") ?? void 0;
      const limit = parseInt(url.searchParams.get("limit") ?? "20", 10);
      const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
      const events = listActivityEvents(db, {
        repo,
        session_id,
        source,
        event_type,
        since,
        limit,
        offset
      });
      return send(res, 200, { events, offset, limit, has_more: events.length === limit });
    }
    if (path === "/sessions" && method === "GET") {
      const repo = url.searchParams.get("repo") ?? void 0;
      const source = url.searchParams.get("source");
      const event_type = url.searchParams.get("event_type");
      const since = url.searchParams.get("since") ?? void 0;
      const limit = parseInt(url.searchParams.get("limit") ?? "20", 10);
      const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
      const grouped = listActivitySessions(db, {
        repo,
        source,
        event_type,
        since,
        limit: Math.max(limit + offset, 200)
      });
      const page = grouped.slice(offset, offset + limit);
      return send(res, 200, {
        sessions: page,
        offset,
        limit,
        has_more: grouped.length > offset + limit
      });
    }
    if (path === "/policy/list" && method === "GET") {
      const orgId = url.searchParams.get("org_id") ?? "";
      return send(res, 200, { policies: listPolicies(db, orgId) });
    }
    if (path === "/policy" && method === "POST") {
      const body = await parseBody(req);
      const id = createPolicy(db, body.org_id, body.rule_type, body.config);
      return send(res, 200, { policy_id: id });
    }
    if (path === "/policy/check" && method === "POST") {
      const body = await parseBody(req);
      const mem = getMemory(db, body.memory_id);
      if (!mem) return send(res, 404, { error: "memory not found" });
      const violations = evaluatePolicy(db, body.org_id, mem);
      return send(res, 200, { violations });
    }
    if (path === "/approval/request" && method === "POST") {
      const body = await parseBody(req);
      const id = requestApproval(db, body.memory_id, body.org_id, body.requested_by ?? "daemon");
      return send(res, 200, { approval_id: id });
    }
    if (path === "/approval/pending" && method === "GET") {
      const orgId = url.searchParams.get("org_id") ?? "";
      return send(res, 200, { approvals: listPendingApprovals(db, orgId) });
    }
    if (path === "/approval/resolve" && method === "POST") {
      const body = await parseBody(req);
      const ok = resolveApproval(db, body.approval_id, body.status, body.reviewed_by ?? "daemon", body.reason);
      return send(res, ok ? 200 : 404, { success: ok });
    }
    if (path.startsWith("/health/") && method === "GET") {
      const memId = path.slice("/health/".length);
      const score = computeHealthScore(db, memId);
      if (!score) return send(res, 404, { error: "not found" });
      return send(res, 200, score);
    }
    if (path === "/health" && method === "GET") {
      const repo = url.searchParams.get("repo") ?? void 0;
      const scores = computeAllHealthScores(db, repo);
      return send(res, 200, { scores });
    }
    if (path === "/contradictions/detect" && method === "POST") {
      const body = await parseBody(req);
      const found = detectContradictions(db, body.repo);
      for (const c of found) {
        emit("contradiction.detected", {
          contradiction_id: c.id,
          memory_a: c.memory_a_id,
          memory_b: c.memory_b_id,
          severity: c.severity
        });
      }
      return send(res, 200, { contradictions: found });
    }
    if (path === "/contradictions" && method === "GET") {
      const resolved = url.searchParams.get("resolved");
      const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
      const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
      const items = listContradictions(db, {
        resolved: resolved === "true" ? true : resolved === "false" ? false : void 0,
        limit,
        offset
      });
      return send(res, 200, {
        contradictions: items,
        offset,
        limit,
        has_more: items.length === limit
      });
    }
    if (path === "/contradictions/resolve" && method === "POST") {
      const body = await parseBody(req);
      const ok = resolveContradiction(db, body.contradiction_id, body.keep_memory_id, body.actor ?? "daemon", body.resolution);
      if (ok) {
        emit("contradiction.resolved", {
          contradiction_id: body.contradiction_id,
          keep_memory_id: body.keep_memory_id
        });
      }
      return send(res, ok ? 200 : 404, { success: ok });
    }
    if (path === "/contradictions/auto-resolve" && method === "POST") {
      const body = await parseBody(req);
      const count = autoResolveContradictions(db, body.repo);
      return send(res, 200, { resolved: count });
    }
    if (path === "/prune" && method === "POST") {
      const body = await parseBody(req);
      const result = pruneMemories(db, body.config);
      return send(res, 200, result);
    }
    if (path.startsWith("/audit/memory/") && method === "GET") {
      const memId = path.slice("/audit/memory/".length);
      const entries = getAuditTrail(db, memId);
      return send(res, 200, { entries });
    }
    if (path === "/audit/recent" && method === "GET") {
      const limit = parseInt(url.searchParams.get("limit") ?? "50");
      const entries = getRecentAudit(db, limit);
      return send(res, 200, { entries });
    }
    if (path === "/audit/rollback" && method === "POST") {
      const body = await parseBody(req);
      const ok = rollbackMemory(db, body.memory_id, body.audit_entry_id, body.actor ?? "daemon");
      return send(res, ok ? 200 : 404, { success: ok });
    }
    send(res, 404, { error: "not found" });
  } catch (err) {
    send(res, 500, { error: err.message });
  }
});
function send(res, status, data) {
  res.statusCode = status;
  res.end(JSON.stringify(data));
}
function openInBrowser(url) {
  const platform = process.platform;
  try {
    if (platform === "darwin") {
      spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
    } else if (platform === "win32") {
      spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
    } else {
      spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
    }
  } catch (err) {
    console.error(`[recall] failed to open browser for ${url}:`, err);
  }
}
async function startDaemon() {
  const backup = ensureDailyBackup();
  if (backup.created) {
    console.log(`[recall] backup created ${backup.created} (retained ${backup.retained.length})`);
  }
  db = initDb();
  server.listen(PORT, () => {
    console.log(`Recall daemon listening on http://localhost:${PORT}`);
    scheduleMaintenanceLoop();
    scheduleDispatcherLoop();
    scheduleCleanupLoop();
    scheduleQualitySnapshotLoop();
    setTimeout(() => {
      const embeddingConfig = loadEmbeddingConfigFromEnv();
      if (!embeddingConfig) return;
      const info = getEmbeddingModelInfo(embeddingConfig);
      if (info && !info.cached) {
        const approx = info.estimated_size_mb ? `~${info.estimated_size_mb}MB` : "download";
        console.log(`[recall] Fetching embedding model (one-time, ${approx}) -> ${info.cache_path}`);
      }
      void ensureEmbeddingProviderReady(embeddingConfig).catch((error) => {
        const message = error instanceof Error ? error.stack ?? error.message : String(error);
        console.error(`[recall] embedding provider warmup failed: ${message}`);
      });
    }, 6e4).unref?.();
  });
}
void startDaemon().catch((error) => {
  const message = error instanceof Error ? error.stack ?? error.message : String(error);
  console.error(`[recall] daemon startup failed: ${message}`);
  process.exit(1);
});
export {
  isRunning as webUiIsRunning
};
//# sourceMappingURL=daemon.js.map